chore: align infrastructure with server
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
const { Pool } = require('pg');
|
||||
async function query() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT nodes FROM "Canvas" WHERE id = \'test-crm\'');
|
||||
console.log(JSON.stringify(res.rows[0].nodes, null, 2));
|
||||
process.exit(0);
|
||||
}
|
||||
query();
|
||||
@@ -0,0 +1,15 @@
|
||||
async function checkToken() {
|
||||
const url = 'http://git.a-crm.controlsystem.kz';
|
||||
const token = '1b3d0d3b5391c67ead8f8dc7380fb7f07434fc64';
|
||||
|
||||
const res = await fetch(`${url}/api/v1/user`, {
|
||||
headers: { 'Authorization': `token ${token}` }
|
||||
});
|
||||
if (res.ok) {
|
||||
const user = await res.json();
|
||||
console.log('Token belongs to:', user.username);
|
||||
} else {
|
||||
console.error('Failed to check token');
|
||||
}
|
||||
}
|
||||
checkToken();
|
||||
@@ -0,0 +1,13 @@
|
||||
const { Pool } = require('pg');
|
||||
async function test() {
|
||||
try {
|
||||
const pool = new Pool({ connectionString: `postgresql://postgres:postgres@localhost:5432/postgres` });
|
||||
await pool.query('CREATE DATABASE "xflow"');
|
||||
console.log('✅ SUCCESS created xflow');
|
||||
process.exit(0);
|
||||
} catch(e) {
|
||||
console.log('❌ failed:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
test();
|
||||
@@ -0,0 +1,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,72 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function createTestCanvas() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: 'task-1',
|
||||
type: 'task',
|
||||
x: 100, y: 100, width: 300, height: 200,
|
||||
text: '# Создать API пользователей\nРазработать эндпоинт для списка пользователей',
|
||||
state: 'todo',
|
||||
assigned_to: 'almalexyz@gmail.com',
|
||||
priority: 'high',
|
||||
alarm: new Date(Date.now() + 86400000).toISOString()
|
||||
},
|
||||
{
|
||||
id: 'task-2',
|
||||
type: 'task',
|
||||
x: 450, y: 100, width: 300, height: 200,
|
||||
text: '# Канбан-доска\nСделать вывод всех задач',
|
||||
state: 'in_progress',
|
||||
assigned_to: 'almalexyz',
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 'task-3',
|
||||
type: 'task',
|
||||
x: 100, y: 350, width: 300, height: 200,
|
||||
text: '# Проверка прав\nНастроить доступ к картам',
|
||||
state: 'review',
|
||||
assigned_to: 'almalexyz',
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 'task-4',
|
||||
type: 'task',
|
||||
x: 450, y: 350, width: 300, height: 200,
|
||||
text: '# Тестирование\nПроверить как работают бейджики',
|
||||
state: 'todo',
|
||||
assigned_to: 'system',
|
||||
priority: 'low'
|
||||
}
|
||||
];
|
||||
|
||||
const canvasData = {
|
||||
id: 'test-crm',
|
||||
name: 'Тестовый CRM проект',
|
||||
ownerId: 'system',
|
||||
isPublic: true,
|
||||
nodes: JSON.stringify(nodes),
|
||||
edges: JSON.stringify([])
|
||||
};
|
||||
|
||||
try {
|
||||
// Upsert canvas
|
||||
await pool.query(`
|
||||
INSERT INTO "Canvas" (id, name, "ownerId", "isPublic", nodes, edges, "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, $5, $6, NOW())
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET nodes = EXCLUDED.nodes, edges = EXCLUDED.edges, "updatedAt" = NOW()
|
||||
`, [canvasData.id, canvasData.name, canvasData.ownerId, canvasData.isPublic, canvasData.nodes, canvasData.edges]);
|
||||
|
||||
console.log('Test canvas fixed successfully!');
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
} finally {
|
||||
pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
createTestCanvas();
|
||||
@@ -0,0 +1,19 @@
|
||||
const { Pool } = require('pg');
|
||||
async function createUser() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
try {
|
||||
await pool.query(`
|
||||
INSERT INTO "User" (id, username, email, password, role, "updatedAt")
|
||||
VALUES ('almalexyz', 'almalexyz', 'almalexyz@gmail.com', 'none', 'admin', NOW())
|
||||
ON CONFLICT DO NOTHING
|
||||
`);
|
||||
await pool.query(`
|
||||
INSERT INTO "Profile" (id, "userId", "displayName", "updatedAt")
|
||||
VALUES ('almalexyz-profile', 'almalexyz', 'Alexander', NOW())
|
||||
ON CONFLICT DO NOTHING
|
||||
`);
|
||||
console.log('User created!');
|
||||
} catch(e) { console.error(e) }
|
||||
process.exit(0);
|
||||
}
|
||||
createUser();
|
||||
@@ -0,0 +1,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()
|
||||
@@ -0,0 +1,56 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const oldId = 'almalexyz';
|
||||
const newId = 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2';
|
||||
|
||||
console.log(`Fixing user ID from ${oldId} to ${newId}...`);
|
||||
|
||||
try {
|
||||
// 1. Check if user exists
|
||||
const userRes = await pool.query('SELECT * FROM "User" WHERE id = $1', [oldId]);
|
||||
if (userRes.rowCount === 0) {
|
||||
console.log('User almalexyz not found. Checking if Gqbp... already exists.');
|
||||
const checkRes = await pool.query('SELECT * FROM "User" WHERE id = $1', [newId]);
|
||||
if (checkRes.rowCount > 0) {
|
||||
console.log('User Gqbp... already exists. Just updating canvases.');
|
||||
await pool.query('UPDATE "Canvas" SET "ownerId" = $1', [newId]);
|
||||
await pool.query('UPDATE "Group" SET "ownerId" = $1', [newId]);
|
||||
console.log('Done.');
|
||||
return;
|
||||
}
|
||||
console.log('User not found at all.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. We need to handle constraints. The safest way in one transaction.
|
||||
await pool.query('BEGIN');
|
||||
|
||||
// Disable constraints (Postgres specific)
|
||||
await pool.query('SET CONSTRAINTS ALL DEFERRED');
|
||||
|
||||
// Update User ID
|
||||
await pool.query('UPDATE "User" SET id = $1 WHERE id = $2', [newId, oldId]);
|
||||
|
||||
// Update relations manually since schema didn't have ON UPDATE CASCADE
|
||||
await pool.query('UPDATE "Profile" SET "userId" = $1 WHERE "userId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Canvas" SET "ownerId" = $1 WHERE "ownerId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "GroupMember" SET "userId" = $1 WHERE "userId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Activity" SET "userId" = $1 WHERE "userId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Follow" SET "followerId" = $1 WHERE "followerId" = $2', [newId, oldId]);
|
||||
await pool.query('UPDATE "Follow" SET "followingId" = $1 WHERE "followingId" = $2', [newId, oldId]);
|
||||
|
||||
await pool.query('COMMIT');
|
||||
console.log('Successfully migrated user ID and all relations.');
|
||||
|
||||
} catch (err) {
|
||||
await pool.query('ROLLBACK');
|
||||
console.error('Migration failed:', err);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,10 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT id, username, email, role FROM "User"');
|
||||
console.log(JSON.stringify(res.rows, null, 2));
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,53 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
|
||||
const canvasId = 'xflow-project';
|
||||
|
||||
// Define a fresh set of nodes that thoroughly represent the project
|
||||
const nodes = [
|
||||
// 1. Core Architecture
|
||||
{ id: 'arch-1', type: 'text', x: 0, y: 0, width: 400, height: 350, text: '# 🏗️ Глобальная Архитектура\n\n- **Nuxt 3** - Full-stack фреймворк (SSR + API)\n- **Prisma ORM** - Типизированный доступ к данным\n- **PostgreSQL** - Надежное хранилище\n- **Firebase Auth** - Вход через Google\n- **Vue Flow** - Ядро интерактивного холста\n- **TailwindCSS** - Современная стилизация', style: { background: '#1e293b', border: '#3b82f6' } },
|
||||
|
||||
// 2. Database & Prisma
|
||||
{ id: 'prisma-schema', type: 'text', x: -500, y: 100, width: 450, height: 500, text: '# 🗄️ База Данных & Prisma\n\n```prisma\nmodel User {\n id String @id\n email String @unique\n username String @unique\n role String @default(\"user\")\n canvases Canvas[]\n groups GroupMember[]\n}\n\nmodel Canvas {\n id String @id\n name String\n nodes Json // Obsidian format\n edges Json\n owner User @relation(...)\n}\n\nmodel Group {\n id String @id\n members GroupMember[]\n}\n```\n\n**Особенности:**\n- Использование `Json` полей для гибкости холста\n- Каскадное удаление данных\n- Связи многие-ко-многим для групп', style: { background: '#1e293b', border: '#10b981' } },
|
||||
|
||||
// 3. Auth & Security
|
||||
{ id: 'auth-system', type: 'text', x: -500, y: 650, width: 450, height: 350, text: '# 🔐 Безопасность и Auth\n\n1. **Firebase**: Клиентская аутентификация Google.\n2. **Google Token**: Отправляется на `/api/auth/google`.\n3. **Prisma Upsert**: Пользователь создается или обновляется в БД.\n4. **JWT**: Сервер генерирует токен (с ID и ролью).\n5. **Middleware**: Проверка JWT через `requireAuth` в серверных роутах.\n6. **RBAC**: Роли `admin` и `user` контролируют доступ.', style: { background: '#1e293b', border: '#ef4444' } },
|
||||
|
||||
// 4. Git Synchronization
|
||||
{ id: 'git-sync', type: 'text', x: 500, y: 100, width: 450, height: 400, text: '# 🔄 Git Синхронизация (Gitea)\n\n- **useGitSync**: Composable для управления статусом.\n- **Real-Sync API**: Обрабатывает `init` и `sync`.\n- **Gitea Integration**:\n - Автоматическое создание репозиториев.\n - Коммиты при каждом сохранении карты.\n - Хранение `.canvas` файлов в Git для версионности.\n - Токены доступа через `.env` переменные.', style: { background: '#1e293b', border: '#f59e0b' } },
|
||||
|
||||
// 5. Frontend Reactivity
|
||||
{ id: 'state-reactivity', type: 'text', x: 500, y: 550, width: 450, height: 350, text: '# 🧪 Реактивность и Состояние\n\n- **elements**: Массив всех узлов и ребер Vue Flow.\n- **selectedNode**: Реактивный объект для панели свойств.\n- **useUnsavedChanges**: Отслеживание изменений (Dirty state).\n- **commitNodeChange**: Синхронизация данных из панели свойств обратно в массив элементов перед сохранением.', style: { background: '#1e293b', border: '#8b5cf6' } },
|
||||
|
||||
// 6. Features & Updates
|
||||
{ id: 'feat-1', type: 'text', x: 0, y: 400, width: 400, height: 450, text: '# ✨ Ключевые Фичи\n\n- **Кросс-ссылки**: Навигация между картами.\n- **Канбан**: Сводная таблица задач по пользователям.\n- **Типы Узлов**:\n - `Text` (Markdown)\n - `Task` (State machine)\n - `AI Prompt` (LLM ready)\n - `Webhook` (External trigger)\n - `Git Sync` (Version control)\n- **Импорт/Экспорт**: Полная совместимость с Obsidian.', style: { background: '#1e293b', border: '#06b6d4' } },
|
||||
|
||||
// 7. Recent Session
|
||||
{ id: 'session-apr-28', type: 'text', x: 0, y: 900, width: 400, height: 250, text: '# 🔥 Сессия 28 Апреля (Итоги)\n\n- ✅ Починена потеря метаданных в Prisma.\n- ✅ Реализована навигация (двойной клик по ссылкам).\n- ✅ Внедрена система ролей и прав.\n- ✅ Добавлен выпадающий список для связей карт.\n- ✅ Синхронизированы Firebase и БД ID.', style: { background: '#064e3b', border: '#10b981' } }
|
||||
];
|
||||
|
||||
const edges = [
|
||||
{ id: 'e1', source: 'arch-1', target: 'prisma-schema' },
|
||||
{ id: 'e2', source: 'arch-1', target: 'auth-system' },
|
||||
{ id: 'e3', source: 'arch-1', target: 'git-sync' },
|
||||
{ id: 'e4', source: 'arch-1', target: 'state-reactivity' },
|
||||
{ id: 'e5', source: 'arch-1', target: 'feat-1' },
|
||||
{ id: 'e6', source: 'feat-1', target: 'session-apr-28' },
|
||||
{ id: 'e7', source: 'prisma-schema', target: 'auth-system' }
|
||||
];
|
||||
|
||||
await pool.query(
|
||||
`INSERT INTO "Canvas" (id, name, nodes, edges, "ownerId", "isPublic", "createdAt", "updatedAt")
|
||||
VALUES ($1, $2, $3, $4, 'GqbpMyUW0NaxjIy5nvCiXCiQu9t2', true, NOW(), NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET nodes = $3, edges = $4, name = $2, "updatedAt" = NOW()`,
|
||||
[canvasId, '🗺️ Глобальная Архитектура XFlow', JSON.stringify(nodes), JSON.stringify(edges)]
|
||||
);
|
||||
|
||||
console.log('Main project canvas rebuilt with deep architectural details (Prisma, Git, Auth, State)');
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,10 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('UPDATE "User" SET role = \'admin\'');
|
||||
console.log('Updated users to admin:', res.rowCount);
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,28 @@
|
||||
const { Buffer } = require('buffer');
|
||||
|
||||
async function testGitea() {
|
||||
const url = 'http://git.a-crm.controlsystem.kz';
|
||||
const token = '1b3d0d3b5391c67ead8f8dc7380fb7f07434fc64';
|
||||
const owner = 'almalexyz'; // Adjust if needed
|
||||
const repo = 'MM';
|
||||
|
||||
console.log(`Testing Gitea API at ${url}...`);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${url}/api/v1/repos/AVTEX/MMAVTEX`, {
|
||||
headers: { 'Authorization': `token ${token}` }
|
||||
});
|
||||
console.log('Repo check status:', res.status);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
console.log('Repo Name:', data.name);
|
||||
} else {
|
||||
const err = await res.text();
|
||||
console.error('Repo check failed:', err);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Fetch error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
testGitea();
|
||||
@@ -0,0 +1,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,18 @@
|
||||
const { Pool } = require('pg');
|
||||
|
||||
async function run() {
|
||||
const pool = new Pool({ connectionString: 'postgresql://postgres:postgres@localhost:5432/xflow' });
|
||||
const res = await pool.query('SELECT id, username FROM "User"');
|
||||
console.log(JSON.stringify(res.rows, null, 2));
|
||||
|
||||
// Also change owner of all canvases to the first non-system user
|
||||
const realUser = res.rows.find(u => u.username !== 'system');
|
||||
if (realUser) {
|
||||
await pool.query('UPDATE "Canvas" SET "ownerId" = $1', [realUser.id]);
|
||||
console.log('Ownership of all canvases transferred to:', realUser.username);
|
||||
}
|
||||
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
run().catch(console.error);
|
||||
@@ -0,0 +1,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()
|
||||
Reference in New Issue
Block a user