feat: AI context, Webhooks, Kanban drag-drop, and UI fixes
This commit is contained in:
@@ -55,19 +55,19 @@
|
||||
<Controls />
|
||||
<MiniMap pannable zoomable class="bg-slate-800 border-slate-700" />
|
||||
<template #node-text="{ data, id }">
|
||||
<FlowNodeRenderer :id="id" :data="data" type="text" :selected="selectedNode?.id === id" @update="updateNodeData" />
|
||||
<FlowNodeRenderer :id="id" :data="data" type="text" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
|
||||
</template>
|
||||
<template #node-file="{ data, id }">
|
||||
<FlowNodeRenderer :id="id" :data="data" type="file" :selected="selectedNode?.id === id" @update="updateNodeData" />
|
||||
<FlowNodeRenderer :id="id" :data="data" type="file" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
|
||||
</template>
|
||||
<template #node-task="{ data, id }">
|
||||
<FlowNodeRenderer :id="id" :data="data" type="task" :selected="selectedNode?.id === id" @update="updateNodeData" />
|
||||
<FlowNodeRenderer :id="id" :data="data" type="task" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
|
||||
</template>
|
||||
<template #node-ai="{ data, id }">
|
||||
<FlowNodeRenderer :id="id" :data="data" type="ai" :selected="selectedNode?.id === id" @update="updateNodeData" />
|
||||
<FlowNodeRenderer :id="id" :data="data" type="ai" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
|
||||
</template>
|
||||
<template #node-webhook="{ data, id }">
|
||||
<FlowNodeRenderer :id="id" :data="data" type="webhook" :selected="selectedNode?.id === id" @update="updateNodeData" />
|
||||
<FlowNodeRenderer :id="id" :data="data" type="webhook" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
|
||||
</template>
|
||||
<template #node-git="{ data, id }">
|
||||
<FlowNodeRenderer :id="id" :data="data" type="git" :selected="selectedNode?.id === id" @update="updateNodeData" />
|
||||
@@ -134,6 +134,21 @@
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Дедлайн (Срок)</label>
|
||||
<input v-model="selectedNode.data.alarm" @blur="commitNodeChange" type="datetime-local" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
|
||||
</div>
|
||||
<div v-if="selectedNode.type === 'webhook'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Webhook URL</label>
|
||||
<input v-model="selectedNode.data.url" @blur="commitNodeChange" placeholder="https://api.example.com/..." class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
|
||||
</div>
|
||||
<div v-if="selectedNode.type === 'webhook'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Метод</label>
|
||||
<select v-model="selectedNode.data.method" @change="commitNodeChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200">
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="selectedNode.type === 'ai'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">AI Промпт (Инструкция)</label>
|
||||
<textarea v-model="selectedNode.data.prompt" @blur="commitNodeChange" rows="3" placeholder="Что должен сделать ИИ? (например: Отредактируй этот текст...)" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none resize-none font-mono"></textarea>
|
||||
</div>
|
||||
<div v-if="selectedNode.data.script_id !== undefined">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Скрипт</label>
|
||||
<input v-model="selectedNode.data.script_id" @blur="commitNodeChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none font-mono" />
|
||||
@@ -168,6 +183,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Force rebuild to fix Vite 404 issue -->
|
||||
<script setup>
|
||||
import { VueFlow } from '@vue-flow/core'
|
||||
import { Background } from '@vue-flow/background'
|
||||
@@ -429,34 +445,65 @@ const onNodeDoubleClick = (event) => {
|
||||
const node = event.node
|
||||
|
||||
// 1. Cross-Canvas Navigation Logic
|
||||
// Check if it's a file node or has a URL/file reference to another canvas
|
||||
const targetId = node.data?.file || node.data?.url || ''
|
||||
|
||||
if (targetId && (node.type === 'file' || targetId.includes('.canvas') || targetId.startsWith('/canvas/'))) {
|
||||
// Clean up ID (e.g. "project.canvas" -> "project", "/canvas/project" -> "project")
|
||||
const cleanId = targetId.replace('.canvas', '').replace('/canvas/', '')
|
||||
console.log('[Navigation] Opening cross-linked canvas:', cleanId)
|
||||
navigateTo(`/canvas/${cleanId}`)
|
||||
return // Stop further processing
|
||||
return
|
||||
} else if (node.type === 'file' && !targetId) {
|
||||
alert('У этого узла еще нет ссылки на другую карту. Выберите карту в панели свойств (справа).')
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Task Toggle Logic (fallback)
|
||||
const currentState = node.data?.state || 'todo'
|
||||
|
||||
// Quick toggle workflow: todo -> in_progress -> review -> completed -> todo
|
||||
const nextStateMap = {
|
||||
'todo': 'in_progress',
|
||||
'in_progress': 'review',
|
||||
'review': 'completed',
|
||||
'completed': 'todo',
|
||||
'blocked': 'todo'
|
||||
if (node.type === 'task') {
|
||||
const currentState = node.data?.state || 'todo'
|
||||
const nextStateMap = {
|
||||
'todo': 'in_progress',
|
||||
'in_progress': 'review',
|
||||
'review': 'completed',
|
||||
'completed': 'todo',
|
||||
'blocked': 'todo'
|
||||
}
|
||||
const nextState = nextStateMap[currentState] || 'todo'
|
||||
updateNodeData({ id: node.id, data: { state: nextState } })
|
||||
}
|
||||
}
|
||||
|
||||
const executeNode = async ({ id, type }) => {
|
||||
console.log(`[FlowEditor] Executing node ${id} of type ${type}`)
|
||||
|
||||
const nextState = nextStateMap[currentState] || 'todo'
|
||||
updateNodeData({ id: node.id, data: { state: nextState } })
|
||||
// Collect context from incoming nodes
|
||||
const incomingEdges = elements.value.filter(el => el.target === id)
|
||||
const contextNodes = incomingEdges.map(edge => {
|
||||
const sourceNode = elements.value.find(el => el.id === edge.source)
|
||||
return {
|
||||
id: sourceNode?.id,
|
||||
type: sourceNode?.type,
|
||||
text: sourceNode?.data?.text || sourceNode?.data?.label || ''
|
||||
}
|
||||
}).filter(n => n.text)
|
||||
|
||||
try {
|
||||
const res = await $fetch(`/api/nodes/${id}/execute`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
canvasId: props.canvasId,
|
||||
context: contextNodes
|
||||
},
|
||||
headers: authHeaders.value
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
console.log(`[FlowEditor] Execution successful:`, res.result)
|
||||
// Перезагружаем данные, чтобы увидеть результат
|
||||
await loadData()
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[FlowEditor] Execution failed:`, err)
|
||||
alert('Ошибка при выполнении узла: ' + (err.message || 'Неизвестная ошибка'))
|
||||
}
|
||||
}
|
||||
|
||||
const onConnect = (connection) => {
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
<div v-if="type === 'task' && data.priority" :class="priorityBadgeClass">
|
||||
{{ priorityLabel }}
|
||||
</div>
|
||||
<!-- Кнопка запуска для спец-узлов -->
|
||||
<button
|
||||
v-if="['ai', 'webhook'].includes(type)"
|
||||
@click.stop="handleExecute"
|
||||
:disabled="isExecuting"
|
||||
class="ml-auto p-1.5 hover:bg-slate-700 rounded-md transition-colors group relative"
|
||||
title="Запустить выполнение"
|
||||
>
|
||||
<span v-if="isExecuting" class="block animate-spin text-blue-400">🌀</span>
|
||||
<span v-else class="block group-hover:scale-110 transition-transform">▶️</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content preview for measurement -->
|
||||
@@ -69,7 +80,19 @@ const props = defineProps({
|
||||
selected: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
const emit = defineEmits(['update', 'execute'])
|
||||
const isExecuting = ref(false)
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (isExecuting.value) return
|
||||
isExecuting.value = true
|
||||
try {
|
||||
emit('execute', { id: props.id, type: props.type })
|
||||
} finally {
|
||||
// Состояние загрузки сбрасывается внешним событием или по таймауту для безопасности
|
||||
setTimeout(() => { isExecuting.value = false }, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
// Root element ref
|
||||
const rootEl = ref(null)
|
||||
|
||||
+78
-29
@@ -29,35 +29,45 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="p-4 flex-1 overflow-y-auto space-y-4 custom-scrollbar">
|
||||
<div
|
||||
v-for="task in getTasksForColumn(col.id)"
|
||||
:key="task.id"
|
||||
class="bg-slate-900 border border-slate-600 rounded-lg p-4 shadow-sm hover:border-blue-500 transition-colors cursor-pointer group"
|
||||
@click="openTask(task)"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="text-xs font-medium px-2 py-0.5 rounded" :class="getPriorityClass(task)">
|
||||
{{ getPriorityLabel(task) }}
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-500 font-mono">{{ task.id.split('-')[1] || task.id }}</div>
|
||||
</div>
|
||||
|
||||
<h4 class="font-medium text-white mb-2 line-clamp-2">{{ getTaskTitle(task) }}</h4>
|
||||
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="text-xs text-slate-400 flex items-center gap-1">
|
||||
<span>🗺️</span>
|
||||
<span class="truncate max-w-[120px]">{{ task.canvasName }}</span>
|
||||
</div>
|
||||
<div v-if="task.alarm || task.data?.alarm" class="text-xs text-orange-400 flex items-center gap-1" :title="new Date(task.alarm || task.data?.alarm).toLocaleString('ru-RU')">
|
||||
<span>⏰</span>
|
||||
<span>{{ formatShortDate(task.alarm || task.data?.alarm) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 flex-1 overflow-y-auto space-y-4 custom-scrollbar min-h-[200px]">
|
||||
<ClientOnly>
|
||||
<draggable
|
||||
v-model="columnsData[col.id]"
|
||||
group="tasks"
|
||||
item-key="id"
|
||||
class="space-y-4 min-h-[200px]"
|
||||
@change="(evt) => onTaskMove(evt, col.id)"
|
||||
>
|
||||
<template #item="{ element: task }">
|
||||
<div
|
||||
class="bg-slate-900 border border-slate-600 rounded-lg p-4 shadow-sm hover:border-blue-500 transition-colors cursor-pointer group"
|
||||
@click="openTask(task)"
|
||||
>
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="text-xs font-medium px-2 py-0.5 rounded" :class="getPriorityClass(task)">
|
||||
{{ getPriorityLabel(task) }}
|
||||
</div>
|
||||
<div class="text-[10px] text-slate-500 font-mono">{{ task.id.split('-')[1] || task.id }}</div>
|
||||
</div>
|
||||
|
||||
<h4 class="font-medium text-white mb-2 line-clamp-2">{{ getTaskTitle(task) }}</h4>
|
||||
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<div class="text-xs text-slate-400 flex items-center gap-1">
|
||||
<span>🗺️</span>
|
||||
<span class="truncate max-w-[120px]">{{ task.canvasName }}</span>
|
||||
</div>
|
||||
<div v-if="task.alarm || task.data?.alarm" class="text-xs text-orange-400 flex items-center gap-1" :title="new Date(task.alarm || task.data?.alarm).toLocaleString('ru-RU')">
|
||||
<span>⏰</span>
|
||||
<span>{{ formatShortDate(task.alarm || task.data?.alarm) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</ClientOnly>
|
||||
|
||||
<div v-if="getTasksForColumn(col.id).length === 0" class="text-center py-6 border-2 border-dashed border-slate-700 rounded-lg">
|
||||
<div v-if="columnsData[col.id].length === 0" class="text-center py-6 border-2 border-dashed border-slate-700 rounded-lg pointer-events-none">
|
||||
<span class="text-slate-500 text-sm">Нет задач</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,11 +78,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ref, onMounted, computed, reactive } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
const auth = useAuth()
|
||||
const loading = ref(true)
|
||||
const tasks = ref([])
|
||||
const columnsData = reactive({
|
||||
todo: [],
|
||||
in_progress: [],
|
||||
review: [],
|
||||
blocked: [],
|
||||
completed: []
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ id: 'todo', title: 'К выполнению', icon: '📋', color: 'text-slate-400' },
|
||||
@@ -91,6 +109,14 @@ const fetchTasks = async () => {
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
tasks.value = data.tasks || []
|
||||
|
||||
// Split tasks into columns
|
||||
columns.forEach(col => {
|
||||
columnsData[col.id] = tasks.value.filter(t => {
|
||||
const taskState = t.state || t.data?.state || 'todo'
|
||||
return taskState === col.id
|
||||
})
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load tasks', e)
|
||||
@@ -99,6 +125,29 @@ const fetchTasks = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onTaskMove = async (evt, newStatus) => {
|
||||
if (evt.added) {
|
||||
const task = evt.added.element
|
||||
console.log(`Moving task ${task.id} to ${newStatus}`)
|
||||
|
||||
try {
|
||||
await $fetch('/api/tasks/update-status', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
taskId: task.id,
|
||||
canvasId: task.canvasId,
|
||||
newState: newStatus
|
||||
},
|
||||
headers: { Authorization: `Bearer ${auth.token.value}` }
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Failed to update task status', e)
|
||||
// Rollback on error? (In a production app, we should)
|
||||
alert('Не удалось обновить статус задачи на сервере')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getTasksForColumn = (state) => {
|
||||
return tasks.value.filter(t => {
|
||||
const taskState = t.state || t.data?.state || 'todo'
|
||||
|
||||
@@ -58,7 +58,11 @@ export default defineEventHandler(async (event) => {
|
||||
throw createError({ statusCode: 400, message: 'Invalid action' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Gitea sync error:', error)
|
||||
throw createError({ statusCode: 500, message: error?.message || 'Gitea operation failed' })
|
||||
const details = error.data || error.message || error
|
||||
console.error('Gitea sync error details:', details)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: typeof details === 'string' ? details : JSON.stringify(details)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
import { generateAIResponse } from '~/server/utils/ai'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
const nodeId = getRouterParam(event, 'id')
|
||||
const body = await readBody(event)
|
||||
const { canvasId, context } = body
|
||||
|
||||
if (!nodeId || !canvasId) {
|
||||
throw createError({ statusCode: 400, message: 'Node ID and Canvas ID are required' })
|
||||
}
|
||||
|
||||
// 1. Найти холст и убедиться в правах доступа
|
||||
const canvas = await prisma.canvas.findUnique({
|
||||
where: { id: canvasId }
|
||||
})
|
||||
|
||||
if (!canvas) {
|
||||
throw createError({ statusCode: 404, message: 'Canvas not found' })
|
||||
}
|
||||
|
||||
// Проверка прав (владелец или админ)
|
||||
if (canvas.ownerId !== user.id && user.role !== 'admin') {
|
||||
throw createError({ statusCode: 403, message: 'Access denied' })
|
||||
}
|
||||
|
||||
// 2. Найти конкретный узел в JSON холста
|
||||
const nodes = (canvas.nodes as any[]) || []
|
||||
const nodeIdx = nodes.findIndex(n => n.id === nodeId)
|
||||
|
||||
if (nodeIdx === -1) {
|
||||
throw createError({ statusCode: 404, message: 'Node not found in canvas' })
|
||||
}
|
||||
|
||||
const node = nodes[nodeIdx]
|
||||
|
||||
try {
|
||||
let result = ''
|
||||
|
||||
if (node.type === 'ai') {
|
||||
// Формируем промпт с учетом контекста от соседей
|
||||
let fullPrompt = node.prompt || node.text || 'Analyze the provided context.'
|
||||
|
||||
if (context && context.length > 0) {
|
||||
const contextText = context.map((c: any) => `[Node ${c.id}]: ${c.text}`).join('\n\n')
|
||||
fullPrompt = `CONTEXT INFORMATION:\n${contextText}\n\nUSER REQUEST/INSTRUCTION:\n${fullPrompt}`
|
||||
}
|
||||
|
||||
console.log(`[AI Execute] Generating response with context for node ${nodeId}...`)
|
||||
result = await generateAIResponse(fullPrompt)
|
||||
|
||||
// Сохраняем результат в текст узла
|
||||
nodes[nodeIdx].text = result
|
||||
|
||||
} else if (node.type === 'webhook') {
|
||||
const url = node.url
|
||||
const method = node.method || 'GET'
|
||||
|
||||
if (!url) throw new Error('Webhook URL is not configured')
|
||||
|
||||
console.log(`[Webhook Execute] Calling ${method} ${url}...`)
|
||||
|
||||
const response = await $fetch<any>(url, {
|
||||
method,
|
||||
...(method === 'POST' ? { body: { context, timestamp: new Date() } } : {})
|
||||
})
|
||||
|
||||
result = typeof response === 'string' ? response : JSON.stringify(response, null, 2)
|
||||
nodes[nodeIdx].text = `### Response from ${url}\n\n\`\`\`json\n${result}\n\`\`\``
|
||||
|
||||
} else {
|
||||
throw createError({ statusCode: 400, message: `Execution not supported for type: ${node.type}` })
|
||||
}
|
||||
|
||||
// 3. Сохраняем обновленный холст
|
||||
await prisma.canvas.update({
|
||||
where: { id: canvasId },
|
||||
data: { nodes }
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
message: 'Node executed and canvas updated'
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Node execution error:', error)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: error.message || 'Execution failed'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { prisma } from '~/server/utils/prisma'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
const body = await readBody(event)
|
||||
const { taskId, canvasId, newState } = body
|
||||
|
||||
if (!taskId || !canvasId || !newState) {
|
||||
throw createError({ statusCode: 400, message: 'taskId, canvasId and newState are required' })
|
||||
}
|
||||
|
||||
// 1. Найти холст
|
||||
const canvas = await prisma.canvas.findUnique({
|
||||
where: { id: canvasId }
|
||||
})
|
||||
|
||||
if (!canvas) {
|
||||
throw createError({ statusCode: 404, message: 'Canvas not found' })
|
||||
}
|
||||
|
||||
// 2. Найти и обновить узел в JSON
|
||||
const nodes = (canvas.nodes as any[]) || []
|
||||
const nodeIdx = nodes.findIndex(n => n.id === taskId)
|
||||
|
||||
if (nodeIdx === -1) {
|
||||
throw createError({ statusCode: 404, message: 'Task node not found in canvas' })
|
||||
}
|
||||
|
||||
// Обновляем состояние
|
||||
nodes[nodeIdx].state = newState
|
||||
|
||||
// Также обновляем дату изменения, если нужно
|
||||
nodes[nodeIdx].updatedAt = new Date().toISOString()
|
||||
|
||||
// 3. Сохранить изменения в БД
|
||||
await prisma.canvas.update({
|
||||
where: { id: canvasId },
|
||||
data: { nodes }
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Task ${taskId} moved to ${newState}`
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,31 @@
|
||||
export const generateAIResponse = async (prompt: string, systemPrompt = 'You are a helpful assistant.') => {
|
||||
const config = useRuntimeConfig()
|
||||
const apiKey = config.deepseekApiKey || process.env.DEEPSEEK_API_KEY
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('DEEPSEEK_API_KEY is not configured')
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await $fetch('https://api.deepseek.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: {
|
||||
model: 'deepseek-chat',
|
||||
messages: [
|
||||
{ role: 'system', content: systemPrompt },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
stream: false
|
||||
}
|
||||
}) as any
|
||||
|
||||
return response.choices[0].message.content
|
||||
} catch (error: any) {
|
||||
console.error('DeepSeek AI error:', error.data || error.message)
|
||||
throw new Error(`AI generation failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
const { Pool } = require('pg');
|
||||
async function query() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT nodes FROM "Canvas" WHERE id = \'test-crm\'');
|
||||
console.log(JSON.stringify(res.rows[0].nodes, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
query();
|
||||
@@ -0,0 +1,15 @@
|
||||
async function checkToken() {
|
||||
const url = 'http://git.a-crm.controlsystem.kz';
|
||||
const token = '1b3d0d3b5391c67ead8f8dc7380fb7f07434fc64';
|
||||
|
||||
const res = await fetch(`${url}/api/v1/user`, {
|
||||
headers: { 'Authorization': `token ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const user = await res.json();
|
||||
console.log('Token belongs to:', user.username);
|
||||
} else {
|
||||
console.error('Failed to check token');
|
||||
}
|
||||
}
|
||||
checkToken();
|
||||
@@ -0,0 +1,13 @@
|
||||
const { Pool } = require('pg');
|
||||
async function test() {
|
||||
try {
|
||||
const pool = new Pool({ connectionString: `postgresql://postgres:postgres@localhost:5432/postgres` });
|
||||
await pool.query('CREATE DATABASE "xflow"');
|
||||
console.log('✅ SUCCESS created xflow');
|
||||
process.exit(0);
|
||||
} catch(e) {
|
||||
console.log('❌ failed:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,72 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function createTestCanvas() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: 'task-1',
|
||||
type: 'task',
|
||||
x: 100, y: 100, width: 300, height: 200,
|
||||
text: '# Создать API пользователей\nРазработать эндпоинт для списка пользователей',
|
||||
state: 'todo',
|
||||
assigned_to: 'almalexyz@gmail.com',
|
||||
priority: 'high',
|
||||
alarm: new Date(Date.now() + 86400000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
type: 'task',
|
||||
x: 450, y: 100, width: 300, height: 200,
|
||||
text: '# Канбан-доска\nСделать вывод всех задач',
|
||||
state: 'in_progress',
|
||||
assigned_to: 'almalexyz',
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 'task-3',
|
||||
type: 'task',
|
||||
x: 100, y: 350, width: 300, height: 200,
|
||||
text: '# Проверка прав\nНастроить доступ к картам',
|
||||
state: 'review',
|
||||
assigned_to: 'almalexyz',
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 'task-4',
|
||||
type: 'task',
|
||||
x: 450, y: 350, width: 300, height: 200,
|
||||
text: '# Тестирование\nПроверить как работают бейджики',
|
||||
state: 'todo',
|
||||
assigned_to: 'system',
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
const canvasData = {
|
||||
id: 'test-crm',
|
||||
name: 'Тестовый CRM проект',
|
||||
ownerId: 'system',
|
||||
isPublic: true,
|
||||
nodes: JSON.stringify(nodes),
|
||||
edges: JSON.stringify([])
|
||||
};
|
||||
|
||||
try {
|
||||
// Upsert canvas
|
||||
await pool.query(`
|
||||
INSERT INTO "Canvas" (id, name, "ownerId", "isPublic", nodes, edges, "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET nodes = EXCLUDED.nodes, edges = EXCLUDED.edges, "updatedAt" = NOW()
|
||||
`, [canvasData.id, canvasData.name, canvasData.ownerId, canvasData.isPublic, canvasData.nodes, canvasData.edges]);
|
||||
|
||||
console.log('Test canvas fixed successfully!');
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
} finally {
|
||||
pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
createTestCanvas();
|
||||
@@ -0,0 +1,19 @@
|
||||
const { Pool } = require('pg');
|
||||
async function createUser() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
try {
|
||||
await pool.query(`
|
||||
INSERT INTO "User" (id, username, email, password, role, "updatedAt")
|
||||
VALUES ('almalexyz', 'almalexyz', 'almalexyz@gmail.com', 'none', 'admin', NOW())
|
||||
ON CONFLICT DO NOTHING
|
||||
`);
|
||||
await pool.query(`
|
||||
INSERT INTO "Profile" (id, "userId", "displayName", "updatedAt")
|
||||
VALUES ('almalexyz-profile', 'almalexyz', 'Alexander', NOW())
|
||||
ON CONFLICT DO NOTHING
|
||||
`);
|
||||
console.log('User created!');
|
||||
} catch(e) { console.error(e) }
|
||||
process.exit(0);
|
||||
}
|
||||
createUser();
|
||||
@@ -0,0 +1,56 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const oldId = 'almalexyz';
|
||||
const newId = 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2';
|
||||
|
||||
console.log(`Fixing user ID from ${oldId} to ${newId}...`);
|
||||
|
||||
try {
|
||||
// 1. Check if user exists
|
||||
const userRes = await pool.query('SELECT * FROM "User" WHERE id = $1', [oldId]);
|
||||
if (userRes.rowCount === 0) {
|
||||
console.log('User almalexyz not found. Checking if Gqbp... already exists.');
|
||||
const checkRes = await pool.query('SELECT * FROM "User" WHERE id = $1', [newId]);
|
||||
if (checkRes.rowCount > 0) {
|
||||
console.log('User Gqbp... already exists. Just updating canvases.');
|
||||
await pool.query('UPDATE "Canvas" SET "ownerId" = $1', [newId]);
|
||||
await pool.query('UPDATE "Group" SET "ownerId" = $1', [newId]);
|
||||
console.log('Done.');
|
||||
return;
|
||||
}
|
||||
console.log('User not found at all.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. We need to handle constraints. The safest way in one transaction.
|
||||
await pool.query('BEGIN');
|
||||
|
||||
// Disable constraints (Postgres specific)
|
||||
await pool.query('SET CONSTRAINTS ALL DEFERRED');
|
||||
|
||||
// Update User ID
|
||||
await pool.query('UPDATE "User" SET id = $1 WHERE id = $2', [newId, oldId]);
|
||||
|
||||
// Update relations manually since schema didn't have ON UPDATE CASCADE
|
||||
await pool.query('UPDATE "Profile" SET "userId" = $1 WHERE "userId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Canvas" SET "ownerId" = $1 WHERE "ownerId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "GroupMember" SET "userId" = $1 WHERE "userId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Activity" SET "userId" = $1 WHERE "userId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Follow" SET "followerId" = $1 WHERE "followerId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Follow" SET "followingId" = $1 WHERE "followingId" = $2', [newId, oldId]);
|
||||
|
||||
await pool.query('COMMIT');
|
||||
console.log('Successfully migrated user ID and all relations.');
|
||||
|
||||
} catch (err) {
|
||||
await pool.query('ROLLBACK');
|
||||
console.error('Migration failed:', err);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,10 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT id, username, email FROM "User"');
|
||||
console.log(JSON.stringify(res.rows, null, 2));
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,95 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const canvasId = 'xflow-project';
|
||||
const res = await pool.query('SELECT * FROM "Canvas" WHERE id = $1', [canvasId]);
|
||||
|
||||
if (res.rowCount === 0) {
|
||||
console.error('Canvas xflow-project not found in DB');
|
||||
await pool.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = res.rows[0];
|
||||
let nodes = typeof canvas.nodes === 'string' ? JSON.parse(canvas.nodes) : canvas.nodes;
|
||||
let edges = typeof canvas.edges === 'string' ? JSON.parse(canvas.edges) : canvas.edges;
|
||||
|
||||
// Find a good spot for new nodes. Usually far to the right or bottom.
|
||||
// Let's put them near the "Future Development" node if it exists.
|
||||
const futureNode = nodes.find(n => n.id === 'future-1') || { x: 500, y: 500 };
|
||||
|
||||
const startX = futureNode.x + 400;
|
||||
const startY = futureNode.y;
|
||||
|
||||
const newNodes = [
|
||||
{
|
||||
id: 'session-28-04',
|
||||
type: 'text',
|
||||
x: startX,
|
||||
y: startY,
|
||||
width: 400,
|
||||
height: 200,
|
||||
text: '# 🎉 Итоги сессии 28.04\n\nСегодня мы значительно продвинули XFlow вперед, решив критические проблемы синхронизации и внедрив новые функции управления.',
|
||||
style: { background: '#064e3b', border: '#10b981' }
|
||||
},
|
||||
{
|
||||
id: 'task-sync-fix',
|
||||
type: 'task',
|
||||
x: startX + 450,
|
||||
y: startY - 100,
|
||||
width: 300,
|
||||
height: 150,
|
||||
text: '### Синхронизация Метаданных\n\nИсправлена потеря данных при сохранении. Поля `assigned_to`, `priority`, `alarm` теперь сохраняются надежно.',
|
||||
state: 'completed',
|
||||
assigned_to: 'almalexyz',
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 'task-kanban-fix',
|
||||
type: 'task',
|
||||
x: startX + 450,
|
||||
y: startY + 100,
|
||||
width: 300,
|
||||
height: 150,
|
||||
text: '### Канбан-доска\n\nЦентрализованный вид всех задач со всех карт. Исправлена фильтрация по пользователю.',
|
||||
state: 'completed',
|
||||
assigned_to: 'almalexyz',
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 'task-auth-fix',
|
||||
type: 'task',
|
||||
x: startX + 450,
|
||||
y: startY + 300,
|
||||
width: 300,
|
||||
height: 150,
|
||||
text: '### Миграция ID и Прав\n\nИсправлены 403/404 ошибки. Синхронизированы Firebase ID и Prisma ID.',
|
||||
state: 'completed',
|
||||
assigned_to: 'almalexyz',
|
||||
priority: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
const newEdges = [
|
||||
{ id: 'e-session-1', source: 'future-1', target: 'session-28-04' },
|
||||
{ id: 'e-session-2', source: 'session-28-04', target: 'task-sync-fix' },
|
||||
{ id: 'e-session-3', source: 'session-28-04', target: 'task-kanban-fix' },
|
||||
{ id: 'e-session-4', source: 'session-28-04', target: 'task-auth-fix' }
|
||||
];
|
||||
|
||||
// Merge
|
||||
nodes = [...nodes, ...newNodes];
|
||||
edges = [...edges, ...newEdges];
|
||||
|
||||
await pool.query(
|
||||
'UPDATE "Canvas" SET nodes = $1, edges = $2, "updatedAt" = NOW() WHERE id = $3',
|
||||
[JSON.stringify(nodes), JSON.stringify(edges), canvasId]
|
||||
);
|
||||
|
||||
console.log('Successfully merged session results into xflow-project');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
Generated
+19
@@ -23,6 +23,7 @@
|
||||
"pg": "^8.12.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -15601,6 +15602,12 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sortablejs": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
|
||||
"integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||
@@ -18335,6 +18342,18 @@
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuedraggable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
|
||||
"integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sortablejs": "1.14.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"pg": "^8.12.0",
|
||||
"vue": "^3.5.0",
|
||||
"vue-router": "^4.5.0",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const { Pool } = require('pg');
|
||||
const fs = require('fs');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const nodes = [
|
||||
{ id: 'root', type: 'text', x: 0, y: 0, width: 400, height: 100, text: '# План Развития XFlow\nВизуальная CRM/ERP Система', style: { background: '#1e293b', border: '#3b82f6' } },
|
||||
|
||||
// Stage 1
|
||||
{ id: 'stage1', type: 'text', x: -500, y: 200, width: 350, height: 150, text: '## Этап 1: Фундамент\nРоли (RBAC), Управление Командой, Master Canvas', style: { background: '#1e293b', border: '#3b82f6' } },
|
||||
{ id: 's1-1', type: 'task', x: -900, y: 400, width: 300, height: 120, text: 'Система Ролей (RBAC)', state: 'todo', assigned_to: 'admin', priority: 'high' },
|
||||
{ id: 's1-2', type: 'task', x: -500, y: 450, width: 300, height: 120, text: 'Интерфейс приглашения юзеров', state: 'todo', assigned_to: 'admin', priority: 'medium' },
|
||||
|
||||
// Stage 2
|
||||
{ id: 'stage2', type: 'text', x: 0, y: 200, width: 350, height: 150, text: '## Этап 2: Умные Узлы\nТипизация, Панель свойств, Цветовая индикация', style: { background: '#1e293b', border: '#10b981' } },
|
||||
{ id: 's2-1', type: 'task', x: -150, y: 400, width: 300, height: 120, text: 'Герметичное сохранение метаданных', state: 'completed', assigned_to: 'antigravity', priority: 'high' },
|
||||
{ id: 's2-2', type: 'task', x: 150, y: 450, width: 300, height: 120, text: 'Типизация (Webhook, AI, Git)', state: 'completed', assigned_to: 'antigravity', priority: 'high' },
|
||||
|
||||
// Stage 3
|
||||
{ id: 'stage3', type: 'text', x: 500, y: 200, width: 350, height: 150, text: '## Этап 3: Аналитика\nКанбан-борд, Дашборд, Уведомления', style: { background: '#1e293b', border: '#8b5cf6' } },
|
||||
{ id: 's3-1', type: 'task', x: 500, y: 400, width: 300, height: 120, text: 'Персональный Канбан-дашборд', state: 'completed', assigned_to: 'antigravity', priority: 'high' },
|
||||
{ id: 's3-2', type: 'task', x: 900, y: 450, width: 300, height: 120, text: 'Drag-n-Drop в Канбане', state: 'todo', assigned_to: 'admin', priority: 'medium' },
|
||||
|
||||
// Stage 4
|
||||
{ id: 'stage4', type: 'text', x: 0, y: 650, width: 400, height: 150, text: '## Этап 4: Интеграции\nGit/Gitea Deep Sync, S3 Storage, Workflows', style: { background: '#1e293b', border: '#f59e0b' } },
|
||||
|
||||
// Update
|
||||
{ id: 'update', type: 'text', x: -500, y: -200, width: 400, height: 200, text: '### Обновление 28.04\n✅ Сохранение метаданных\n✅ База под Канбан\n✅ Типизация узлов', style: { background: '#064e3b', border: '#10b981' } }
|
||||
];
|
||||
|
||||
const edges = [
|
||||
{ id: 'e1', source: 'root', target: 'stage1' },
|
||||
{ id: 'e2', source: 'root', target: 'stage2' },
|
||||
{ id: 'e3', source: 'root', target: 'stage3' },
|
||||
{ id: 'e4', source: 'root', target: 'stage4' },
|
||||
{ id: 'e5', source: 'root', target: 'update' },
|
||||
{ id: 'e1-1', source: 'stage1', target: 's1-1' },
|
||||
{ id: 'e1-2', source: 'stage1', target: 's1-2' },
|
||||
{ id: 'e2-1', source: 'stage2', target: 's2-1' },
|
||||
{ id: 'e2-2', source: 'stage2', target: 's2-2' },
|
||||
{ id: 'e3-1', source: 'stage3', target: 's3-1' },
|
||||
{ id: 'e3-2', source: 'stage3', target: 's3-2' }
|
||||
];
|
||||
|
||||
const canvasId = 'master-plan';
|
||||
const name = '🗺️ Генеральный План Развития';
|
||||
|
||||
// Upsert the canvas
|
||||
await pool.query(
|
||||
`INSERT INTO "Canvas" (id, name, nodes, edges, "ownerId", "isPublic", "createdAt", "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, 'system', true, NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET nodes = $3, edges = $4, name = $2, "updatedAt" = NOW()`,
|
||||
[canvasId, name, JSON.stringify(nodes), JSON.stringify(edges)]
|
||||
);
|
||||
|
||||
console.log('Master plan published to database as canvas: master-plan');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,53 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const canvasId = 'xflow-project';
|
||||
|
||||
// Define a fresh set of nodes that thoroughly represent the project
|
||||
const nodes = [
|
||||
// 1. Core Architecture
|
||||
{ id: 'arch-1', type: 'text', x: 0, y: 0, width: 400, height: 350, text: '# 🏗️ Глобальная Архитектура\n\n- **Nuxt 3** - Full-stack фреймворк (SSR + API)\n- **Prisma ORM** - Типизированный доступ к данным\n- **PostgreSQL** - Надежное хранилище\n- **Firebase Auth** - Вход через Google\n- **Vue Flow** - Ядро интерактивного холста\n- **TailwindCSS** - Современная стилизация', style: { background: '#1e293b', border: '#3b82f6' } },
|
||||
|
||||
// 2. Database & Prisma
|
||||
{ id: 'prisma-schema', type: 'text', x: -500, y: 100, width: 450, height: 500, text: '# 🗄️ База Данных & Prisma\n\n```prisma\nmodel User {\n id String @id\n email String @unique\n username String @unique\n role String @default(\"user\")\n canvases Canvas[]\n groups GroupMember[]\n}\n\nmodel Canvas {\n id String @id\n name String\n nodes Json // Obsidian format\n edges Json\n owner User @relation(...)\n}\n\nmodel Group {\n id String @id\n members GroupMember[]\n}\n```\n\n**Особенности:**\n- Использование `Json` полей для гибкости холста\n- Каскадное удаление данных\n- Связи многие-ко-многим для групп', style: { background: '#1e293b', border: '#10b981' } },
|
||||
|
||||
// 3. Auth & Security
|
||||
{ id: 'auth-system', type: 'text', x: -500, y: 650, width: 450, height: 350, text: '# 🔐 Безопасность и Auth\n\n1. **Firebase**: Клиентская аутентификация Google.\n2. **Google Token**: Отправляется на `/api/auth/google`.\n3. **Prisma Upsert**: Пользователь создается или обновляется в БД.\n4. **JWT**: Сервер генерирует токен (с ID и ролью).\n5. **Middleware**: Проверка JWT через `requireAuth` в серверных роутах.\n6. **RBAC**: Роли `admin` и `user` контролируют доступ.', style: { background: '#1e293b', border: '#ef4444' } },
|
||||
|
||||
// 4. Git Synchronization
|
||||
{ id: 'git-sync', type: 'text', x: 500, y: 100, width: 450, height: 400, text: '# 🔄 Git Синхронизация (Gitea)\n\n- **useGitSync**: Composable для управления статусом.\n- **Real-Sync API**: Обрабатывает `init` и `sync`.\n- **Gitea Integration**:\n - Автоматическое создание репозиториев.\n - Коммиты при каждом сохранении карты.\n - Хранение `.canvas` файлов в Git для версионности.\n - Токены доступа через `.env` переменные.', style: { background: '#1e293b', border: '#f59e0b' } },
|
||||
|
||||
// 5. Frontend Reactivity
|
||||
{ id: 'state-reactivity', type: 'text', x: 500, y: 550, width: 450, height: 350, text: '# 🧪 Реактивность и Состояние\n\n- **elements**: Массив всех узлов и ребер Vue Flow.\n- **selectedNode**: Реактивный объект для панели свойств.\n- **useUnsavedChanges**: Отслеживание изменений (Dirty state).\n- **commitNodeChange**: Синхронизация данных из панели свойств обратно в массив элементов перед сохранением.', style: { background: '#1e293b', border: '#8b5cf6' } },
|
||||
|
||||
// 6. Features & Updates
|
||||
{ id: 'feat-1', type: 'text', x: 0, y: 400, width: 400, height: 450, text: '# ✨ Ключевые Фичи\n\n- **Кросс-ссылки**: Навигация между картами.\n- **Канбан**: Сводная таблица задач по пользователям.\n- **Типы Узлов**:\n - `Text` (Markdown)\n - `Task` (State machine)\n - `AI Prompt` (LLM ready)\n - `Webhook` (External trigger)\n - `Git Sync` (Version control)\n- **Импорт/Экспорт**: Полная совместимость с Obsidian.', style: { background: '#1e293b', border: '#06b6d4' } },
|
||||
|
||||
// 7. Recent Session
|
||||
{ id: 'session-apr-28', type: 'text', x: 0, y: 900, width: 400, height: 250, text: '# 🔥 Сессия 28 Апреля (Итоги)\n\n- ✅ Починена потеря метаданных в Prisma.\n- ✅ Реализована навигация (двойной клик по ссылкам).\n- ✅ Внедрена система ролей и прав.\n- ✅ Добавлен выпадающий список для связей карт.\n- ✅ Синхронизированы Firebase и БД ID.', style: { background: '#064e3b', border: '#10b981' } }
|
||||
];
|
||||
|
||||
const edges = [
|
||||
{ id: 'e1', source: 'arch-1', target: 'prisma-schema' },
|
||||
{ id: 'e2', source: 'arch-1', target: 'auth-system' },
|
||||
{ id: 'e3', source: 'arch-1', target: 'git-sync' },
|
||||
{ id: 'e4', source: 'arch-1', target: 'state-reactivity' },
|
||||
{ id: 'e5', source: 'arch-1', target: 'feat-1' },
|
||||
{ id: 'e6', source: 'feat-1', target: 'session-apr-28' },
|
||||
{ id: 'e7', source: 'prisma-schema', target: 'auth-system' }
|
||||
];
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO "Canvas" (id, name, nodes, edges, "ownerId", "isPublic", "createdAt", "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2', true, NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET nodes = $3, edges = $4, name = $2, "updatedAt" = NOW()`,
|
||||
[canvasId, '🗺️ Глобальная Архитектура XFlow', JSON.stringify(nodes), JSON.stringify(edges)]
|
||||
);
|
||||
|
||||
console.log('Main project canvas rebuilt with deep architectural details (Prisma, Git, Auth, State)');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,47 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const canvasId = 'xflow-project';
|
||||
|
||||
const originalNodes = [
|
||||
{"id":"arch-1","type":"text","text":"# 🏗️ Основная Архитектура\n\n- **Nuxt 3** - Vue 3 фреймворк\n- **TailwindCSS** - Стилизация\n- **Vue Flow** - Движок канваса\n- **Firebase** - Аутентификация\n- **In-memory DB** - Хранение данных\n- **PWA** - Progressive Web App","x":100,"y":-240,"width":320,"height":290,"color":"1"},
|
||||
{"id":"fe-1","type":"text","text":"# 🎨 Фронтенд\n\n- **FlowEditor.vue** - Основной редактор\n- **FlowNodeRenderer.vue** - Отображение узлов\n- **CanvasViewer.vue** - Устаревший просмотр\n- **Админ Панель** - Управление интерфейсом\n- **Система Аутентификации** - Вход через Google","x":540,"y":-265,"width":280,"height":340,"color":"2"},
|
||||
{"id":"be-1","type":"text","text":"# ⚙️ Backend API\n\n- `/api/canvas/*` - CRUD операций с канвасами\n- `/api/nodes/*` - Управление узлами\n- `/api/auth/*` - Аутентификация\n- `/api/admin/*` - Административные операции\n- `/api/push/*` - Уведомления","x":540,"y":100,"width":280,"height":320,"color":"3"},
|
||||
{"id":"feat-1","type":"text","text":"# ✨ Ключевые Функции\n\n- **Редактор в стиле Node-RED**\n - Палитра сбоку\n - Панель свойств\n - Drag & drop\n - Соединения проводами\n\n- **Совместимость с Obsidian**\n - Экспорт в .canvas формат\n - Импорт существующих карт\n - Поддержка Markdown\n\n- **Социальные Функции**\n - Группы пользователей\n - Общие канвасы\n - Лента активности","x":960,"y":-35,"width":300,"height":455,"color":"5"},
|
||||
{"id":"db-1","type":"text","text":"## Схема Базы Данных\n\n- `users` - Пользовательские аккаунты\n- `canvases` - Ментальные карты\n- `nodes` - Отдельные узлы\n- `groups` - Группы пользователей\n- `profiles` - Профили пользователей\n- `activities` - Лента активности","x":960,"y":480,"width":300,"height":260,"color":"2"},
|
||||
{"id":"deploy-1","type":"text","text":"# 🚀 Развертывание\n\n- **Docker Compose**\n- **Nginx** - Обратный прокси\n- **Gitea** - Git сервер\n- **PostgreSQL** - База данных\n- **Certbot** - SSL сертификаты","x":960,"y":790,"width":300,"height":250,"color":"3"},
|
||||
{"id":"nodes-1","type":"text","text":"# 📦 Типы Узлов\n\n- **Текстовая Заметка** - Markdown контент\n- **Ссылка на Файл** - Ссылки на файлы\n- **Узел Задачи** - Отслеживание состояния\n- **AI/LLM Узел** - AI обработка\n- **Webhook Узел** - HTTP триггеры\n- **Git Sync Узел** - Контроль версий","x":100,"y":160,"width":320,"height":320,"color":"4"},
|
||||
{"id":"social-1","type":"text","text":"# 👥 Социальная Сеть\n\n- **Профили Пользователей**\n - Аватар и био\n - Статистика канвасов\n - Подписчики/подписки\n\n- **Система Групп**\n - Публичные/приватные группы\n - Ролевые разрешения\n - Общие канвасы","x":100,"y":1000,"width":320,"height":320,"color":"4"},
|
||||
{"id":"tech-1","type":"text","text":"# 🛠️ Технологический Стек\n\n```json\n{\n \"frontend\": \"Vue 3 + Nuxt 3\",\n \"styling\": \"TailwindCSS\",\n \"canvas\": \"Vue Flow\",\n \"auth\": \"Firebase Google\",\n \"storage\": \"In-memory\",\n \"pwa\": \"@vite-pwa/nuxt\"\n}\n```","x":100,"y":560,"width":320,"height":355,"color":"6"},
|
||||
{"id":"future-1","type":"text","text":"# 🚀 Будущее Развитие\n\n- **Git Интеграция**\n - Commit/push/pull карт\n - История версий\n - Управление ветками\n\n- **AI Интеграция**\n - OpenClaw агенты\n - Автоматизация LLM узлов\n - Умные предложения\n\n- **Социальная Сеть**\n - Профили пользователей\n - Система подписок\n - Ленты активности","x":540,"y":450,"width":280,"height":465,"color":"1"},
|
||||
{"id":"files-1","type":"text","text":"## Структура Проекта\n\n```\napp/\n├── components/\n│ ├── FlowEditor.vue\n│ └── FlowNodeRenderer.vue\n├── pages/\n│ ├── canvas.vue\n│ └── admin.vue\n└── server/api/\n```","x":540,"y":970,"width":280,"height":350,"color":"5"}
|
||||
];
|
||||
|
||||
const originalEdges = [
|
||||
{"id":"e1","fromNode":"arch-1","fromSide":"right","toNode":"fe-1","toSide":"left","color":"1"},
|
||||
{"id":"e2","fromNode":"arch-1","fromSide":"right","toNode":"be-1","toSide":"left","color":"1"},
|
||||
{"id":"e3","fromNode":"arch-1","fromSide":"bottom","toNode":"nodes-1","toSide":"top","color":"1"},
|
||||
{"id":"e4","fromNode":"fe-1","fromSide":"right","toNode":"feat-1","toSide":"left","color":"2"},
|
||||
{"id":"e5","fromNode":"be-1","fromSide":"right","toNode":"feat-1","toSide":"left","color":"3"},
|
||||
{"id":"e6","fromNode":"nodes-1","fromSide":"bottom","toNode":"tech-1","toSide":"top","color":"4"},
|
||||
{"id":"e7","fromNode":"be-1","fromSide":"bottom","toNode":"future-1","toSide":"top","color":"3"},
|
||||
{"id":"e8","fromNode":"feat-1","fromSide":"bottom","toNode":"db-1","toSide":"top","color":"5"},
|
||||
{"id":"e9","fromNode":"db-1","fromSide":"bottom","toNode":"deploy-1","toSide":"top","color":"2"},
|
||||
{"id":"e10","fromNode":"tech-1","fromSide":"right","toNode":"future-1","toSide":"left","color":"6"},
|
||||
{"id":"e11","fromNode":"tech-1","fromSide":"bottom","toNode":"social-1","toSide":"top","color":"6"},
|
||||
{"id":"e12","fromNode":"future-1","fromSide":"bottom","toNode":"files-1","toSide":"top","color":"1"},
|
||||
{"id":"e13","fromNode":"social-1","fromSide":"right","toNode":"files-1","toSide":"left","color":"4"}
|
||||
];
|
||||
|
||||
await pool.query(
|
||||
'UPDATE "Canvas" SET nodes = $1, edges = $2 WHERE id = $3',
|
||||
[JSON.stringify(originalNodes), JSON.stringify(originalEdges), canvasId]
|
||||
);
|
||||
|
||||
console.log('Successfully reverted xflow-project to original state');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,10 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('UPDATE "User" SET role = \'admin\'');
|
||||
console.log('Updated users to admin:', res.rowCount);
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,20 @@
|
||||
const { Pool } = require('pg');
|
||||
async function test() {
|
||||
const creds = [
|
||||
['admin', 'admin'],
|
||||
['postgres', 'postgres'],
|
||||
['gitea', 'gitea_password'],
|
||||
['gitea', 'gitea']
|
||||
];
|
||||
for (const [u, p] of creds) {
|
||||
try {
|
||||
const pool = new Pool({ connectionString: `postgresql://${u}:${p}@localhost:5432/postgres` });
|
||||
await pool.query('SELECT 1');
|
||||
console.log('✅ SUCCESS WITH:', u, p);
|
||||
process.exit(0);
|
||||
} catch(e) {
|
||||
console.log('❌ failed:', u, p, e.message.substring(0, 40));
|
||||
}
|
||||
}
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,8 @@
|
||||
const { Pool } = require('pg');
|
||||
async function test() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT * FROM "User"');
|
||||
console.log(res.rows);
|
||||
process.exit(0);
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,8 @@
|
||||
const { Pool } = require('pg');
|
||||
async function test() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT id, "ownerId" FROM "Canvas"');
|
||||
console.log(res.rows);
|
||||
process.exit(0);
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,8 @@
|
||||
const { Pool } = require('pg');
|
||||
async function test() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT id, "ownerId", "isPublic" FROM "Canvas" WHERE id = \'xflow-project\'');
|
||||
console.log(res.rows);
|
||||
process.exit(0);
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,8 @@
|
||||
const { Pool } = require('pg');
|
||||
async function test() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT * FROM "User"');
|
||||
console.log(res.rows);
|
||||
process.exit(0);
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,28 @@
|
||||
const { Buffer } = require('buffer');
|
||||
|
||||
async function testGitea() {
|
||||
const url = 'http://git.a-crm.controlsystem.kz';
|
||||
const token = '1b3d0d3b5391c67ead8f8dc7380fb7f07434fc64';
|
||||
const owner = 'almalexyz'; // Adjust if needed
|
||||
const repo = 'MM';
|
||||
|
||||
console.log(`Testing Gitea API at ${url}...`);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${url}/api/v1/repos/AVTEX/MMAVTEX`, {
|
||||
headers: { 'Authorization': `token ${token}` }
|
||||
});
|
||||
console.log('Repo check status:', res.status);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
console.log('Repo Name:', data.name);
|
||||
} else {
|
||||
const err = await res.text();
|
||||
console.error('Repo check failed:', err);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fetch error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
testGitea();
|
||||
@@ -0,0 +1,18 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT id, username FROM "User"');
|
||||
console.log(JSON.stringify(res.rows, null, 2));
|
||||
|
||||
// Also change owner of all canvases to the first non-system user
|
||||
const realUser = res.rows.find(u => u.username !== 'system');
|
||||
if (realUser) {
|
||||
await pool.query('UPDATE "Canvas" SET "ownerId" = $1', [realUser.id]);
|
||||
console.log('Ownership of all canvases transferred to:', realUser.username);
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,66 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const canvasId = 'xflow-project';
|
||||
const res = await pool.query('SELECT * FROM "Canvas" WHERE id = $1', [canvasId]);
|
||||
|
||||
if (res.rowCount === 0) {
|
||||
console.error('Canvas xflow-project not found');
|
||||
await pool.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = res.rows[0];
|
||||
let nodes = typeof canvas.nodes === 'string' ? JSON.parse(canvas.nodes) : canvas.nodes;
|
||||
let edges = typeof canvas.edges === 'string' ? JSON.parse(canvas.edges) : canvas.edges;
|
||||
|
||||
// 1. Update Tech Stack (tech-1)
|
||||
const techNode = nodes.find(n => n.id === 'tech-1');
|
||||
if (techNode) {
|
||||
techNode.text = '# 🛠️ Технологический Стек\n\n```json\n{\n \"frontend\": \"Vue 3 + Nuxt 3\",\n \"styling\": \"TailwindCSS\",\n \"canvas\": \"Vue Flow\",\n \"auth\": \"Firebase Google\",\n \"database\": \"PostgreSQL + Prisma\",\n \"pwa\": \"@vite-pwa/nuxt\",\n \"git_sync\": \"Gitea API\"\n}\n```';
|
||||
techNode.height = 400;
|
||||
}
|
||||
|
||||
// 2. Update Backend API (be-1)
|
||||
const beNode = nodes.find(n => n.id === 'be-1');
|
||||
if (beNode) {
|
||||
beNode.text = '# ⚙️ Backend API\n\n- `/api/canvas/*` - CRUD холстов\n- `/api/tasks` - **Канбан-агрегатор**\n- `/api/git/real-sync` - **Git Синхронизация**\n- `/api/auth/google` - Firebase Auth\n- `/api/users/*` - Профили и роли\n- `/api/groups/*` - Совместная работа';
|
||||
beNode.height = 350;
|
||||
}
|
||||
|
||||
// 3. Update Key Features (feat-1)
|
||||
const featNode = nodes.find(n => n.id === 'feat-1');
|
||||
if (featNode) {
|
||||
featNode.text = '# ✨ Ключевые Функции\n\n- **Умный Редактор**\n - Drag & Drop палитра\n - Панель свойств узлов\n - **Кросс-ссылки между картами**\n\n- **Канбан-борд**\n - Персональные задачи\n - Синхронизация статусов\n\n- **Obsidian Sync**\n - Нативная поддержка `.canvas`\n - Сохранение метаданных\n\n- **Git Интеграция**\n - Авто-сохранение в Gitea';
|
||||
featNode.height = 500;
|
||||
}
|
||||
|
||||
// 4. Add "Latest Updates" Node (session-summary)
|
||||
const updateNodeId = 'updates-apr-28';
|
||||
if (!nodes.find(n => n.id === updateNodeId)) {
|
||||
nodes.push({
|
||||
id: updateNodeId,
|
||||
type: 'text',
|
||||
x: 1400,
|
||||
y: -200,
|
||||
width: 350,
|
||||
height: 300,
|
||||
text: '# 🔥 Обновления (28 Апреля)\n\n- ✅ **Data Persistence**: Исправлена потеря метаданных задач.\n- ✅ **Cross-Linking**: Навигация между картами через узлы-файлы.\n- ✅ **Personal Kanban**: Рабочая доска задач с фильтрацией.\n- ✅ **Admin Roles**: Система прав для удаления и редактирования.\n- ✅ **UX Fixes**: Переименование карт и выпадающие списки ссылок.',
|
||||
style: { background: '#064e3b', border: '#10b981' }
|
||||
});
|
||||
edges.push({ id: 'e-update', source: 'feat-1', target: updateNodeId });
|
||||
}
|
||||
|
||||
// Update in DB
|
||||
await pool.query(
|
||||
'UPDATE "Canvas" SET nodes = $1, edges = $2, "updatedAt" = NOW() WHERE id = $3',
|
||||
[JSON.stringify(nodes), JSON.stringify(edges), canvasId]
|
||||
);
|
||||
|
||||
console.log('Main project canvas updated with latest architecture and features');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
Reference in New Issue
Block a user