Архітектура CRM Backend

Фаза 22.0–48. Статус: DEPLOYED (живе на VPS, 68+ ендпоінтів, модульна архітектура). Останнє оновлення: 2026-04-28 (Фаза 48 — Architecture Decomposition)

Огляд

Browser (:18888)
  │
  ├── Static files ──► Nginx ──► Docker frontend (React + Nginx)
  │
  ├── /api/crm/* ────► Nginx ──► Bun Master Bot (:19210)
  │                                 ├── crmAuthMiddleware (HMAC-SHA256)
  │                                 └── shared/routes/router.ts → 17 domain modules
  │
  ├── /api/auth/* ───► Nginx ──► Bun (:19210) → master-bot/routes/auth.ts
  ├── /api/cli/*  ───► Nginx ──► Bun (:19210) → master-bot/routes/cli.ts
  ├── /api/internal/* ► Bun (:19210, loopback only) → master-bot/routes/internal.ts
  │
  ├── /api/sse/* ────► Nginx (unbuffered) ──► Bun (:19210)
  │
  └── /ws/terminal/* ► Nginx (upgrade) ──► Bun (:19210) → master-bot/routes/websocket.ts

Структура модулів (Фаза 48)

API Server (master-bot/api-server.ts, 196 рядків) — тонкий диспетчер:

CRM Routes (shared/routes/router.ts, 512 рядків) — таблиця диспетчеризації для 17 доменних модулів:

Потік аутентифікації

User ──► LoginOverlay (frontend)
         │
         ├── Email/password → POST /api/auth/login
         └── OAuth → /api/auth/{google,github} → callback
         │
         ▼
    Successful auth → HMAC-SHA256 token
         │
         ├── payload = { sub: userId, iat, exp: +24h }
         ├── signature = HMAC-SHA256(base64url(payload), CRM_SECRET)
         └── token = base64url(payload) + "." + signature
         │
         ▼
    localStorage('crm-token') → All API calls: Authorization: Bearer <token>
         │
         ▼
    Token expires → 401 → login required

Ключові файли

Файл Роль
shared/auth.ts Генерація/верифікація токенів, email/OAuth, скидання пароля, верифікація email, CORS, middleware
shared/vault.ts AES-256-GCM сховище секретів (CRM_SECRET створюється автоматично)
frontend/src/crm/components/LoginOverlay.jsx UI входу (email, OAuth, скидання пароля, верифікація email)

Властивості безпеки

Маршрути CRM API

Усі маршрути під /api/crm/ вимагають Authorization: Bearer <token>.

GET /api/crm/projects

Список усіх зареєстрованих проєктів з актуальним статусом здоров'я. Включає master як псевдо-проєкт (DEBT-6).

Поле Джерело
name, type, bot_username config/bot_registry.json (master: віртуальний запис, type: "system")
healthy HTTP health check (child-боти: child-порт, master: /api/master/health)
tmux_alive tmux has-session -t <name> (master: citadel-master)
status Heartbeat-файл або обчислюється з health+tmux (master: healthy/degraded/down)

Master завжди ставиться першим записом у масиві відповіді.

GET /api/crm/projects/:name

Повна детальна картка проєкту.

Поле Джерело
manifest Сирий текст з <cwd>/MANIFEST.md
heartbeat JSON з state/heartbeat_<name>.json
skills[] Імена файлів <cwd>/skills/*.md (без _*.md)
healthy, tmux_alive Те саме, що в ендпоінті списку

GET /api/crm/projects/:name/logs

Tail структурованих JSONL-логів.

Параметр За замовчуванням Макс.
lines 100 500
category dialog system, dialog, error

Джерело: /var/log/citadel/<name>/<category>-YYYY-MM-DD.log

Спочатку читає найсвіжіші лог-файли, парсить JSONL, повертає у хронологічному порядку.

GET /api/crm/projects/:name/files

Список вмісту директорії із захистом від path traversal.

Параметр За замовчуванням Опис
path / Відносний шлях у межах cwd проєкту

Безпека: resolve(base, requested) має startsWith(resolve(base)). Порушення → 403.

Записи відповіді: { name, type: "file"|"directory", size?, modified }, відсортовані директоріями вперед.

GET /api/crm/projects/:name/skills

Встановлені скіли з атрибуцією джерела.

Джерело Виявлення
file <cwd>/skills/*.md (без _*.md)
manifest MANIFEST.mdskills[] + library_skills[] (JSON parse)

Дедуплікація: джерело file має пріоритет над manifest.

POST /api/crm/projects/:name/restart (Фаза 22.3)

Рестарт child-бота через watchdog.

Аспект Деталі
Метод Тільки POST (GET повертає 405)
Auth Токен HMAC-SHA256 (як і для інших CRM-маршрутів)
Cooldown 30 секунд на проєкт (повертає 429, якщо занадто часто)
Механізм Викликає restartChild() з watchdog.ts
Відповідь { ok: true } або { error: "..." }

GET /api/crm/projects/:name/metrics (Фаза 22.1)

Часовий ряд метрик якості для sparkline-графіків.

Параметр За замовчуванням Опис
days 7 Кількість днів для агрегування

Джерело: /var/log/citadel/<name>/quality-events.log → лічильники success/failure по днях.

WS /ws/terminal/:name (Фаза 22.3)

WebSocket-термінал, що стрімить вміст tmux-панелі.

Аспект Деталі
Auth Query-параметр ?token= (перевіряється через verifyToken().valid)
Режим За замовчуванням read-only; ?mode=interactive вмикає send-keys
Polling tmux capture-pane -p -e -t {session} -S -80 кожні 200мс
Delta Надсилається тільки при зміні вмісту порівняно з попереднім

Безпека: захист від Path Traversal (Фаза 22.3, DEBT-7)

Усі ендпоінти валідують імена проєктів перед обробкою:

const SAFE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
export function isValidProjectName(name: string): boolean {
  return name.length > 0 && name.length <= 64 && SAFE_NAME_RE.test(name);
}

Застосовується в 3 точках входу: routeCrmRequest(), routeSseRequest(), WebSocket upgrade handler у bot.ts. Невалідні імена → 403 Forbidden.

Цілісність даних: атомарні записи (Фаза 22.3, DEBT-2)

Записи в реєстр у onboarding.ts використовують атомарний патерн:

async function atomicWriteJson(filePath: string, data: unknown): Promise<void> {
  const tmp = `${filePath}.tmp.${process.pid}`;
  await Bun.write(tmp, JSON.stringify(data, null, 2));
  Bun.spawn(["mv", tmp, filePath]); // POSIX atomic rename
}

Ключові файли

Файл Роль Рядки
shared/crm-routes.ts 55+ хендлерів + роутер routeCrmRequest() + isValidProjectName() ~4800
shared/sage.ts Sage Worker: runSageAnalysis(), runBenchmark(), LLM judge, A/B-тестування ~550
shared/db.ts SQLite SSOT: userQueries, projectQueries, skillQueries, benchmarkQueries, chatQueries ~600
shared/auth.ts Генерація і верифікація токенів, CORS, middleware ~100
master-bot/bot.ts Bun.serve: HTTP-роутер + WebSocket-хендлер термінала ~1160
master-bot/onboarding.ts atomicWriteJson() для записів у реєстр ~850

Nginx Proxy

Файл: infra/nginx/citadel-crm.conf — заміняє застарілий конфіг citadel-os.

Три upstreams:

Upstream Порт Сервіс
crm_backend 19210 Bun Master Bot (CRM API, health)
phaser_frontend 18889 Docker Phaser office (legacy)
mcp_bridge 19200 Docker MCP state-bridge (legacy)
Location Backend Auth Спеціальна конфігурація
/ phaser_frontend basic auth Docker Phaser frontend
/api/crm/ crm_backend token (без basic) 30s timeout, keepalive
/api/master/health crm_backend none Публічний health check
/api/sse/ crm_backend token (без basic) proxy_buffering off, 1h timeout (Фаза 22.1)
/ws/ crm_backend token (без basic) WebSocket upgrade, 1h timeout (Фаза 22.1)
/api/ mcp_bridge none (без basic) Legacy state API + SSE
/health mcp_bridge none Legacy bridge health

Усі locations прокидають X-Real-IP, X-Forwarded-For, X-Forwarded-Proto.

Заблоковані шляхи: /.* (dotfiles), /config/, /state/, /scripts/.

Примітка: Пріоритет location'ів у nginx гарантує, що /api/crm/ і /api/sse/ мапляться раніше, ніж catch-all /api/ (legacy MCP).

Інтеграція в Master Bot

master-bot/bot.ts Bun.serve (:19210):

async fetch(req) {
  /api/master/health  →  public, no auth
  /api/crm/*          →  CORS preflight → crmAuthMiddleware → routeCrmRequest()
  *                   →  404
}

Роутер диспетчеризує виклики у функції-хендлери з shared/crm-routes.ts. CORS-заголовки інжектяться у кожну CRM-відповідь (включно з помилками 401).

Зведення ендпоінтів (усі фази)

Фаза Можливість Ендпоінт Статус
22.0 Список проєктів GET /api/crm/projects DONE
22.0 Деталі проєкту GET /api/crm/projects/:name DONE
22.0 Tail логів GET /api/crm/projects/:name/logs DONE
22.0 Список файлів GET /api/crm/projects/:name/files DONE
22.0 Список скілів GET /api/crm/projects/:name/skills DONE
22.1 SSE-стрім логів GET /api/sse/logs/:name DONE
22.1 Історія метрик GET /api/crm/projects/:name/metrics DONE
22.3 Рестарт проєкту POST /api/crm/projects/:name/restart DONE
22.3 WebSocket-термінал WS /ws/terminal/:name DONE
24.5 Specs CRUD GET/POST /api/crm/projects/:name/specs DONE
24.5 Активна роль GET/POST /api/crm/projects/:name/active-role DONE
25 Skills bundle GET /api/crm/projects/:name/skills-bundle DONE
25 Синхронізація learnings GET/POST /api/crm/projects/:name/learnings DONE
26 Workers CRUD GET/POST /api/crm/projects/:name/workers DONE
32 Збереження вікі PUT /api/crm/projects/:name/wiki/save DONE
32 Збереження/видалення скілів PUT/DELETE /api/crm/projects/:name/skills/* DONE
33 Налаштування акаунта GET/PUT /api/crm/account/settings DONE
33 Створення проєкту POST /api/crm/projects/create DONE
34 Issue CRUD POST/GET/PUT /api/mcp/issues/:project DONE
34 Wiki MCP PUT /api/mcp/wiki/:project DONE
34 Синхронізація roadmap GET/PUT /api/mcp/roadmap/:project DONE
34 CLI init GET /api/cli/init/:project/:mode DONE
35 Ingest логів термінала POST /api/crm/projects/:name/terminal/log DONE
36 Cloud PM chat POST /api/crm/projects/:name/chat DONE
36.6 Генерація скілів POST /api/crm/projects/:name/skills/generate DONE
36.7 Список ноутбуків GET /api/crm/projects/:name/notebooks DONE

| 40.14 | Roadmap (контент) | GET /api/mcp/roadmap/:project (+ поле content) | DONE |

Всього: 46+ ендпоінтів (REST + SSE + WebSocket + CLI/MCP)

NotebookLM Bridge (Фаза 36.3)

Окремий Python FastAPI-сервіс на :19213 (тільки localhost). Не є частиною Bun CRM API.

Ендпоінт Метод Призначення
/query POST Семантичний пошук через Google NotebookLM
/sync POST Поставити джерело в чергу для async-синхронізації (fire-and-forget)
/notebooks/init POST Створити ноутбук для нового проєкту
/health GET Статус auth, кількість ноутбуків, статистика черги

Bun CRM тригерить fire-and-forget виклики до Bridge у таких місцях:

Технічний борг

Повний реєстр дивись у docs/backlog/technical-debt.md. Ключові пункти:

ID Назва Статус
DEBT-2 Атомарні JSON-записи RESOLVED (Фаза 22.3)
DEBT-3 Запечений Docker-токен → session auth RESOLVED (Фаза 31)
DEBT-6 Сліпа зона CRM щодо master-бота RESOLVED (Фаза 22.3)
DEBT-7 Захист від path traversal RESOLVED (Фаза 22.3)

Підтримується Rick (Orchestrator). Оновлюється після завершення кожної фази.