feat: AI context, Webhooks, Kanban drag-drop, and UI fixes

This commit is contained in:
2026-04-29 17:43:18 +05:00
parent 89b3340253
commit bc2f99408d
29 changed files with 991 additions and 52 deletions
+59 -12
View File
@@ -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,24 +445,20 @@ 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)
if (node.type === 'task') {
const currentState = node.data?.state || 'todo'
// Quick toggle workflow: todo -> in_progress -> review -> completed -> todo
const nextStateMap = {
'todo': 'in_progress',
'in_progress': 'review',
@@ -454,10 +466,45 @@ const onNodeDoubleClick = (event) => {
'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}`)
// 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) => {
const edge = {
+24 -1
View File
@@ -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)
+54 -5
View File
@@ -29,10 +29,17 @@
</span>
</div>
<div class="p-4 flex-1 overflow-y-auto space-y-4 custom-scrollbar">
<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
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)"
>
@@ -56,8 +63,11 @@
</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'
+6 -2
View File
@@ -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)
})
}
})
+96
View File
@@ -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}`
}
})
+31
View File
@@ -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}`)
}
}
+8
View File
@@ -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();
+15
View File
@@ -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();
+13
View File
@@ -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();
+72
View File
@@ -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();
+19
View File
@@ -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();
+56
View File
@@ -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();
+10
View File
@@ -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);
+95
View File
@@ -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);
+19
View File
@@ -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",
+1
View File
@@ -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": {
+61
View File
@@ -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);
+53
View File
@@ -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);
+47
View File
@@ -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);
+10
View File
@@ -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);
+20
View File
@@ -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();
+8
View File
@@ -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();
+8
View File
@@ -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();
+8
View File
@@ -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();
+8
View File
@@ -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();
+28
View File
@@ -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();
+18
View File
@@ -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);
+66
View File
@@ -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);