chore: align infrastructure with server
This commit is contained in:
+18
-14
@@ -26,7 +26,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm install
|
run: npm install
|
||||||
@@ -49,21 +49,25 @@ jobs:
|
|||||||
- name: Deploy to server
|
- name: Deploy to server
|
||||||
run: |
|
run: |
|
||||||
apk add --no-cache docker-cli docker-compose
|
apk add --no-cache docker-cli docker-compose
|
||||||
mkdir -p /opt/xflow/.output /opt/xflow/prisma /opt/xflow/node_modules
|
# Подготовка папок в новой структуре
|
||||||
cp -r .output/. /opt/xflow/.output/
|
mkdir -p /root/projects/xflow/.output /root/projects/xflow/prisma /root/projects/xflow/node_modules
|
||||||
cp -r prisma/. /opt/xflow/prisma/
|
cp -r .output/. /root/projects/xflow/.output/
|
||||||
cp -r node_modules/. /opt/xflow/node_modules/
|
cp -r prisma/. /root/projects/xflow/prisma/
|
||||||
cp package.json /opt/xflow/package.json
|
cp -r node_modules/. /root/projects/xflow/node_modules/
|
||||||
cp prisma.config.ts /opt/xflow/prisma.config.ts
|
cp package.json /root/projects/xflow/package.json
|
||||||
cp server/docker-compose.yml /opt/xflow/docker-compose.yml
|
cp prisma.config.ts /root/projects/xflow/prisma.config.ts
|
||||||
sed -i 's/infra-network/infra/' /opt/xflow/docker-compose.yml
|
|
||||||
docker stop xflow 2>/dev/null || true
|
# Перезапуск конкретного сервиса в рамках общей инфраструктуры
|
||||||
docker rm xflow 2>/dev/null || true
|
# Мы используем мастер-файл из корня проектов
|
||||||
docker compose -f /opt/xflow/docker-compose.yml up -d --no-deps xflow
|
docker stop xflow_crm 2>/dev/null || true
|
||||||
|
docker rm xflow_crm 2>/dev/null || true
|
||||||
|
docker compose -f /root/projects/docker-compose.yml up -d --no-deps xflow_crm
|
||||||
|
|
||||||
echo "⏳ Waiting for container to start..."
|
echo "⏳ Waiting for container to start..."
|
||||||
sleep 5
|
sleep 5
|
||||||
docker exec xflow sh -c "cd /app && export DATABASE_URL=postgresql://xflow:xflow_pass@postgres:5432/xflow && npx prisma migrate deploy --config prisma.config.ts && npx prisma db seed"
|
# Миграции с использованием новой БД xflow_crm_db
|
||||||
echo "✅ XFlow deployed at $(date)"
|
docker exec xflow_crm sh -c "cd /app && export DATABASE_URL=postgresql://admin:${POSTGRES_PASSWORD}@xflow_db:5432/xflow_crm_db && npx prisma migrate deploy --config prisma.config.ts && npx prisma db seed"
|
||||||
|
echo "✅ XFlow CRM deployed at $(date)"
|
||||||
env:
|
env:
|
||||||
FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
|
FIREBASE_API_KEY: ${{ secrets.FIREBASE_API_KEY }}
|
||||||
FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }}
|
FIREBASE_AUTH_DOMAIN: ${{ secrets.FIREBASE_AUTH_DOMAIN }}
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
name: 🚀 Деплой XFlow Server
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, master]
|
|
||||||
paths:
|
|
||||||
- 'server/**'
|
|
||||||
- '.github/workflows/deploy-server.yml'
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deploy:
|
|
||||||
runs-on: self-hosted
|
|
||||||
name: Деплой сервера майндкарт
|
|
||||||
steps:
|
|
||||||
- name: 📥 Скачать код
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: 🔄 Синхронизация файлов
|
|
||||||
run: |
|
|
||||||
SERVER_PATH="/opt/xflow-server"
|
|
||||||
|
|
||||||
mkdir -p $SERVER_PATH/api
|
|
||||||
mkdir -p $SERVER_PATH/nginx/html
|
|
||||||
mkdir -p $SERVER_PATH/nginx/conf.d
|
|
||||||
mkdir -p $SERVER_PATH/scripts
|
|
||||||
|
|
||||||
# Синхронизация docker-compose
|
|
||||||
cp -f server/docker-compose.yml $SERVER_PATH/docker-compose.yml
|
|
||||||
|
|
||||||
# Синхронизация API
|
|
||||||
rsync -av --delete server/api/ $SERVER_PATH/api/
|
|
||||||
|
|
||||||
# Синхронизация nginx
|
|
||||||
cp -f server/nginx/nginx.conf $SERVER_PATH/nginx/nginx.conf
|
|
||||||
cp -f server/nginx/html/* $SERVER_PATH/nginx/html/
|
|
||||||
|
|
||||||
# Синхронизация скриптов
|
|
||||||
cp -f server/scripts/* $SERVER_PATH/scripts/ 2>/dev/null || true
|
|
||||||
|
|
||||||
# Копировать .env если не существует
|
|
||||||
if [ ! -f $SERVER_PATH/.env ]; then
|
|
||||||
cp -f server/.env.example $SERVER_PATH/.env
|
|
||||||
echo "⚠️ Создан .env из примера - ОБНОВИТЕ ДЛЯ PRODUCTION!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Файлы синхронизированы"
|
|
||||||
|
|
||||||
- name: 🐳 Пересборка и перезапуск
|
|
||||||
run: |
|
|
||||||
cd /opt/xflow-server
|
|
||||||
|
|
||||||
# Пересобрать API
|
|
||||||
docker compose build xflow-api
|
|
||||||
|
|
||||||
# Перезапустить все сервисы
|
|
||||||
docker compose down
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
echo "✅ Сервисы перезапущены"
|
|
||||||
|
|
||||||
- name: ✅ Проверка здоровья
|
|
||||||
run: |
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
# Проверка API
|
|
||||||
API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8082/health 2>/dev/null || echo "000")
|
|
||||||
if [ "$API_STATUS" = "200" ]; then
|
|
||||||
echo "✅ API работает"
|
|
||||||
else
|
|
||||||
echo "⚠️ API статус: $API_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Проверка Gitea
|
|
||||||
GITEA_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3001 2>/dev/null || echo "000")
|
|
||||||
if [ "$GITEA_STATUS" = "200" ]; then
|
|
||||||
echo "✅ Gitea работает"
|
|
||||||
else
|
|
||||||
echo "⚠️ Gitea статус: $GITEA_STATUS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Список контейнеров
|
|
||||||
docker compose -f /opt/xflow-server/docker-compose.yml ps
|
|
||||||
|
|
||||||
echo "✅ ДЕПЛОЙ ЗАВЕРШЁН УСПЕШНО!"
|
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-slate-950">
|
<div class="min-h-screen bg-slate-950">
|
||||||
<div class="max-w-7xl mx-auto p-6">
|
<div class="max-w-7xl mx-auto px-4 py-8">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<!-- Debug Info for Prod -->
|
||||||
|
<div v-if="auth.user.value" class="mb-4 p-2 bg-slate-800 border border-slate-700 rounded text-[10px] font-mono text-slate-500 flex justify-between items-center">
|
||||||
|
<span>Logged in as: <b class="text-blue-400">{{ auth.user.value.email }}</b></span>
|
||||||
|
<span>Internal ID: <b class="text-emerald-400">{{ auth.user.value.id }}</b></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-8">
|
||||||
<h1 class="text-2xl font-bold text-slate-100">Мои Карты</h1>
|
<h1 class="text-2xl font-bold text-slate-100">Мои Карты</h1>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button @click="triggerImport" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg font-medium transition">
|
<button @click="triggerImport" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded-lg font-medium transition">
|
||||||
|
|||||||
@@ -31,11 +31,16 @@ export default defineEventHandler(async (event) => {
|
|||||||
orderBy: { updatedAt: 'desc' }
|
orderBy: { updatedAt: 'desc' }
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
const totalCount = await prisma.canvas.count()
|
||||||
canvases: canvasList.map(c => ({
|
|
||||||
|
const canvases = canvasList.map(c => ({
|
||||||
...c,
|
...c,
|
||||||
nodeCount: Array.isArray(c.nodes) ? (c.nodes as any[]).length : 0,
|
nodeCount: Array.isArray(c.nodes) ? (c.nodes as any[]).length : 0,
|
||||||
nodes: undefined
|
nodes: undefined
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
canvases,
|
||||||
|
totalCount
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
const body = await readBody(event)
|
const body = await readBody(event)
|
||||||
const { canvasId, context } = body
|
const { canvasId, context } = body
|
||||||
|
|
||||||
|
console.log(`[Execute] nodeId=${nodeId} canvasId=${canvasId} userId=${user.id}`)
|
||||||
|
|
||||||
if (!nodeId || !canvasId) {
|
if (!nodeId || !canvasId) {
|
||||||
throw createError({ statusCode: 400, message: 'Node ID and Canvas ID are required' })
|
throw createError({ statusCode: 400, message: 'Node ID and Canvas ID are required' })
|
||||||
}
|
}
|
||||||
@@ -17,6 +19,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
where: { id: canvasId }
|
where: { id: canvasId }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log(`[Execute] Canvas found: ${!!canvas}, nodes count: ${(canvas?.nodes as any[])?.length}`)
|
||||||
|
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
throw createError({ statusCode: 404, message: 'Canvas not found' })
|
throw createError({ statusCode: 404, message: 'Canvas not found' })
|
||||||
}
|
}
|
||||||
@@ -30,11 +34,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
const nodes = (canvas.nodes as any[]) || []
|
const nodes = (canvas.nodes as any[]) || []
|
||||||
const nodeIdx = nodes.findIndex(n => n.id === nodeId)
|
const nodeIdx = nodes.findIndex(n => n.id === nodeId)
|
||||||
|
|
||||||
|
console.log(`[Execute] Node found at index: ${nodeIdx}`)
|
||||||
|
|
||||||
if (nodeIdx === -1) {
|
if (nodeIdx === -1) {
|
||||||
throw createError({ statusCode: 404, message: 'Node not found in canvas' })
|
throw createError({ statusCode: 404, message: `Node ${nodeId} not found in canvas ${canvasId}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = nodes[nodeIdx]
|
const node = nodes[nodeIdx]
|
||||||
|
console.log(`[Execute] Node type: ${node.type}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let result = ''
|
let result = ''
|
||||||
@@ -48,8 +55,10 @@ export default defineEventHandler(async (event) => {
|
|||||||
fullPrompt = `CONTEXT INFORMATION:\n${contextText}\n\nUSER REQUEST/INSTRUCTION:\n${fullPrompt}`
|
fullPrompt = `CONTEXT INFORMATION:\n${contextText}\n\nUSER REQUEST/INSTRUCTION:\n${fullPrompt}`
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[AI Execute] Generating response with context for node ${nodeId}...`)
|
// Модель можно задать прямо в узле: node.model = 'qwen_coder' | 'llama' | 'gemma' | 'laguna' | 'nemotron'
|
||||||
result = await generateAIResponse(fullPrompt)
|
const model = node.model || undefined
|
||||||
|
console.log(`[AI Execute] model=${model || 'default'} nodeId=${nodeId}`)
|
||||||
|
result = await generateAIResponse(fullPrompt, 'You are a helpful assistant.', model)
|
||||||
|
|
||||||
// Сохраняем результат в текст узла
|
// Сохраняем результат в текст узла
|
||||||
nodes[nodeIdx].text = result
|
nodes[nodeIdx].text = result
|
||||||
|
|||||||
+72
-14
@@ -1,31 +1,89 @@
|
|||||||
export const generateAIResponse = async (prompt: string, systemPrompt = 'You are a helpful assistant.') => {
|
/**
|
||||||
const config = useRuntimeConfig()
|
* XFlow AI Utility — OpenRouter Integration (Stable Models)
|
||||||
const apiKey = config.deepseekApiKey || process.env.DEEPSEEK_API_KEY
|
*
|
||||||
|
* Поддерживаемые модели (наиболее стабильные и без жестких rate-limit):
|
||||||
|
*
|
||||||
|
* • poolside/laguna-m.1:free — быстрые инженерные задачи, Claude-based (default)
|
||||||
|
* • nvidia/nemotron-3-super-120b-a12b:free — MoE 120B, тяжёлая аналитика, сложные рассуждения
|
||||||
|
*/
|
||||||
|
|
||||||
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/chat/completions'
|
||||||
|
|
||||||
|
/** Доступные модели с описанием для выбора в UI */
|
||||||
|
export const AI_MODELS = {
|
||||||
|
/** Сложные инженерные и архитектурные задачи, высокая стабильность */
|
||||||
|
laguna: 'poolside/laguna-m.1:free',
|
||||||
|
/** MoE 120B тяжёлая аналитика, архитектурный анализ, сложные рассуждения */
|
||||||
|
nemotron: 'nvidia/nemotron-3-super-120b-a12b:free',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AiModelKey = keyof typeof AI_MODELS
|
||||||
|
|
||||||
|
/** Дефолтная модель, если не указана */
|
||||||
|
const DEFAULT_MODEL: AiModelKey = 'laguna'
|
||||||
|
|
||||||
|
export const generateAIResponse = async (
|
||||||
|
prompt: string,
|
||||||
|
systemPrompt = 'You are a helpful assistant.',
|
||||||
|
modelKey?: AiModelKey | string,
|
||||||
|
): Promise<string> => {
|
||||||
|
const apiKey =
|
||||||
|
process.env.NUXT_OPEN_ROUTER_API_KEY ||
|
||||||
|
process.env.OPEN_ROUTER_API_KEY
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('DEEPSEEK_API_KEY is not configured')
|
throw new Error('OPEN_ROUTER_API_KEY is not configured')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Резолвим модель: принимаем как ключ ('laguna') так и полное имя
|
||||||
|
const resolvedModel: string =
|
||||||
|
modelKey && modelKey in AI_MODELS
|
||||||
|
? AI_MODELS[modelKey as AiModelKey]
|
||||||
|
: modelKey && modelKey.includes('/')
|
||||||
|
? modelKey
|
||||||
|
: AI_MODELS[DEFAULT_MODEL]
|
||||||
|
|
||||||
|
console.log(`[AI] Using model: ${resolvedModel}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await $fetch('https://api.deepseek.com/v1/chat/completions', {
|
const response = await $fetch<any>(OPENROUTER_BASE_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${apiKey}`,
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://a-crm.controlsystem.kz',
|
||||||
|
'X-Title': 'XFlow AI Node',
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
model: 'deepseek-chat',
|
model: resolvedModel,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt },
|
||||||
],
|
],
|
||||||
stream: false
|
stream: false,
|
||||||
}
|
},
|
||||||
}) as any
|
})
|
||||||
|
|
||||||
return response.choices[0].message.content
|
const content = response?.choices?.[0]?.message?.content
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('Empty response from AI model')
|
||||||
|
}
|
||||||
|
return content
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('DeepSeek AI error:', error.data || error.message)
|
console.error(`[AI] Error with model ${resolvedModel}:`, error?.data || error?.message)
|
||||||
throw new Error(`AI generation failed: ${error.message}`)
|
throw new Error(`AI generation failed (${resolvedModel}): ${error?.message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Хелпер: список моделей для фронтенда */
|
||||||
|
export const getAvailableModels = () =>
|
||||||
|
Object.entries(AI_MODELS).map(([key, modelId]) => ({
|
||||||
|
key,
|
||||||
|
modelId,
|
||||||
|
label: modelLabels[key as AiModelKey],
|
||||||
|
}))
|
||||||
|
|
||||||
|
const modelLabels: Record<AiModelKey, string> = {
|
||||||
|
laguna: '🏊 Laguna M.1 — стабильная инженерная (Claude)',
|
||||||
|
nemotron: '🔬 Nemotron Super 120B — тяжёлая аналитика (NVIDIA)',
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U xflow -d xflow"]
|
test: [ "CMD-SHELL", "pg_isready -U xflow -d xflow" ]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -82,6 +82,8 @@ export default defineNuxtConfig({
|
|||||||
giteaOrg: process.env.GITEA_ORG || '',
|
giteaOrg: process.env.GITEA_ORG || '',
|
||||||
giteaMindmapRepo: process.env.GITEA_MINDMAP_REPO || '',
|
giteaMindmapRepo: process.env.GITEA_MINDMAP_REPO || '',
|
||||||
giteaProjectRepo: process.env.GITEA_PROJECT_REPO || '',
|
giteaProjectRepo: process.env.GITEA_PROJECT_REPO || '',
|
||||||
|
deepseekApiKey: process.env.DEEPSEEK_API_KEY || '',
|
||||||
|
qwenApiKey: '',
|
||||||
public: {
|
public: {
|
||||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
|
apiBase: process.env.NUXT_PUBLIC_API_BASE || '/api',
|
||||||
firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY || process.env.FIREBASE_API_KEY || '',
|
firebaseApiKey: process.env.NUXT_PUBLIC_FIREBASE_API_KEY || process.env.FIREBASE_API_KEY || '',
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
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,227 @@
|
|||||||
|
const { Pool } = require('pg')
|
||||||
|
|
||||||
|
// XFlow DB (through SSH tunnel)
|
||||||
|
const DB_URL = 'postgresql://xflow:xflow_pass@localhost:5433/xflow'
|
||||||
|
const pool = new Pool({ connectionString: DB_URL })
|
||||||
|
|
||||||
|
const CANVAS_ID = 'npb-aipb-project-brief'
|
||||||
|
const CANVAS_NAME = '🎨 NPB/AIPB — Project Brief'
|
||||||
|
const OWNER_ID = 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2'
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// NODES — merged from NPB.json + XFlow pre-prompt context
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
const nodes = [
|
||||||
|
// ── CENTRAL NODE ──────────────────────────
|
||||||
|
{
|
||||||
|
id: 'main',
|
||||||
|
type: 'text',
|
||||||
|
x: -474, y: -603,
|
||||||
|
width: 450, height: 220,
|
||||||
|
label: '🎨 AIPB — AI Portrait Builder',
|
||||||
|
text: `# 🎨 AIPB\n\n**Генератор художественных портретов с ИИ.**\n\n- **Статус:** v1.1.0 Architecture\n- **Стек:** Nuxt 3, MediaPipe, Replicate, Firebase\n- **Концепт:** Парные экраны (Display + Remote)\n- **Фишка:** Управление жестами или с планшета`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1e293b', border: '#334155' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── TECH STACK ────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'tech-stack',
|
||||||
|
type: 'text',
|
||||||
|
x: -1049, y: -262,
|
||||||
|
width: 400, height: 380,
|
||||||
|
label: '🏗️ Tech Stack',
|
||||||
|
text: `# 🏗️ Стек AIPB\n\n- **Framework:** Nuxt 3 (SSR + Nitro)\n- **CV:** MediaPipe Hands (JS)\n- **AI APIs:** Replicate (SDXL), Face Swap\n- **Backend/Sync:** Firebase RTDB\n- **Auth:** Firebase Auth\n- **State:** Pinia + Firebase Listeners\n- **UI:** TailwindCSS + Animations`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1e293b', border: '#334155' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── AUTH SYSTEM ───────────────────────────
|
||||||
|
{
|
||||||
|
id: 'auth-system',
|
||||||
|
type: 'text',
|
||||||
|
x: -1049, y: 180,
|
||||||
|
width: 400, height: 300,
|
||||||
|
label: '🔐 Auth Stack',
|
||||||
|
text: `# 🔐 Firebase Auth & Roles\n\n- **Admin:** Управление киосками, стата\n- **Kiosk (Service):** Авторизация самой точки\n- **Remote (Anonymous):** Временный токен для планшета\n- **Security:** Firebase Rules (доступ только к своей сессии)`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1e293b', border: '#334155' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── DUAL DEVICE ───────────────────────────
|
||||||
|
{
|
||||||
|
id: 'dual-device',
|
||||||
|
type: 'text',
|
||||||
|
x: 100, y: -603,
|
||||||
|
width: 450, height: 320,
|
||||||
|
label: '📡 Синхронизация пар',
|
||||||
|
text: `# 📡 Dual-Device Sync\n\n- **Pairing:** Генерация PIN/QR кода на ПК\n- **Connection:** Сканирование с планшета\n- **Realtime:** Firebase RTDB для синхронизации State\n- **Latency:** <100ms отклик интерфейса`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#0f766e', border: '#115e59' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── KIOSK ARCHITECTURE ────────────────────
|
||||||
|
{
|
||||||
|
id: 'architecture',
|
||||||
|
type: 'text',
|
||||||
|
x: 600, y: -603,
|
||||||
|
width: 450, height: 350,
|
||||||
|
label: '🖥️ Kiosk Architecture',
|
||||||
|
text: `# 🖥️ Разделение UI\n\n**1. Display View (ТВ/ПК):**\n- Трансляция камеры\n- Красивые idle-анимации\n- Рендер ИИ и таймеров\n- QR-код для скачивания\n\n**2. Controller View (Планшет):**\n- Кнопки выбора (Пол, Стиль, Язык)\n- Кнопка спуска затвора\n- Ввод Email/Telegram`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#0f766e', border: '#115e59' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── USER FLOW ─────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'user-flow',
|
||||||
|
type: 'text',
|
||||||
|
x: -128, y: 147,
|
||||||
|
width: 450, height: 400,
|
||||||
|
label: '🔄 User Flow (8 Steps)',
|
||||||
|
text: `# 🔄 Процесс генерации\n\n1. **Welcome** — Старт на планшете/жестом\n2. **Language** — Выбор языка\n3. **Gender** — Выбор пола (M/F)\n4. **Style** — Выбор стиля\n5. **Capture** — Фото с таймером (3с)\n6. **Confirm** — Проверка кадра на планшете\n7. **Loading** — Процесс ИИ генерации (на экране)\n8. **Result** — Сохранение и шеринг`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1e293b', border: '#334155' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── GESTURE CONTROLS ──────────────────────
|
||||||
|
{
|
||||||
|
id: 'gestures',
|
||||||
|
type: 'text',
|
||||||
|
x: -566, y: -72,
|
||||||
|
width: 400, height: 300,
|
||||||
|
label: '👋 Gesture Controls',
|
||||||
|
text: `# 👋 Управление жестами\n\n- **✊ Fist:** Клик / Выбор\n- **✌️ V-sign:** Скролл вниз\n- **🖐️ Palm:** Пауза / Стоп\n\n*Альтернатива или дополнение к планшету*\n*Используется MediaPipe Hands (WASM в браузере)*`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1e293b', border: '#334155' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── ROADMAP ───────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'roadmap',
|
||||||
|
type: 'text',
|
||||||
|
x: 380, y: 147,
|
||||||
|
width: 400, height: 300,
|
||||||
|
label: '🚀 Roadmap',
|
||||||
|
text: `# 🚀 Планы (v1.2.0+)\n\n- [ ] Прямая интеграция с TG-ботом\n- [ ] Админ-панель со статистикой\n- [ ] Оффлайн фоллбэк для обрыва связи\n- [ ] Платные премиум-стили\n- [ ] Галерея работ сообщества`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1e293b', border: '#334155' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── AI STYLES ─────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'ai-styles',
|
||||||
|
type: 'text',
|
||||||
|
x: 850, y: 147,
|
||||||
|
width: 400, height: 320,
|
||||||
|
label: '🎭 AI Styles',
|
||||||
|
text: `# 🎭 Художественные стили\n\n- **Cinematic:** Кино-освещение\n- **Anime:** Стилизация под аниме\n- **Fantasy:** Магическая атмосфера\n- **Cyberpunk:** Неон и технологии\n- **Professional:** Студийное фото\n- **Artistic:** Масляная живопись\n\n*Провайдер: Replicate (SDXL)*`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#312e81', border: '#3730a3' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── INFRASTRUCTURE (SERVER) ───────────────
|
||||||
|
{
|
||||||
|
id: 'infrastructure',
|
||||||
|
type: 'text',
|
||||||
|
x: 850, y: -603,
|
||||||
|
width: 450, height: 400,
|
||||||
|
label: '🖥️ Инфраструктура сервера',
|
||||||
|
text: `# 🖥️ Сервер\n\n- **VM (Debian):** 10.0.0.120\n- **Jump-host:** alex@178.238.78.98\n- **Docker Network:** \`infra\`\n- **Services:**\n - \`postgres:5432\` (Shared PostgreSQL)\n - \`gitea:3001\` (Git server)\n - \`xflow:3002\` (XFlow CRM)\n - \`npb-app:3000\` (NPB/AIPB — наш проект)\n- **Reverse Proxy:** Nginx (FastPanel)\n\n**SSH-туннель к БД:**\n\`\`\`\nssh -J alex@178.238.78.98 -L 5432:localhost:5432 root@10.0.0.120\n\`\`\``,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1c1917', border: '#44403c' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── DEPLOYMENT ────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'deployment',
|
||||||
|
type: 'text',
|
||||||
|
x: -474, y: 650,
|
||||||
|
width: 450, height: 300,
|
||||||
|
label: '🚀 Деплой & CI/CD',
|
||||||
|
text: `# 🔄 Деплой (Gitea Actions)\n\n1. \`git push\` → Gitea\n2. Gitea Actions запускает \`deploy.yml\`\n3. Runner делает \`git pull\` на сервере\n4. Пересобирает Docker-контейнер \`npb-app\`\n\n**Время деплоя:** ~3-5 минут\n\n⚠️ В PowerShell — команды по одной, без \`&&\``,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#14532d', border: '#166534' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── KNOWN ISSUES ──────────────────────────
|
||||||
|
{
|
||||||
|
id: 'known-issues',
|
||||||
|
type: 'text',
|
||||||
|
x: -1049, y: 580,
|
||||||
|
width: 400, height: 300,
|
||||||
|
label: '🐛 Известные проблемы',
|
||||||
|
text: `# 🐛 Известные проблемы\n\n- **PWA:** Clear site data при non-precached-url ошибке\n- **Gitea push 403:** Токен мог истечь — проверить \`GITEA_TOKEN\` в .env\n- **AI Key:** Groq ключ может быть невалидным — обновить в .env\n- **Logs:** Убрать debug-логи из \`execute.post.ts\` перед продом`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#450a0a', border: '#7f1d1d' }
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── DEV TIPS ──────────────────────────────
|
||||||
|
{
|
||||||
|
id: 'dev-tips',
|
||||||
|
type: 'text',
|
||||||
|
x: 380, y: 580,
|
||||||
|
width: 450, height: 300,
|
||||||
|
label: '💡 Dev Tips',
|
||||||
|
text: `# 💡 Советы по разработке\n\n- **Локальный запуск:** \`npm run dev\`\n- **SSH-туннель к БД:**\n \`ssh -J alex@178.238.78.98 -L 5432:localhost:5432 root@10.0.0.120\`\n- **Список пользователей:** \`node scripts/list_users.cjs\`\n- **Тест AI:** \`node scripts/test_qwen.cjs\`\n- **Не использовать \`&&\` в PowerShell!**`,
|
||||||
|
state: 'pending',
|
||||||
|
style: { background: '#1e293b', border: '#334155' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// EDGES — connections between nodes
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
const edges = [
|
||||||
|
// Main → top cluster
|
||||||
|
{ id: 'e1', source: 'main', target: 'dual-device' },
|
||||||
|
{ id: 'e2', source: 'dual-device', target: 'architecture' },
|
||||||
|
{ id: 'e3', source: 'architecture', target: 'infrastructure' },
|
||||||
|
|
||||||
|
// Main → left cluster
|
||||||
|
{ id: 'e4', source: 'main', target: 'tech-stack' },
|
||||||
|
{ id: 'e5', source: 'tech-stack', target: 'auth-system' },
|
||||||
|
|
||||||
|
// Main → center/bottom cluster
|
||||||
|
{ id: 'e6', source: 'main', target: 'gestures' },
|
||||||
|
{ id: 'e7', source: 'main', target: 'user-flow' },
|
||||||
|
{ id: 'e8', source: 'user-flow', target: 'roadmap' },
|
||||||
|
{ id: 'e9', source: 'roadmap', target: 'ai-styles' },
|
||||||
|
|
||||||
|
// Bottom
|
||||||
|
{ id: 'e10', source: 'main', target: 'deployment' },
|
||||||
|
{ id: 'e11', source: 'auth-system', target: 'known-issues' },
|
||||||
|
{ id: 'e12', source: 'deployment', target: 'dev-tips' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
// DB UPSERT
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const check = await pool.query('SELECT id FROM "Canvas" WHERE id = $1', [CANVAS_ID])
|
||||||
|
|
||||||
|
if (check.rows.length > 0) {
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE "Canvas" SET nodes = $1, edges = $2, name = $3, "updatedAt" = NOW() WHERE id = $4',
|
||||||
|
[JSON.stringify(nodes), JSON.stringify(edges), CANVAS_NAME, CANVAS_ID]
|
||||||
|
)
|
||||||
|
console.log('✅ NPB/AIPB Brief canvas UPDATED!')
|
||||||
|
} else {
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO "Canvas" (id, name, "ownerId", "isPublic", nodes, edges, "createdAt", "updatedAt") VALUES ($1, $2, $3, true, $4, $5, NOW(), NOW())',
|
||||||
|
[CANVAS_ID, CANVAS_NAME, OWNER_ID, JSON.stringify(nodes), JSON.stringify(edges)]
|
||||||
|
)
|
||||||
|
console.log('✅ NPB/AIPB Brief canvas CREATED!')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n📊 Stats: ${nodes.length} nodes, ${edges.length} edges`)
|
||||||
|
console.log(`🔗 Canvas ID: ${CANVAS_ID}`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Error:', e.message)
|
||||||
|
} finally {
|
||||||
|
await pool.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
const { Pool } = require('pg')
|
||||||
|
|
||||||
|
// Connection string from .env
|
||||||
|
const DB_URL = 'postgresql://xflow:xflow_pass@localhost:5433/xflow'
|
||||||
|
const pool = new Pool({ connectionString: DB_URL })
|
||||||
|
|
||||||
|
const CANVAS_ID = 'xflow-project-brief'
|
||||||
|
const OWNER_ID = 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2'
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
id: 'main',
|
||||||
|
type: 'text',
|
||||||
|
x: 0, y: 0,
|
||||||
|
width: 450, height: 220,
|
||||||
|
label: '🤖 XFlow — Project Brief',
|
||||||
|
text: `# 🤖 XFlow\n\n**Visual CRM/ERP system based on Obsidian Canvas.**\n\n- **Owner:** Alex Ives (almalexyz@gmail.com)\n- **Firebase UID:** \`GqbpMyUW0NaxjIy5nvCiXCiQu9t2\`\n- **Repo:** [git.a-crm.controlsystem.kz](http://git.a-crm.controlsystem.kz/AVTEX/XFlow)\n- **Prod:** [a-crm.controlsystem.kz](https://a-crm.controlsystem.kz)\n- **Local:** [localhost:3000](http://localhost:3000)`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tech-stack',
|
||||||
|
type: 'text',
|
||||||
|
x: 600, y: -250,
|
||||||
|
width: 400, height: 350,
|
||||||
|
label: '🏗️ Tech Stack',
|
||||||
|
text: `# 🏗️ Стек технологий\n\n- **Frontend:** Nuxt 3 (SSR + API routes)\n- **Canvas Engine:** Vue Flow\n- **Styling:** TailwindCSS\n- **ORM:** Prisma 7 + @prisma/adapter-pg\n- **Database:** PostgreSQL 16 (Docker)\n- **Auth:** Firebase Auth + JWT\n- **PWA:** @vite-pwa/nuxt\n- **CI/CD:** Gitea Actions`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'infrastructure',
|
||||||
|
type: 'text',
|
||||||
|
x: 600, y: 250,
|
||||||
|
width: 400, height: 350,
|
||||||
|
label: '🖥️ Infrastructure',
|
||||||
|
text: `# 🖥️ Инфраструктура\n\n- **VM (Debian):** 10.0.0.120\n- **Docker Network:** \`infra\`\n- **Services:**\n - \`postgres:5432\` (Shared DB)\n - \`gitea:3001\` (Git server)\n - \`xflow:3002\` (Production app)\n - \`npb-app:3000\` (Other project)\n- **Reverse Proxy:** Nginx (FastPanel)\n\n[🔗 Перейти к карте инфраструктуры](canvas/infrastructure)`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'database',
|
||||||
|
type: 'text',
|
||||||
|
x: 1100, y: 0,
|
||||||
|
width: 400, height: 300,
|
||||||
|
label: '🗄️ Database Schema',
|
||||||
|
text: `# 🗄️ Prisma Schema\n\n- **User:** Firebase ID, Email, Role (admin/user)\n- **Canvas:** ID, Name, Owner, Public, Nodes (Json), Edges (Json)\n\n⚠️ **Важно:** Узлы хранятся как JSON-массив. При обновлении нужно перезаписывать весь массив.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'auth-flow',
|
||||||
|
type: 'text',
|
||||||
|
x: -600, y: -250,
|
||||||
|
width: 400, height: 300,
|
||||||
|
label: '🔐 Auth Flow',
|
||||||
|
text: `# 🔐 Аутентификация\n\n1. Client logins via Firebase Google Auth.\n2. Token sent to \`/api/auth/google\`.\n3. Server upserts user (ID = Firebase UID).\n4. Returns JWT in \`xflow_token\` cookie.\n5. \`requireAuth\` middleware protects routes.\n\n⚠️ User.id MUST match Firebase UID.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'features',
|
||||||
|
type: 'text',
|
||||||
|
x: -600, y: 250,
|
||||||
|
width: 400, height: 420,
|
||||||
|
label: '✅ Features',
|
||||||
|
text: `# ✅ Реализовано\n\n- **Canvas:** Interactive Vue Flow editor\n- **Nodes:** text, task, ai, webhook, file, git\n- **Kanban:** Board (/tasks) with DnD\n- **AI:** Execution with context from neighbors\n- **Webhooks:** GET/POST execution\n- **Git:** Sync maps with Gitea\n- **Auth:** RBAC (user/admin) & Firebase\n- **PWA:** Service Worker & Manifest`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'todo',
|
||||||
|
type: 'text',
|
||||||
|
x: 0, y: 450,
|
||||||
|
width: 400, height: 320,
|
||||||
|
label: '🔴 To-Do',
|
||||||
|
text: `# 🔴 Что нужно доделать\n\n- **AI Key:** Get a working Groq key (llama-3.3-70b).\n- **Dashboard:** Main page with stats.\n- **Webhooks:** Telegram notifications.\n- **Users:** Invitation system.\n- **Stability:** Fix PWA service worker caching issue.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dev-tips',
|
||||||
|
type: 'text',
|
||||||
|
x: 0, y: -450,
|
||||||
|
width: 450, height: 320,
|
||||||
|
label: '💡 Dev Tips',
|
||||||
|
text: `# 💡 Советы по разработке\n\n- **Local run:** \`npm run dev\`\n- **DB Tunnel:** \`ssh -J alex@178.238.78.98 -L 5433:localhost:5432 root@10.0.0.120\`\n- **Check DB:** \`node scripts/list_users.cjs\`\n- **Test AI:** \`node scripts/test_qwen.cjs\`\n- **Note:** Don't use \`git push && ...\` in PowerShell.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'issues',
|
||||||
|
type: 'text',
|
||||||
|
x: -1100, y: 0,
|
||||||
|
width: 400, height: 280,
|
||||||
|
label: '🐛 Known Issues',
|
||||||
|
text: `# 🐛 Известные проблемы\n\n- **PWA:** Clear site data if non-precached-url error occurs.\n- **Gitea:** 403 error on push if token expired.\n- **Logs:** Remove debug logs from \`execute.post.ts\` before prod.`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const edges = [
|
||||||
|
{ id: 'e1', source: 'main', target: 'tech-stack' },
|
||||||
|
{ id: 'e2', source: 'main', target: 'infrastructure' },
|
||||||
|
{ id: 'e3', source: 'main', target: 'features' },
|
||||||
|
{ id: 'e4', source: 'main', target: 'auth-flow' },
|
||||||
|
{ id: 'e5', source: 'main', target: 'todo' },
|
||||||
|
{ id: 'e6', source: 'main', target: 'dev-tips' },
|
||||||
|
{ id: 'e7', source: 'tech-stack', target: 'database' },
|
||||||
|
{ id: 'e8', source: 'infrastructure', target: 'database' },
|
||||||
|
{ id: 'e9', source: 'features', target: 'issues' }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const check = await pool.query('SELECT id FROM "Canvas" WHERE id = $1', [CANVAS_ID])
|
||||||
|
|
||||||
|
if (check.rows.length > 0) {
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE "Canvas" SET nodes = $1, edges = $2, name = $3, "updatedAt" = NOW() WHERE id = $4',
|
||||||
|
[JSON.stringify(nodes), JSON.stringify(edges), '🤖 XFlow — Project Brief', CANVAS_ID]
|
||||||
|
)
|
||||||
|
console.log('✅ XFlow Brief canvas updated!')
|
||||||
|
} else {
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO "Canvas" (id, name, "ownerId", "isPublic", nodes, edges, "createdAt", "updatedAt") VALUES ($1, $2, $3, true, $4, $5, NOW(), NOW())',
|
||||||
|
[CANVAS_ID, '🤖 XFlow — Project Brief', OWNER_ID, JSON.stringify(nodes), JSON.stringify(edges)]
|
||||||
|
)
|
||||||
|
console.log('✅ XFlow Brief canvas created!')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Error:', e.message)
|
||||||
|
} finally {
|
||||||
|
await pool.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
@@ -2,7 +2,7 @@ const { Pool } = require('pg');
|
|||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||||
const res = await pool.query('SELECT id, username, email FROM "User"');
|
const res = await pool.query('SELECT id, username, email, role FROM "User"');
|
||||||
console.log(JSON.stringify(res.rows, null, 2));
|
console.log(JSON.stringify(res.rows, null, 2));
|
||||||
await pool.end();
|
await pool.end();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/**
|
||||||
|
* Тест стабильных OpenRouter моделей (Laguna & Nemotron)
|
||||||
|
* Запуск: node scripts/test_openrouter.cjs
|
||||||
|
*/
|
||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
|
const OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1/chat/completions'
|
||||||
|
|
||||||
|
const MODELS = {
|
||||||
|
laguna: 'poolside/laguna-m.1:free',
|
||||||
|
nemotron: 'nvidia/nemotron-3-super-120b-a12b:free',
|
||||||
|
}
|
||||||
|
|
||||||
|
const LABELS = {
|
||||||
|
laguna: '🏊 Laguna M.1 (Claude)',
|
||||||
|
nemotron: '🔬 Nemotron Super 120B (NVIDIA)',
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testModel(key, modelId) {
|
||||||
|
const apiKey = process.env.OPEN_ROUTER_API_KEY || process.env.NUXT_OPEN_ROUTER_API_KEY
|
||||||
|
if (!apiKey) throw new Error('OPEN_ROUTER_API_KEY not set in .env')
|
||||||
|
|
||||||
|
const start = Date.now()
|
||||||
|
try {
|
||||||
|
const res = await fetch(OPENROUTER_BASE_URL, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'HTTP-Referer': 'https://a-crm.controlsystem.kz',
|
||||||
|
'X-Title': 'XFlow AI Test',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: modelId,
|
||||||
|
messages: [
|
||||||
|
{ role: 'system', content: 'You are a helpful assistant. Reply briefly.' },
|
||||||
|
{ role: 'user', content: 'Say "OK" and your model name in one short sentence.' },
|
||||||
|
],
|
||||||
|
stream: false,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
const elapsed = ((Date.now() - start) / 1000).toFixed(1)
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.log(` ❌ ${LABELS[key]} — HTTP ${res.status}: ${data?.error?.message || JSON.stringify(data)}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = data?.choices?.[0]?.message?.content || '(empty)'
|
||||||
|
console.log(` ✅ ${LABELS[key]} (${elapsed}s): ${content.trim().slice(0, 120)}`)
|
||||||
|
} catch (err) {
|
||||||
|
console.log(` ❌ ${LABELS[key]} — ${err.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('\n🚀 XFlow — OpenRouter Stable Models Test\n')
|
||||||
|
const apiKey = process.env.OPEN_ROUTER_API_KEY || process.env.NUXT_OPEN_ROUTER_API_KEY
|
||||||
|
console.log(`🔑 API Key: ${apiKey ? apiKey.slice(0, 20) + '...' : 'NOT FOUND'}\n`)
|
||||||
|
|
||||||
|
for (const [key, modelId] of Object.entries(MODELS)) {
|
||||||
|
process.stdout.write(`Testing ${LABELS[key]}...\n`)
|
||||||
|
await testModel(key, modelId)
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n✨ Done!\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
const https = require('https')
|
||||||
|
|
||||||
|
const apiKey = process.env.NUXT_QWEN_API_KEY || process.env.QWEN_API_KEY
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
console.error('❌ QWEN_API_KEY не найден в .env!')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Ключ найден:', apiKey.substring(0, 8) + '...')
|
||||||
|
console.log('📡 Отправляю запрос к Qwen API...')
|
||||||
|
|
||||||
|
const body = JSON.stringify({
|
||||||
|
model: 'qwen-turbo',
|
||||||
|
messages: [
|
||||||
|
{ role: 'user', content: 'Say "Hello from Qwen!" in Russian, briefly.' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
hostname: 'dashscope.aliyuncs.com',
|
||||||
|
path: '/compatible-mode/v1/chat/completions',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${apiKey}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Content-Length': Buffer.byteLength(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = https.request(options, (res) => {
|
||||||
|
let data = ''
|
||||||
|
res.on('data', chunk => data += chunk)
|
||||||
|
res.on('end', () => {
|
||||||
|
console.log('📨 Статус:', res.statusCode)
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(data)
|
||||||
|
if (parsed.choices) {
|
||||||
|
console.log('✅ Ответ:', parsed.choices[0].message.content)
|
||||||
|
} else {
|
||||||
|
console.log('❌ Ответ без choices:', JSON.stringify(parsed, null, 2))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('❌ Сырой ответ:', data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
req.on('error', (e) => {
|
||||||
|
console.error('❌ Ошибка запроса:', e.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
req.write(body)
|
||||||
|
req.end()
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
const { Pool } = require('pg')
|
||||||
|
|
||||||
|
const DB_URL = process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/xflow'
|
||||||
|
const pool = new Pool({ connectionString: DB_URL })
|
||||||
|
|
||||||
|
const CANVAS_ID = 'infrastructure'
|
||||||
|
const OWNER_ID = 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2'
|
||||||
|
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
id: 'server',
|
||||||
|
type: 'text',
|
||||||
|
x: 0, y: 0,
|
||||||
|
width: 420, height: 280,
|
||||||
|
label: '🖥️ Сервер',
|
||||||
|
text: `# 🖥️ Сервер (ВМ)\n\n- **IP (внутренний):** 10.0.0.120\n- **Jump-хост:** 178.238.78.98 (alex)\n- **ОС:** Debian GNU/Linux\n- **Nginx:** управляется FastPanel (не трогаем)\n- **Доступ:** \`ssh -J alex@178.238.78.98 root@10.0.0.120\``
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'docker-network',
|
||||||
|
type: 'text',
|
||||||
|
x: 500, y: -200,
|
||||||
|
width: 420, height: 200,
|
||||||
|
label: '🐳 Docker сеть: infra',
|
||||||
|
text: `# 🐳 Docker сеть: infra\n\nВсе контейнеры общаются внутри сети \`infra\`.\nМежду собой они обращаются по именам контейнеров.\n\nПример: приложение XFlow достучивается до базы\nпо адресу \`postgres:5432\`, а не через localhost.`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'postgres',
|
||||||
|
type: 'text',
|
||||||
|
x: 500, y: 100,
|
||||||
|
width: 420, height: 320,
|
||||||
|
label: '🗄️ PostgreSQL (контейнер)',
|
||||||
|
text: `# 🗄️ PostgreSQL (контейнер: postgres)\n\n- **Порт:** 5432 (проброшен на хост)\n- **Суперпользователь:** \`xflow / xflow_pass\`\n\n**Базы данных:**\n| База | Владелец | Назначение |\n|------|----------|------------|\n| \`xflow\` | xflow | Наш проект |\n| \`gitea\` | gitea | Gitea |\n| \`artbazar\` | gitea | Другой проект |\n| \`postgres\` | gitea | Не используем |\n\n⚠️ **Важно:** локальный Windows Postgres занимает порт 5432, поэтому туннель нужно пробрасывать на порт 5433!`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'xflow-app',
|
||||||
|
type: 'text',
|
||||||
|
x: 500, y: 500,
|
||||||
|
width: 420, height: 240,
|
||||||
|
label: '🚀 XFlow (контейнер)',
|
||||||
|
text: `# 🚀 XFlow (контейнер: xflow)\n\n- **Внутренний порт:** 3000\n- **Внешний порт:** 3002\n- **URL в проде:** https://a-crm.controlsystem.kz\n- **DATABASE_URL:** \`postgresql://xflow:xflow_pass@postgres:5432/xflow\`\n- **Деплой:** Gitea Actions → git pull → docker-compose restart\n- **Папка на сервере:** /opt/xflow`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gitea',
|
||||||
|
type: 'text',
|
||||||
|
x: -500, y: 100,
|
||||||
|
width: 420, height: 240,
|
||||||
|
label: '🦊 Gitea (контейнер)',
|
||||||
|
text: `# 🦊 Gitea (контейнер: gitea)\n\n- **Порт:** 3001 → http://git.a-crm.controlsystem.kz\n- **SSH:** 2221\n- **БД:** \`gitea\` база внутри postgres-контейнера\n- **Репозиторий:** AVTEX/XFlow\n- **Runner:** gitea-runner (отдельный контейнер)\n- **Workflow:** .gitea/workflows/deploy.yml`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'npb',
|
||||||
|
type: 'text',
|
||||||
|
x: -500, y: 450,
|
||||||
|
width: 420, height: 160,
|
||||||
|
label: '📦 NPB (контейнер)',
|
||||||
|
text: `# 📦 NPB (другое приложение)\n\n- **npb-app:** порт 3000 (стандартный)\n- **npb:** порт 3003\n- ⚠️ Не трогаем — другой проект организации`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tunnel',
|
||||||
|
type: 'text',
|
||||||
|
x: -500, y: -300,
|
||||||
|
width: 500, height: 320,
|
||||||
|
label: '🔌 SSH Туннель (правильный)',
|
||||||
|
text: `# 🔌 SSH Туннель к продовой БД\n\n**ПРАВИЛЬНАЯ команда:**\n\`\`\`bash\nssh -J alex@178.238.78.98 \\\\\n -o ServerAliveInterval=60 \\\\\n -L 5433:localhost:5432 \\\\\n root@10.0.0.120\n\`\`\`\n\n**Почему порт 5433?**\nПорт 5432 на Windows уже занят локальным PostgreSQL!\nЕсли пробрасывать на 5432 — туннель проваливается в локальную БД.\n\n**Локальный .env для работы с прод-БД:**\n\`\`\`\nDATABASE_URL=postgresql://xflow:xflow_pass\n @localhost:5433/xflow\n\`\`\``
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nginx',
|
||||||
|
type: 'text',
|
||||||
|
x: 0, y: -350,
|
||||||
|
width: 420, height: 200,
|
||||||
|
label: '🌐 Nginx (FastPanel)',
|
||||||
|
text: `# 🌐 Nginx (управляется FastPanel)\n\n- Обратный прокси для всех сервисов\n- **a-crm.controlsystem.kz** → 3002 (XFlow)\n- **git.a-crm.controlsystem.kz** → 3001 (Gitea)\n- SSL/TLS сертификаты\n- ⚠️ Конфиги не трогаем вручную — только через FastPanel`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deploy-flow',
|
||||||
|
type: 'text',
|
||||||
|
x: 1000, y: 100,
|
||||||
|
width: 420, height: 300,
|
||||||
|
label: '🔄 Процесс деплоя',
|
||||||
|
text: `# 🔄 Процесс деплоя\n\n1. **git push** → локальный компьютер → Gitea\n2. **Gitea Actions** запускает \`deploy.yml\`\n3. **Runner** (контейнер gitea-runner) выполняет:\n - \`git pull\` в /opt/xflow\n - \`npm ci && npm run build\`\n - \`docker-compose restart xflow\`\n4. **Приложение** перезапускается с новым кодом\n\n**Время деплоя:** ~2-3 минуты`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const edges = [
|
||||||
|
{ id: 'e1', source: 'server', target: 'docker-network' },
|
||||||
|
{ id: 'e2', source: 'docker-network', target: 'postgres' },
|
||||||
|
{ id: 'e3', source: 'docker-network', target: 'xflow-app' },
|
||||||
|
{ id: 'e4', source: 'docker-network', target: 'gitea' },
|
||||||
|
{ id: 'e5', source: 'docker-network', target: 'npb' },
|
||||||
|
{ id: 'e6', source: 'nginx', target: 'xflow-app' },
|
||||||
|
{ id: 'e7', source: 'nginx', target: 'gitea' },
|
||||||
|
{ id: 'e8', source: 'tunnel', target: 'postgres' },
|
||||||
|
{ id: 'e9', source: 'gitea', target: 'deploy-flow' },
|
||||||
|
{ id: 'e10', source: 'deploy-flow', target: 'xflow-app' },
|
||||||
|
{ id: 'e11', source: 'xflow-app', target: 'postgres' }
|
||||||
|
]
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
// Проверяем существует ли карта
|
||||||
|
const check = await pool.query('SELECT id FROM "Canvas" WHERE id = $1', [CANVAS_ID])
|
||||||
|
|
||||||
|
if (check.rows.length > 0) {
|
||||||
|
await pool.query(
|
||||||
|
'UPDATE "Canvas" SET nodes = $1, edges = $2, name = $3, "updatedAt" = NOW() WHERE id = $4',
|
||||||
|
[JSON.stringify(nodes), JSON.stringify(edges), '🖥️ Инфраструктура Сервера', CANVAS_ID]
|
||||||
|
)
|
||||||
|
console.log('✅ Карта инфраструктуры обновлена!')
|
||||||
|
} else {
|
||||||
|
await pool.query(
|
||||||
|
'INSERT INTO "Canvas" (id, name, "ownerId", "isPublic", nodes, edges, "createdAt", "updatedAt") VALUES ($1, $2, $3, true, $4, $5, NOW(), NOW())',
|
||||||
|
[CANVAS_ID, '🖥️ Инфраструктура Сервера', OWNER_ID, JSON.stringify(nodes), JSON.stringify(edges)]
|
||||||
|
)
|
||||||
|
console.log('✅ Карта инфраструктуры создана!')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('❌ Ошибка:', e.message)
|
||||||
|
} finally {
|
||||||
|
await pool.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
+33
-15
@@ -1,21 +1,39 @@
|
|||||||
# XFlow Server Configuration
|
# --- Database Settings ---
|
||||||
# Copy this file to .env and update the values
|
POSTGRES_USER=admin
|
||||||
|
POSTGRES_PASSWORD=your_secure_password
|
||||||
|
|
||||||
# Domain configuration
|
DB_NAME_GITEA=gitea_db
|
||||||
GITEA_DOMAIN=your-domain.com
|
DB_NAME_NPB=npb_app_db
|
||||||
|
DB_NAME_XFLOW=xflow_crm_db
|
||||||
|
|
||||||
# Gitea Configuration
|
# --- External Ports ---
|
||||||
GITEA_TOKEN=your_gitea_api_token_here
|
DB_EXTERNAL_PORT=5432
|
||||||
|
ADMINER_PORT=8080
|
||||||
|
MINIO_API_PORT=9000
|
||||||
|
MINIO_CONSOLE_PORT=9001
|
||||||
|
GITEA_EXTERNAL_PORT=3001
|
||||||
|
XFLOW_PORT=3002
|
||||||
|
NBP_PORT=3000
|
||||||
|
|
||||||
# Security
|
# --- MinIO Settings ---
|
||||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
MINIO_ROOT_USER=admin
|
||||||
|
MINIO_ROOT_PASSWORD=your_minio_password
|
||||||
|
|
||||||
# WebDAV (optional)
|
# --- Gitea Settings ---
|
||||||
WEBDAV_USER=admin
|
GITEA_RUNNER_TOKEN=your_runner_token
|
||||||
WEBDAV_PASSWORD=secure_password_here
|
GITEA_RUNNER_NAME=avtex-runner
|
||||||
|
GITEA_TOKEN=your_personal_access_token
|
||||||
|
GITEA_ORG=AVTEX
|
||||||
|
GITEA_MINDMAP_REPO=MMAVTEX
|
||||||
|
GITEA_PROJECT_REPO=XFlow
|
||||||
|
|
||||||
# Database (change in production!)
|
# --- Firebase Settings ---
|
||||||
POSTGRES_PASSWORD=change_this_password
|
FIREBASE_API_KEY=
|
||||||
|
FIREBASE_AUTH_DOMAIN=
|
||||||
|
FIREBASE_PROJECT_ID=
|
||||||
|
FIREBASE_APP_ID=
|
||||||
|
FIREBASE_MESSAGING_SENDER_ID=
|
||||||
|
|
||||||
# API Configuration
|
# --- AI & External APIs ---
|
||||||
API_PORT=8080
|
GOOGLE_API_KEY=
|
||||||
|
REPLICATE_API_KEY=
|
||||||
|
|||||||
+20
-77
@@ -1,96 +1,39 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
# Инфраструктура AVTEX
|
# XFlow CRM Service Configuration
|
||||||
# FastPanel проксирует порты напрямую — nginx контейнер не нужен
|
# Этот файл описывает только само приложение.
|
||||||
# Один postgres для всех проектов организации
|
# База данных, Gitea и хранилища являются внешними сервисами.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
xflow_crm:
|
||||||
# PostgreSQL — общий для всех проектов
|
image: node:22-alpine
|
||||||
postgres:
|
container_name: xflow_crm
|
||||||
image: postgres:16-alpine
|
|
||||||
container_name: postgres
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- infra
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${POSTGRES_USER:-admin}
|
|
||||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
||||||
POSTGRES_DB: postgres
|
|
||||||
volumes:
|
|
||||||
- postgres-data:/var/lib/postgresql/data
|
|
||||||
- /etc/timezone:/etc/timezone:ro
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
|
|
||||||
# Gitea — Git сервер организации AVTEX
|
|
||||||
gitea:
|
|
||||||
image: gitea/gitea:latest
|
|
||||||
container_name: gitea
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- infra
|
|
||||||
ports:
|
|
||||||
- "3001:3000"
|
|
||||||
- "2221:22"
|
|
||||||
environment:
|
|
||||||
USER_UID: 1000
|
|
||||||
USER_GID: 1000
|
|
||||||
GITEA__database__DB_TYPE: postgres
|
|
||||||
GITEA__database__HOST: postgres:5432
|
|
||||||
GITEA__database__NAME: gitea
|
|
||||||
GITEA__database__USER: gitea
|
|
||||||
GITEA__database__PASSWD: ${GITEA_DB_PASSWORD:-gitea_password}
|
|
||||||
GITEA__server__DOMAIN: git.a-crm.controlsystem.kz
|
|
||||||
GITEA__server__ROOT_URL: http://git.a-crm.controlsystem.kz/
|
|
||||||
GITEA__server__SSH_DOMAIN: git.a-crm.controlsystem.kz
|
|
||||||
GITEA__server__SSH_PORT: 2221
|
|
||||||
GITEA__server__SSH_LISTEN_PORT: 22
|
|
||||||
GITEA__security__INSTALL_LOCK: "true"
|
|
||||||
GITEA__service__DISABLE_REGISTRATION: "false"
|
|
||||||
volumes:
|
|
||||||
- gitea-data:/data
|
|
||||||
- /etc/timezone:/etc/timezone:ro
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
|
|
||||||
# XFlow — Mind Map приложение
|
|
||||||
xflow:
|
|
||||||
image: node:20-alpine
|
|
||||||
container_name: xflow
|
|
||||||
restart: always
|
restart: always
|
||||||
working_dir: /app
|
working_dir: /app
|
||||||
command: node .output/server/index.mjs
|
command: node .output/server/index.mjs
|
||||||
networks:
|
|
||||||
- infra
|
|
||||||
ports:
|
ports:
|
||||||
- "3002:3000"
|
- "${XFLOW_PORT:-3002}:3000"
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
NUXT_JWT_SECRET: ${NUXT_JWT_SECRET}
|
GITEA_URL: http://xflow_gitea:3000
|
||||||
GITEA_URL: http://gitea:3000
|
|
||||||
GITEA_TOKEN: ${GITEA_TOKEN}
|
GITEA_TOKEN: ${GITEA_TOKEN}
|
||||||
GITEA_ORG: ${GITEA_ORG:-AVTEX}
|
GITEA_ORG: ${GITEA_ORG:-AVTEX}
|
||||||
GITEA_MINDMAP_REPO: ${GITEA_MINDMAP_REPO:-MMAVTEX}
|
GITEA_MINDMAP_REPO: ${GITEA_MINDMAP_REPO:-MMAVTEX}
|
||||||
GITEA_PROJECT_REPO: ${GITEA_PROJECT_REPO:-XFlow}
|
GITEA_PROJECT_REPO: ${GITEA_PROJECT_REPO:-XFlow}
|
||||||
DATABASE_URL: postgresql://xflow:${XFLOW_DB_PASSWORD:-xflow_pass}@postgres:5432/xflow
|
DATABASE_URL: postgresql://${POSTGRES_USER:-admin}:${POSTGRES_PASSWORD}@xflow_db:5432/${DB_NAME_XFLOW:-xflow_crm_db}
|
||||||
NUXT_PUBLIC_FIREBASE_API_KEY: ${NUXT_PUBLIC_FIREBASE_API_KEY:-${FIREBASE_API_KEY}}
|
NUXT_PUBLIC_FIREBASE_API_KEY: ${FIREBASE_API_KEY}
|
||||||
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN:-${FIREBASE_AUTH_DOMAIN}}
|
NUXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${FIREBASE_AUTH_DOMAIN}
|
||||||
NUXT_PUBLIC_FIREBASE_PROJECT_ID: ${NUXT_PUBLIC_FIREBASE_PROJECT_ID:-${FIREBASE_PROJECT_ID}}
|
NUXT_PUBLIC_FIREBASE_PROJECT_ID: ${FIREBASE_PROJECT_ID}
|
||||||
NUXT_PUBLIC_FIREBASE_APP_ID: ${NUXT_PUBLIC_FIREBASE_APP_ID:-${FIREBASE_APP_ID}}
|
NUXT_PUBLIC_FIREBASE_APP_ID: ${FIREBASE_APP_ID}
|
||||||
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID:-${FIREBASE_MESSAGING_SENDER_ID}}
|
NUXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${FIREBASE_MESSAGING_SENDER_ID}
|
||||||
volumes:
|
volumes:
|
||||||
- /opt/xflow/.output:/app/.output
|
- .:/app # Монтируем текущую папку проекта
|
||||||
- /opt/xflow/prisma:/app/prisma:ro
|
networks:
|
||||||
- /opt/xflow/prisma.config.ts:/app/prisma.config.ts:ro
|
- xflow_infra
|
||||||
- /opt/xflow/node_modules:/app/node_modules:ro
|
|
||||||
networks:
|
networks:
|
||||||
infra:
|
xflow_infra:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres-data:
|
|
||||||
gitea-data:
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Подключаемся к postgres под суперпользователем и создаем базы
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "postgres" <<-EOSQL
|
||||||
|
CREATE DATABASE "$DB_NAME_GITEA";
|
||||||
|
CREATE DATABASE "$DB_NAME_NPB";
|
||||||
|
CREATE DATABASE "$DB_NAME_XFLOW";
|
||||||
|
EOSQL
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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();
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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