chore: repo cleanup + docs for admin API and DB schema (v1.2.8)
- remove stale docs/admin-frontend-spec.md (old Express/EJS admin), unused templates/ (SmartAdmin copy), dead scripts/sync-agents.cjs, committed dev.pid - add docs/API.md (admin REST API reference) and docs/DATABASE.md (DB schema) - refresh .env.example: drop stale ADMIN_PORT/SHOP_CONTAINER, add ADMIN_URL, HEALTH_PORT, DEFAULT_LANGUAGE, CHATBOT_API_* - add npm run lint so Gitea workflows pass - rebrand web-testing suite from APAW to telegram-shop
This commit is contained in:
22
.env.example
22
.env.example
@@ -12,6 +12,7 @@ SUPER_ADMIN_IDS=123456789
|
||||
SUPPORT_LINK=https://t.me/your_support
|
||||
|
||||
# --- Catalog ---
|
||||
# Путь к каталогу (используется ботом; по умолчанию не требуется)
|
||||
CATALOG_PATH=./catalog
|
||||
|
||||
# --- Encryption (ОБЯЗАТЕЛЬНО! Без этого приложение упадёт) ---
|
||||
@@ -46,13 +47,26 @@ WG_ALLOWED_IPS=0.0.0.0/0,::/0
|
||||
# --- Tor Proxy ---
|
||||
# SSH backend: куда Tor перенаправляет SSH (по умолчанию хост-машина)
|
||||
SSH_HOST_IP=host.docker.internal
|
||||
# Имя контейнера магазина (для проброса админки через Tor)
|
||||
SHOP_CONTAINER=telegram_shop_prod
|
||||
# Имя контейнера админки (onion target). Жёстко задано в docker-compose.yml как tg_shop_admin.
|
||||
SHOP_CONTAINER=tg_shop_admin
|
||||
|
||||
# --- Admin Panel ---
|
||||
# --- Admin Panel (Next.js, admin-next/) ---
|
||||
# Секрет входа в админку (токен, вводится на /login)
|
||||
ADMIN_SECRET=your_admin_token_here
|
||||
# SUPER_ADMIN_SECRET: If set to a different value than ADMIN_SECRET, users logging in
|
||||
# with this token get super_admin role (seed phrase access, commission management).
|
||||
# If not set or same as ADMIN_SECRET, all admins are super admins.
|
||||
SUPER_ADMIN_SECRET=
|
||||
ADMIN_PORT=3001
|
||||
# Публичный URL админки (для фото товаров и внешних ссылок)
|
||||
ADMIN_URL=http://your-host:3000
|
||||
# Порт health-сервера бота (Docker healthcheck)
|
||||
HEALTH_PORT=3001
|
||||
|
||||
# --- Bot i18n ---
|
||||
# Язык по умолчанию: en / es / de
|
||||
DEFAULT_LANGUAGE=en
|
||||
|
||||
# --- AI Chatbot (admin-next) ---
|
||||
# OpenAI-совместимый endpoint и ключ (используются, если не заданы в site_settings)
|
||||
CHATBOT_API_ENDPOINT=https://api.openai.com/v1
|
||||
CHATBOT_API_KEY=
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -21,11 +21,8 @@ db/*.pre-merge-*
|
||||
# Root-level throwaway scripts
|
||||
/*.mjs
|
||||
|
||||
/*.mjs
|
||||
|
||||
# Production backups (contain secrets + DB snapshots — never commit)
|
||||
production-backup/
|
||||
screenshot-dash.cjs
|
||||
|
||||
# Kilo agent configuration (managed locally, not committed)
|
||||
.kilo/
|
||||
@@ -33,3 +30,8 @@ kilo-meta.json
|
||||
kilo.jsonc
|
||||
AGENTS.md
|
||||
admin-next/.env
|
||||
|
||||
# Admin dev artifacts
|
||||
admin-next/.zscripts/*.pid
|
||||
admin-next/.next/
|
||||
admin-next/node_modules/
|
||||
|
||||
77
README.md
77
README.md
@@ -70,8 +70,11 @@ curl -o /dev/null -w "%{http_code}\n" http://localhost:3000/login # новая
|
||||
| `SUPER_ADMIN_IDS` | — | ID супер-админов |
|
||||
| `SUPPORT_LINK` | — | Ссылка на поддержку |
|
||||
| `DEFAULT_LANGUAGE` | — | Язык по умолчанию (`en`, `es`, `de`; по умолчанию `en`) |
|
||||
| `HEALTH_PORT` | — | Порт health-сервера бота (по умолчанию 3001) |
|
||||
| `SSH_HOST_IP` | — | Куда Tor перенаправляет SSH (по умолчанию host.docker.internal) |
|
||||
| `SHOP_CONTAINER` | — | Имя контейнера магазина (по умолчанию telegram_shop_prod) |
|
||||
| `SHOP_CONTAINER` | — | Имя контейнера админки (onion target; по умолчанию tg_shop_admin) |
|
||||
| `CHATBOT_API_ENDPOINT` | — | OpenAI-совместимый endpoint чатбота (fallback) |
|
||||
| `CHATBOT_API_KEY` | — | API-ключ чатбота (fallback) |
|
||||
| `WG_ENABLED` | — | `true` / `false` (по умолчанию `false`) |
|
||||
| `WG_PRIVATE_KEY` | — | Приватный ключ WireGuard |
|
||||
| `WG_PUBLIC_KEY` | — | Публичный ключ WireGuard |
|
||||
@@ -287,37 +290,45 @@ src/i18n/
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
├── src/
|
||||
│ ├── config/ # Конфигурация (БД, крипто)
|
||||
│ ├── context/ # Контекст и состояния бота
|
||||
│ ├── handlers/ # Обработчики команд
|
||||
├── src/ # Telegram-бот (Node.js)
|
||||
│ ├── config/ # Конфигурация (БД, крипто)
|
||||
│ ├── context/ # Контекст и состояния бота
|
||||
│ ├── handlers/ # Обработчики команд
|
||||
│ │ ├── adminHandlers/ # Обработчики админа (Telegram)
|
||||
│ │ └── userHandlers/ # Обработчики пользователя
|
||||
│ ├── i18n/ # Интернационализация
|
||||
│ │ ├── index.js # tForUser(), tForLang(), LANGUAGE_NAMES
|
||||
│ │ └── locales/ # en.json, es.json, de.json (201 ключ)
|
||||
│ ├── middleware/ # Промежуточные обработчики
|
||||
│ ├── migrations/ # Миграции БД
|
||||
│ ├── models/ # Модели данных
|
||||
│ ├── router/ # Роутинг callback/text бота
|
||||
│ ├── services/ # Бизнес-логика (вкл. chatbotService, leadService)
|
||||
│ ├── utils/ # Утилиты (логирование, валидация, ошибки)
|
||||
│ ├── healthServer.js # Минимальный HTTP /health (Docker healthcheck)
|
||||
│ └── index.js # Точка входа
|
||||
├── admin-next/ # НОВАЯ админ-панель (Next.js 16 + Prisma + shadcn/ui)
|
||||
│ │ └── userHandlers/ # Обработчики пользователя
|
||||
│ ├── i18n/ # Интернационализация
|
||||
│ │ ├── index.js # tForUser(), tForLang(), LANGUAGE_NAMES
|
||||
│ │ └── locales/ # en.json, es.json, de.json
|
||||
│ ├── middleware/ # Промежуточные обработчики
|
||||
│ ├── migrations/ # Миграции БД (001–014)
|
||||
│ ├── models/ # Модели данных
|
||||
│ ├── router/ # Роутинг callback/text бота
|
||||
│ ├── services/ # Бизнес-логика (вкл. chatbotService, leadService)
|
||||
│ ├── utils/ # Утилиты (логирование, валидация, ошибки)
|
||||
│ ├── __tests__/ # Юнит-тесты (vitest)
|
||||
│ ├── healthServer.js # Минимальный HTTP /health (Docker healthcheck)
|
||||
│ └── index.js # Точка входа
|
||||
├── admin-next/ # Админ-панель (Next.js 16 + Prisma + shadcn/ui)
|
||||
│ ├── prisma/schema.prisma # Схема БД (та же SQLite, что у бота)
|
||||
│ ├── src/app/api/ # 45 API-роутов (каталог, заказы, лиды, чатбот)
|
||||
│ ├── src/components/ # 76 UI-компонентов (dashboard, wallets, leads...)
|
||||
│ ├── src/app/api/ # 45 API-роутов (см. docs/API.md)
|
||||
│ ├── src/components/ # UI-компоненты (dashboard, wallets, leads...)
|
||||
│ └── Dockerfile # Multi-stage Next.js standalone
|
||||
├── tor-proxy/ # Tor прокси для SSH и админки
|
||||
│ ├── Dockerfile # Alpine + Tor образ
|
||||
│ ├── entrypoint.sh # Генерация torrc, валидация env vars
|
||||
│ ├── get-onions.sh # Скрипт обновления .env с onion-адресами
|
||||
│ └── hosts/ # Директория для onion-hosts.txt
|
||||
├── wg/ # WireGuard конфигурация
|
||||
│ └── start.sh # Скрипт запуска контейнера
|
||||
├── db/ # SQLite база данных (volume)
|
||||
├── uploads/ # Загруженные фото (volume)
|
||||
├── docs/ # Документация
|
||||
│ ├── API.md # Справочник REST API админки
|
||||
│ └── DATABASE.md # Структура БД (схема, связи, примечания)
|
||||
├── tor-proxy/ # Tor прокси для SSH и админки
|
||||
│ ├── Dockerfile # Alpine + Tor образ
|
||||
│ ├── entrypoint.sh # Генерация torrc, валидация env vars
|
||||
│ ├── get-onions.sh # Скрипт обновления .env с onion-адресами
|
||||
│ └── hosts/ # Директория для onion-hosts.txt
|
||||
├── wg/ # WireGuard конфигурация
|
||||
│ └── start.sh # Скрипт запуска контейнера
|
||||
├── tests/ # Web-тестирование (visual regression, E2E)
|
||||
│ ├── scripts/ # Скрипты тестов (Playwright, pixelmatch)
|
||||
│ └── visual/ # baseline/current/diff скриншоты
|
||||
├── docker/ # Дополнительные compose-конфиги (web-testing)
|
||||
├── db/ # SQLite база данных (volume)
|
||||
├── uploads/ # Загруженные фото (volume)
|
||||
├── Dockerfile # Multi-stage сборка магазина
|
||||
├── docker-compose.yml # Конфигурация трёх контейнеров (бот, админка, tor)
|
||||
├── install.sh # Установщик (POSIX sh)
|
||||
@@ -326,6 +337,14 @@ src/i18n/
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Документация
|
||||
|
||||
| Документ | Содержание |
|
||||
|---|---|
|
||||
| [`docs/API.md`](docs/API.md) | Справочник REST API админ-панели: все эндпоинты, методы, query-параметры, коды ошибок |
|
||||
| [`docs/DATABASE.md`](docs/DATABASE.md) | Структура БД: таблицы, колонки, связи, примечания |
|
||||
| [`VERSION.md`](VERSION.md) | История версий и changelog |
|
||||
|
||||
## Разработка
|
||||
|
||||
```bash
|
||||
|
||||
@@ -8,10 +8,17 @@
|
||||
|
||||
## Current Version
|
||||
|
||||
**v1.2.7** — 2026-08-08
|
||||
**v1.2.8** — 2026-08-09
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.2.8 — 2026-08-09
|
||||
- **chore**: repo cleanup — removed stale `docs/admin-frontend-spec.md` (old Express/EJS admin), unused `templates/` (SmartAdmin copy), dead `scripts/sync-agents.cjs`, committed `admin-next/.zscripts/dev.pid`
|
||||
- **docs**: added `docs/API.md` (admin REST API reference) and `docs/DATABASE.md` (DB schema); README structure updated
|
||||
- **env**: `.env.example` refreshed — removed stale `ADMIN_PORT`/`SHOP_CONTAINER` defaults, added `ADMIN_URL`, `HEALTH_PORT`, `DEFAULT_LANGUAGE`, `CHATBOT_API_ENDPOINT`, `CHATBOT_API_KEY`
|
||||
- **ci**: added `npm run lint` script (syntax check of `src/`) so Gitea workflows no longer fail
|
||||
- **chore**: rebranded web-testing suite from APAW to telegram-shop (package name, container names)
|
||||
|
||||
### v1.2.7 — 2026-08-08
|
||||
- **refactor**: Remove old Express/EJS admin panel (replaced by Next.js admin in admin-next/); bot now has standalone health server; SUPER_ADMIN_SECRET for admin auth
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
1119
|
||||
@@ -18,7 +18,7 @@ services:
|
||||
# ─── Screenshot Capture: Create Baselines ─────────────────────────
|
||||
screenshot-baseline:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-noble
|
||||
container_name: apaw-screenshot-baseline
|
||||
container_name: tgshop-screenshot-baseline
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../tests:/app/tests
|
||||
@@ -38,7 +38,7 @@ services:
|
||||
# ─── Screenshot Capture: Create Current ──────────────────────────
|
||||
screenshot-current:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-noble
|
||||
container_name: apaw-screenshot-current
|
||||
container_name: tgshop-screenshot-current
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../tests:/app/tests
|
||||
@@ -58,7 +58,7 @@ services:
|
||||
# ─── Visual Regression: Compare Screenshots ──────────────────────
|
||||
visual-compare:
|
||||
image: node:24-alpine
|
||||
container_name: apaw-visual-compare
|
||||
container_name: tgshop-visual-compare
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../tests:/app/tests
|
||||
@@ -76,7 +76,7 @@ services:
|
||||
# Captures current screenshots and compares against baselines
|
||||
visual-tester:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-noble
|
||||
container_name: apaw-visual-tester
|
||||
container_name: tgshop-visual-tester
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../tests:/app/tests
|
||||
@@ -106,7 +106,7 @@ services:
|
||||
# ─── Console Error Monitor ──────────────────────────────────────
|
||||
console-monitor:
|
||||
image: mcr.microsoft.com/playwright:v1.52.0-noble
|
||||
container_name: apaw-console-monitor
|
||||
container_name: tgshop-console-monitor
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../tests:/app/tests
|
||||
@@ -131,7 +131,7 @@ services:
|
||||
# Uses @mizchi/vlmkit for AI-powered visual regression testing
|
||||
vlmkit:
|
||||
image: node:24-alpine
|
||||
container_name: apaw-vlmkit
|
||||
container_name: tgshop-vlmkit
|
||||
working_dir: /app
|
||||
volumes:
|
||||
- ../tests:/app/tests
|
||||
|
||||
207
docs/API.md
Normal file
207
docs/API.md
Normal file
@@ -0,0 +1,207 @@
|
||||
# Telegram Shop — Admin API Reference
|
||||
|
||||
Актуальная документация по REST API админ-панели (`admin-next/`, Next.js 16 + Prisma).
|
||||
|
||||
- **Базовый URL**: `http://<host>:3000`
|
||||
- **Формат**: JSON (`Content-Type: application/json`)
|
||||
- **Аутентификация**: cookie `admin_token` (httpOnly, 24ч), устанавливается через `POST /api/auth/login`
|
||||
|
||||
## Аутентификация
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| POST | `/api/auth/login` | Вход по токену. Body: `{ "token": "..." }`. Rate-limit: 5 попыток / 15 мин на IP (429). Устанавливает cookie `admin_token` |
|
||||
| GET | `/api/auth/session` | Проверка сессии. Ответ: `{ "role": "admin" \| "super_admin" }` (401 если нет/просрочен) |
|
||||
| POST | `/api/auth/logout` | Выход, удаляет cookie |
|
||||
|
||||
### Роли
|
||||
|
||||
- `admin` — обычный администратор
|
||||
- `super_admin` — доступ к seed-фразам, экспорту, импорту, очистке БД
|
||||
|
||||
Роль определяется при входе: если `SUPER_ADMIN_SECRET` не задан или равен `ADMIN_SECRET` — все админы получают `super_admin`. Иначе роль `super_admin` даёт только вход по `SUPER_ADMIN_SECRET`.
|
||||
|
||||
**Все эндпоинты ниже требуют cookie `admin_token`** (иначе 401). Эндпоинты с пометкой 🔒 требуют роль `super_admin` (иначе 403).
|
||||
|
||||
## Дашборд и статистика
|
||||
|
||||
### GET `/api/stats/dashboard`
|
||||
Сводная статистика для дашборда:
|
||||
- counts: `totalUsers`, `totalProducts`, `totalPurchases`, `totalSubcategories`, `bannedUsers`, `activeWallets`
|
||||
- статусы покупок: `completedPurchases`, `pendingPurchases`, `cancelledPurchases`
|
||||
- метрики: `totalRevenue` (сумма completed), `aov`, `conversionRate`
|
||||
- временные ряды: `revenueByDay`, `purchasesByDay`, `newUsersByDay` (последние 30 дней)
|
||||
- `topProducts`, `recentPurchases`, `recentUsers`
|
||||
|
||||
## Каталог
|
||||
|
||||
### GET `/api/catalog/tree`
|
||||
Полное дерево каталога: локации, категории, подкатегории с `_count` (вложенные сущности).
|
||||
|
||||
### Локации
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/locations/bulk` | Список локаций (с `_count` категорий/товаров) |
|
||||
| POST | `/api/locations/bulk` | Создать. Body: `{ country, city, district }` |
|
||||
| PUT | `/api/locations/[id]` | Обновить. Body: `{ country?, city?, district? }` |
|
||||
| PATCH | `/api/locations/[id]` | Переключить `is_active` (0/1) |
|
||||
| DELETE | `/api/locations/[id]` | Удалить (409 если есть связанные сущности) |
|
||||
|
||||
### Категории
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/categories/bulk` | Список категорий (с локацией и `_count`) |
|
||||
| POST | `/api/categories/bulk` | Создать. Body: `{ name, locationId }` |
|
||||
| PUT | `/api/categories/[id]` | Обновить. Body: `{ name?, locationId? }` (409 при дубликате в локации) |
|
||||
| PATCH | `/api/categories/[id]` | Переключить `is_active` |
|
||||
| DELETE | `/api/categories/[id]` | Удалить (409 если есть связанные сущности) |
|
||||
|
||||
### Подкатегории
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/subcategories/bulk` | Список подкатегорий |
|
||||
| POST | `/api/subcategories/bulk` | Создать. Body: `{ name, categoryId }` |
|
||||
| PUT | `/api/subcategories/[id]` | Обновить. Body: `{ name?, categoryId? }` |
|
||||
| PATCH | `/api/subcategories/[id]` | Переключить `is_active` |
|
||||
| DELETE | `/api/subcategories/[id]` | Удалить |
|
||||
|
||||
### Товары
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/products/bulk` | Список. Query: `loc`, `cat`, `sub`, `search`, `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
|
||||
| POST | `/api/products/add` | Создать. Body: `{ locationId, categoryId, subcategoryId?, name, description?, privateData?, price, quantityInStock?, photoUrl?, hiddenPhotoUrl?, hiddenCoordinates?, hiddenDescription?, isMono? }`. Обязательны: `locationId, categoryId, name, price`. `isMono=1` → `quantityInStock=999999` |
|
||||
| GET | `/api/products/[id]` | Детали товара (с category, subcategory, location) |
|
||||
| PUT | `/api/products/[id]` | Обновить (частично) |
|
||||
| DELETE | `/api/products/[id]` | Удалить |
|
||||
| POST | `/api/products/[id]/clone` | Клонировать (имя получает суффикс `(Copy)`) |
|
||||
|
||||
## Пользователи
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/users/bulk` | Список. Query: `search` (username/telegramId), `status` (0/2), `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
|
||||
| GET | `/api/users/[id]` | Детали: пользователь + кошельки + последние 20 покупок + связанный лид |
|
||||
| POST | `/api/users/[id]` | Обновить (username, country, city, district, notes и т.д.) |
|
||||
| PATCH | `/api/users/[id]` | Переключить `status` (0=active, 2=blocked) |
|
||||
| POST | `/api/users/[id]/adjust-balance` | Изменить баланс. Body: `{ amount: number, currency: "total_balance" \| "bonus_balance" }`. Пишет audit-запись |
|
||||
| POST | `/api/users/batch-status` | Массово сменить статус. Body: `{ userIds: number[], newStatus: 0 \| 2 }` |
|
||||
|
||||
## Кошельки
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/wallets/bulk` | Пользователи, у которых есть кошельки. Query: `search`. Ответ: `{ users, totalWallets, totalBalance }` |
|
||||
| GET | `/api/wallets/[userId]` | Кошельки конкретного пользователя |
|
||||
| GET | `/api/wallets/overview` | Сводка: суммы и количество по типам (BTC/LTC/ETH/USDT/USDC), последние 20 комиссионных выплат |
|
||||
| GET | `/api/wallets/seeds` | 🔒 Seed-фразы всех кошельков (пишет audit `seed_phrase_viewed`) |
|
||||
| GET | `/api/wallets/export-seeds` | 🔒 Экспорт seed-фраз в CSV (Content-Disposition: attachment) |
|
||||
| POST | `/api/wallets/record-payment` | Записать комиссионную выплату. Body: `{ paidAmount: number, note? }` |
|
||||
|
||||
## Покупки
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/purchases/bulk` | Список. Query: `status` (pending/completed/cancelled), `from`, `to` (YYYY-MM-DD), `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
|
||||
| PATCH | `/api/purchases/[id]` | Сменить статус. Body: `{ status: "completed" \| "cancelled" }`. Только для `pending` |
|
||||
| POST | `/api/purchases/batch-status` | Массово. Body: `{ purchaseIds: number[], status: "completed" \| "cancelled" }` (обновляет только pending) |
|
||||
|
||||
## Транзакции
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/transactions/bulk` | Список. Query: `userId`, `page`, `limit` (≤100, по умолчанию 20). Ответ: `{ data, total, page, limit }` |
|
||||
|
||||
## Лиды и чат
|
||||
|
||||
### Лиды
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/leads/bulk` | Список. Query: `search` (name/phone/email/telegram/telegramId), `status`, `page`, `limit` (≤100). Ответ: `{ data, total, page, limit }` |
|
||||
| GET | `/api/leads/[id]` | Детали лида + связанный пользователь (баланс, покупки, кошельки) |
|
||||
| PUT | `/api/leads/[id]` | Обновить (name, phone, email, telegram, status, notes, customFields и т.д.) |
|
||||
| GET | `/api/leads/[id]/activity` | Активность: `{ hourly: number[24], yearly: { "YYYY-MM-DD": count }, total }` (по audit_log) |
|
||||
| GET | `/api/leads/[id]/sessions` | Чат-сессии лида с распарсенными сообщениями |
|
||||
|
||||
### Чат (публичный виджет)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| POST | `/api/chat` | Отправить сообщение в чат-виджет. Body: `{ sessionId?, message, language?, device?, ip?, country? }`. Создаёт/обновляет `ChatSession`, извлекает данные лида (имя/телефон/email/telegram), вызывает ИИ-провайдера, при `chatbot_sleep_mode` отвечает sleep-сообщением |
|
||||
|
||||
### Оператор
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| POST | `/api/operator` | Подключение/отключение оператора. Body: `{ sessionId, action: "connect" \| "disconnect", operatorName }`. Пишет audit `operator_connect` |
|
||||
|
||||
## Чатбот (настройки)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/admin/chatbot` | Настройки чатбота из `site_settings` (ключи `chatbot_*`), API-ключ маскируется |
|
||||
| PUT | `/api/admin/chatbot` | Обновить настройки. Валидация: `chatbot_temperature` 0–2, `chatbot_max_tokens` 50–4000, `chatbot_max_history` 1–50 |
|
||||
| GET | `/api/chatbot/models?endpoint=&apiKey=` | Список моделей провайдера через OpenAI-совместимый `/models`. Без query — берёт сохранённые настройки. Поддерживает OpenAI (`data[]`) и Ollama (`models[]`) |
|
||||
|
||||
Ключи настроек чатбота (defaults):
|
||||
`chatbot_enabled`, `chatbot_sleep_mode`, `chatbot_sleep_message`, `chatbot_system_prompt`, `chatbot_temperature` (0.7), `chatbot_max_tokens` (1024), `chatbot_max_history` (20), `chatbot_knowledge_base`, `chatbot_provider` (ollama), `chatbot_api_endpoint`, `chatbot_api_key`, `chatbot_model`.
|
||||
|
||||
## Аудит
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/audit/bulk` | Журнал аудита. Query: `page`, `limit` (≤200, по умолчанию 100), `userId`, `from`, `to`, `search`, `action`. Ответ: `{ data, total, page, limit }` |
|
||||
|
||||
## Локализация (i18n)
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/locales` | Все переводы: `{ en: {...}, es: {...}, de: {...} }` |
|
||||
| PUT | `/api/locales` | Обновить ключ. Body: `{ lang, key, value }` |
|
||||
|
||||
## Настройки
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/settings` | Список настроек (секреты маскированы, список в `_masked`) |
|
||||
| PUT | `/api/settings` | Обновить. Body: `{ key, value }`. Ответ: «Settings saved. Restart required.» |
|
||||
| GET | `/api/settings/export` | Полный экспорт БД в JSON (users, wallets, purchases, categories, subcategories, locations, products, auditLogs, commissionPayments, userStates) |
|
||||
| POST | `/api/settings/import` | 🔒 Импорт (заглушка: «Import not yet implemented») |
|
||||
|
||||
## Seed / Demo
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api/seed/data` | Проверка: `{ seeded: boolean }` (есть ли пользователи) |
|
||||
| POST | `/api/seed/demo` | Заполнить демо-данными. Body: `{ reauthToken }` (повторный ввод секрета) |
|
||||
| POST | `/api/seed/clear` | 🔒 Очистить все данные. Body: `{ reauthToken }`. Пишет audit `clear_all` |
|
||||
|
||||
## Корневой
|
||||
|
||||
| Метод | Путь | Описание |
|
||||
|---|---|---|
|
||||
| GET | `/api` | Служебный ответ (health API) |
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
| Код | Значение |
|
||||
|---|---|
|
||||
| 400 | Невалидный запрос / отсутствуют обязательные поля |
|
||||
| 401 | Нет cookie `admin_token` или токен невалиден/просрочен |
|
||||
| 403 | Недостаточно прав (нужен `super_admin`) |
|
||||
| 404 | Сущность не найдена |
|
||||
| 409 | Конфликт (дубликат, есть связанные записи) |
|
||||
| 429 | Слишком много попыток входа |
|
||||
| 500 | Внутренняя ошибка |
|
||||
|
||||
## Соглашения
|
||||
|
||||
- Пагинация: `page` (с 1), `limit` (кап зависит от эндпоинта: 50/100/200)
|
||||
- Даты: `from`/`to` в формате `YYYY-MM-DD` (to инклюзивно до конца дня)
|
||||
- `is_active`/`status` пользователей: `0` = активно, `2` = заблокировано
|
||||
- Статусы покупок: `pending` / `completed` / `cancelled`
|
||||
- Статусы лидов: `new` / `contacted` / `qualified` / `lost` / `spam`
|
||||
220
docs/DATABASE.md
Normal file
220
docs/DATABASE.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Telegram Shop — Database Schema
|
||||
|
||||
Единая SQLite-база (`db/shop.db`), используемая **одновременно** ботом (`src/`, better-sqlite3) и админ-панелью (`admin-next/`, Prisma). Источник истины схемы: `admin-next/prisma/schema.prisma`.
|
||||
|
||||
Миграции бота: `src/migrations/` (001–014, runner: `src/migrations/runner.js`).
|
||||
|
||||
## Схема
|
||||
|
||||
### users — пользователи Telegram
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| telegram_id | TEXT UNIQUE | ID в Telegram |
|
||||
| username | TEXT? | |
|
||||
| country / city / district | TEXT? | География |
|
||||
| status | INTEGER (0) | 0=active, 2=blocked |
|
||||
| total_balance | REAL (0) | Основной баланс |
|
||||
| bonus_balance | REAL (0) | Бонусный баланс |
|
||||
| language | TEXT ('en') | en / es / de |
|
||||
| language_set | INTEGER (0) | Флаг выбора языка |
|
||||
| notes | TEXT? | Заметки админа (миграция 014) |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
Связи: `wallets`, `transactions`, `purchases`.
|
||||
|
||||
### crypto_wallets — криптокошельки
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| user_id | INTEGER FK → users | |
|
||||
| wallet_type | TEXT | BTC / LTC / ETH / USDT / USDC |
|
||||
| address | TEXT | |
|
||||
| derivation_path | TEXT? | |
|
||||
| mnemonic | TEXT? | Seed-фраза (зашифрована) |
|
||||
| balance | REAL (0) | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
Уникальность: `(user_id, wallet_type)` — один кошелёк каждого типа на пользователя.
|
||||
|
||||
### transactions — транзакции
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| user_id | INTEGER FK → users | |
|
||||
| wallet_type | TEXT | |
|
||||
| tx_hash | TEXT? | |
|
||||
| amount | REAL | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
### locations — локации
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| country | TEXT | |
|
||||
| city | TEXT | |
|
||||
| district | TEXT ('') | |
|
||||
| is_active | INTEGER (1) | 0/1 |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
Уникальность: `(country, city, district)`.
|
||||
|
||||
### categories — категории
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| location_id | INTEGER FK → locations | |
|
||||
| name | TEXT | |
|
||||
| is_active | INTEGER (1) | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
Уникальность: `(location_id, name)`.
|
||||
|
||||
### subcategories — подкатегории
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| category_id | INTEGER FK → categories | |
|
||||
| name | TEXT | |
|
||||
| is_active | INTEGER (1) | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
Уникальность: `(category_id, name)`.
|
||||
|
||||
### products — товары
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| location_id | INTEGER FK → locations | |
|
||||
| category_id | INTEGER FK → categories | |
|
||||
| subcategory_id | INTEGER? FK → subcategories | |
|
||||
| name | TEXT | |
|
||||
| description | TEXT? | |
|
||||
| private_data | TEXT? | Скрытый контент (выдаётся после покупки) |
|
||||
| price | REAL | |
|
||||
| quantity_in_stock | INTEGER (0) | Для mono-товаров = 999999 |
|
||||
| photo_url | TEXT? | Публичное фото |
|
||||
| hidden_photo_url | TEXT? | Скрытое фото |
|
||||
| hidden_coordinates | TEXT? | Скрытые координаты |
|
||||
| hidden_description | TEXT? | Скрытое описание |
|
||||
| is_mono | INTEGER (0) | 1 = неограниченный товар |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
### purchases — покупки
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| user_id | INTEGER FK → users | |
|
||||
| product_id | INTEGER FK → products | |
|
||||
| wallet_type | TEXT? | |
|
||||
| tx_hash | TEXT? | |
|
||||
| quantity | INTEGER | |
|
||||
| total_price | REAL | |
|
||||
| purchase_date | DATETIME | |
|
||||
| status | TEXT ('pending') | pending / completed / cancelled |
|
||||
|
||||
### commission_payments — комиссионные выплаты
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| total_balance_usd | REAL | |
|
||||
| commission_rate | REAL | |
|
||||
| commission_amount_usd | REAL | |
|
||||
| paid_amount_usd | REAL | |
|
||||
| wallet_count | INTEGER | |
|
||||
| note | TEXT? | |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
### audit_log — журнал аудита
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| action | TEXT | `balance_adjust`, `operator_connect`, `seed_phrase_viewed`, `clear_all` и др. |
|
||||
| admin_id | TEXT | Роль или telegram_id |
|
||||
| details | TEXT? | JSON-строка с контекстом |
|
||||
| created_at | DATETIME | |
|
||||
|
||||
### chat_sessions — чат-сессии (чатбот)
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| session_id | TEXT UNIQUE | |
|
||||
| telegram_id | TEXT? | |
|
||||
| lead_id | INTEGER? FK → leads | |
|
||||
| messages | TEXT | JSON-массив `{role, content, timestamp}` |
|
||||
| language | TEXT ('en') | |
|
||||
| device / ip / country | TEXT? | |
|
||||
| customer_profile | TEXT? | AI-профиль клиента (JSON) |
|
||||
| is_active | BOOLEAN (true) | |
|
||||
| operator_name | TEXT? | |
|
||||
| auto_reply_disabled | BOOLEAN (false) | |
|
||||
| operator_connected_at | DATETIME? | |
|
||||
| created_at / updated_at | DATETIME | |
|
||||
|
||||
### leads — лиды
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| telegram_id | TEXT? UNIQUE | |
|
||||
| name / phone / email / telegram | TEXT? | |
|
||||
| status | TEXT ('new') | new / contacted / qualified / lost / spam |
|
||||
| verification | TEXT ('pending') | |
|
||||
| notes | TEXT? | |
|
||||
| custom_fields | TEXT ('{}') | JSON |
|
||||
| geo_address | TEXT? | |
|
||||
| ai_lead_score | REAL? | |
|
||||
| created_at / updated_at | DATETIME | |
|
||||
|
||||
Связь: `chatSessions`.
|
||||
|
||||
### site_settings — настройки (ключ-значение)
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| id | INTEGER PK | |
|
||||
| key | TEXT UNIQUE | Например `chatbot_*` |
|
||||
| value | TEXT | |
|
||||
| created_at / updated_at | DATETIME | |
|
||||
|
||||
### user_states — состояния пользователей бота
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| chat_id | TEXT PK | |
|
||||
| state_data | TEXT? | JSON состояния |
|
||||
| updated_at | INTEGER | Unix-время |
|
||||
|
||||
## Связи (ER-сводка)
|
||||
|
||||
```
|
||||
users 1──N crypto_wallets
|
||||
users 1──N transactions
|
||||
users 1──N purchases
|
||||
locations 1──N categories 1──N subcategories
|
||||
locations 1──N products
|
||||
categories 1──N products
|
||||
subcategories 1──N products
|
||||
products 1──N purchases
|
||||
leads 1──N chat_sessions
|
||||
leads 1──1 users (по telegram_id, не FK)
|
||||
```
|
||||
|
||||
## Примечания
|
||||
|
||||
- **Бот и админка работают с одной БД**: любые изменения мгновенно видны обеим сторонам
|
||||
- `users` и `leads` — **отдельные таблицы**, связываются по `telegram_id` (не внешний ключ)
|
||||
- `mnemonic` хранится зашифрованным (ключ `ENCRYPTION_KEY`)
|
||||
- Каскадное удаление: wallets/transactions/purchases удаляются вместе с пользователем; products — вместе с location/category
|
||||
- Миграции бота нумеруются `NNN_*.js`; Prisma-схема админки должна отражать ту же структуру
|
||||
@@ -1,750 +0,0 @@
|
||||
# Админ-панель Telegram Shop — Техническое описание фронтенда и API
|
||||
|
||||
> **Назначение документа**: комплексное ТЗ для отдела фронтенда по реализации полноценного админ-кабинета поверх существующего бэкенда.
|
||||
> **Версия бэкенда**: v1.2.4 (2026-08-05)
|
||||
> **Дата**: 2026-08-05
|
||||
> **Статус**: актуально на момент передачи
|
||||
|
||||
---
|
||||
|
||||
## Оглавление
|
||||
|
||||
1. [Обзор системы](#1-обзор-системы)
|
||||
2. [Технологический стек](#2-технологический-стек)
|
||||
3. [Архитектура развёртывания](#3-архитектура-развёртывания)
|
||||
4. [Доступ и безопасность](#4-доступ-и-безопасность)
|
||||
5. [Схема базы данных](#5-схема-базы-данных)
|
||||
6. [Карта эндпоинтов (полная)](#6-карта-эндпоинтов-полная)
|
||||
7. [Спецификация экранов](#7-спецификация-экранов)
|
||||
8. [Роли и права](#8-роли-и-права)
|
||||
9. [Формат данных](#9-формат-данных)
|
||||
10. [Существующие ограничения и подводные камни](#10-существующие-ограничения-и-подводные-камни)
|
||||
11. [Требования к новому фронтенду](#11-требования-к-новому-фронтенду)
|
||||
12. [API-контракты JSON (дополнить бэкенду)](#12-api-контракты-json-дополнить-бэкенду)
|
||||
13. [Чек-лист приёмки](#13-чек-лист-приёмки)
|
||||
|
||||
---
|
||||
|
||||
## 1. Обзор системы
|
||||
|
||||
Telegram Shop — это **Telegram-бот-магазин** (Node.js) с **встроенной админ-панелью** на Express + EJS (шаблон SmartAdmin). Продажи идут через Telegram-бота, админка управляет всем магазином через браузер.
|
||||
|
||||
Бизнес-сущности:
|
||||
|
||||
| Сущность | Описание |
|
||||
|---|---|
|
||||
| **Users** | Покупатели из Telegram (id, ник, локация, статус, балансы, язык) |
|
||||
| **Locations** | Гео-иерархия: Страна → Город → Район (округа) |
|
||||
| **Categories / Subcategories** | Категории товаров, привязанные к локации; подкатегории внутри категории |
|
||||
| **Products** | Товары: цена, остаток, публичное фото, скрытый контент (фото/координаты/описание), private-заметки, цифровые (mono) |
|
||||
| **Purchases** | Заказы: пользователь, товар, валюта, tx_hash, количество, сумма, статус |
|
||||
| **Crypto Wallets** | Криптокошельки пользователей (BTC/LTC/ETH/USDT/USDC), с зашифрованными seed-фразами |
|
||||
| **Transactions** | Ончейн-транзакции пользователей |
|
||||
| **Commission payments** | Платежи комиссии владельцу платформы (SaaS-модель) |
|
||||
| **Audit log** | Журнал действий админов |
|
||||
| **Locales (i18n)** | JSON-файлы переводов бота (en/es/de), редактируются из админки |
|
||||
|
||||
**Ключевой факт**: бот и админка живут в **одном процессе** и используют **одну SQLite-базу** (`db/shop.db`) — поэтому любые изменения в админке мгновенно видны боту и наоборот. Отдельного REST API **нет** — админка рендерит EJS на сервере и ходит в БД напрямую.
|
||||
|
||||
---
|
||||
|
||||
## 2. Технологический стек
|
||||
|
||||
### Текущий (серверная часть — НЕ трогать без отдельной задачи)
|
||||
|
||||
| Слой | Технология |
|
||||
|---|---|
|
||||
| Язык | Node.js ≥ 18 (ES Modules, `"type": "module"`) |
|
||||
| Web-фреймворк | Express 4 |
|
||||
| Шаблонизация | EJS + `express-ejs-layouts` (layout: `views/layout.ejs`) |
|
||||
| Шаблон UI | SmartAdmin (собственные партиалы в `views/partials/`, SCSS в `public/sass/`, сборка `smartapp.min.css`) |
|
||||
| БД | SQLite через `better-sqlite3` (обёртка `db.runAsync/allAsync/getAsync` в `src/config/database.js`) |
|
||||
| Загрузка файлов | Multer (только картинки jpeg/png/webp/gif, ≤ 10 МБ) в `uploads/` |
|
||||
| QR | `qrcode` (для seed-фраз) |
|
||||
| CDN-ресурсы | Bootstrap 5.3.2, FontAwesome 6.4.2 с jsDelivr (только в legacy-`layout.js`) |
|
||||
|
||||
### Текущий фронтенд (что есть)
|
||||
|
||||
- **Два параллельных набора вьюх**:
|
||||
1. **EJS-шаблоны** (`views/*.ejs`) — используются роутами (dashboard, users, wallets, purchases, audit, settings, categories, locations, payment-wallets, seed, locales, catalog, products, product-edit, user-detail).
|
||||
2. **JS-рендер-функции** (`views/*.js`) — legacy/дублирующий слой, возвращающий HTML-строки (`layout()`, `renderCatalog()`, `renderWalletLayout()` и т.д.). **На практике роуты рендерят EJS; JS-слой частично устарел** (дублирует catalog и wallets). Новый фронтенд их не использует.
|
||||
- Статика: Bootstrap 5 (SmartAdmin-тема), ApexCharts, jQuery, FontAwesome, собственные скрипты `public/scripts/`.
|
||||
- AJAX-вызовы есть только в нескольких местах (catalog-модалка, wallets-обновление балансов, locales).
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектура развёртывания
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
├── HTTPS/LAN ───────────────► telegram_shop_prod :3001 (Express: бот + админка)
|
||||
│
|
||||
└── Tor Network ──► tor-proxy ──► HiddenService :80 ──► telegram_shop_prod :3001
|
||||
(admin onion-адрес, работа без HTTPS)
|
||||
```
|
||||
|
||||
- Единственный контейнер `telegram_shop_prod` (node:22-alpine) слушает порт `3001` (`ADMIN_PORT`).
|
||||
- Доступ к админке: напрямую `http://host:3001` или через `.onion` (Tor Browser).
|
||||
- Volume: `db/` (SQLite), `uploads/` (фото товаров, отдаются через `/uploads`), `.env` (только чтение).
|
||||
- WireGuard опционален (`WG_ENABLED`).
|
||||
|
||||
**Следствие для фронтенда**: админка доступна по **HTTP без HTTPS** (в т.ч. через Tor). Это накладывает жёсткие ограничения:
|
||||
- нельзя использовать Secure-куки;
|
||||
- нельзя полагаться на Origin-проверку (Tor) — CSRF реализован вручную (см. §10);
|
||||
- весь JS/CSS должен быть либо локальным, либо закешированным (CDN может быть недоступен из Tor).
|
||||
|
||||
---
|
||||
|
||||
## 4. Доступ и безопасность
|
||||
|
||||
### 4.1. Аутентификация (важно для фронта)
|
||||
|
||||
Реализована в `src/admin/auth.js`. **Не JWT, а самописный HMAC-токен**:
|
||||
|
||||
| Параметр | Значение |
|
||||
|---|---|
|
||||
| Кука | `admin_token` (httpOnly, `sameSite=false`, maxAge 24ч) |
|
||||
| Формат токена | `base64(payload).hmac_sha256(payload)` |
|
||||
| Payload | `{ role, jti, iat, exp }` |
|
||||
| Секрет | `ADMIN_SECRET` (env) |
|
||||
| Вход | POST `/login` с полем `token` (пароль = админ-токен) |
|
||||
| Выход | GET `/logout` |
|
||||
|
||||
Роли (определяются **каким секретом залогинились**):
|
||||
|
||||
| Роль | Условие | Права |
|
||||
|---|---|---|
|
||||
| `admin` | токен = `ADMIN_SECRET` | Все страницы, кроме seed-выгрузки |
|
||||
| `super_admin` | токен = `SUPER_ADMIN_SECRET` (если задан и ≠ ADMIN_SECRET) | + просмотр/экспорт seed-фраз, комиссии |
|
||||
|
||||
При `SUPER_ADMIN_SECRET` не заданном — **все админы считаются супер-админами** (`config.SUPER_ADMIN_IDS` по умолчанию = `ADMIN_IDS`, но роли в админке задаются именно через секреты, не через `ADMIN_IDS`).
|
||||
|
||||
Rate-limit логина: 5 попыток / 15 минут с IP.
|
||||
|
||||
**Деструктивные действия** (`/seed/*`) требуют повторного ввода токена в поле `reauth_token` (middleware `requireReAuth`).
|
||||
|
||||
### 4.2. Защита статики и загрузок
|
||||
|
||||
- `/uploads/*` — только после `requireAuth`, заголовки `X-Content-Type-Options: nosniff` + `Content-Disposition: attachment` (фото не рендерятся inline из uploads, только скачивание).
|
||||
- Файлы uploads именуются `{timestamp}-{hex}.{ext}`, MIME-белый список.
|
||||
- CSRF: заглушка (`csrf.js` возвращает пустой токен и no-op) — **сознательно отключено ради совместимости с Tor**. См. §10.
|
||||
|
||||
### 4.3. Существующие уязвимости, которые фронтенд не должен усугублять
|
||||
|
||||
1. Нет реальной CSRF-защиты — все POST-формы уязвимы к cross-site-запросам. В новом фронте использовать токены, если бэкенд их вернёт, иначе как минимум не убирать подтверждения на деструктив.
|
||||
2. Некоторые вьюхи выводят данные с ручным `esc()`/`escapeHtml()`; часть EJS использует `<%= %>` (экранируется) — но есть места с `JSON.stringify` прямо в `<script>` (инъекция через данные). Новому фронту: **все данные из API экранировать на клиенте**.
|
||||
3. Пароль админа (ADMIN_SECRET) отправляется как обычное поле формы.
|
||||
4. Seed-фразы — сверхчувствительные данные; показывать только super_admin, с аудитом (logAudit: `seed_phrase_viewed`, `seed_phrase_qr_viewed`, `csv_seed_export`), желательно с подтверждением.
|
||||
|
||||
---
|
||||
|
||||
## 5. Схема базы данных
|
||||
|
||||
Источник истины: `src/migrations/*.js` (001–012). SQLite, FK включены, WAL-режим.
|
||||
|
||||
### 5.1. `users`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK AUTOINCREMENT | Внутренний ID |
|
||||
| `telegram_id` | TEXT UNIQUE NOT NULL | ID в Telegram (строка!) |
|
||||
| `username` | TEXT | Ник |
|
||||
| `country` / `city` / `district` | TEXT | Гео из бота (снимок) |
|
||||
| `status` | INTEGER DEFAULT 0 | `0`=активен, `2`=заблокирован, прочее=удалён |
|
||||
| `total_balance` | REAL DEFAULT 0 | Основной баланс в USD |
|
||||
| `bonus_balance` | REAL DEFAULT 0 | Бонусный баланс в USD |
|
||||
| `language` | TEXT DEFAULT 'en' | Язык бота (`en`/`es`/`de`) |
|
||||
| `language_set` | INTEGER DEFAULT 0 | Флаг выбора языка |
|
||||
| `created_at` | DATETIME | Регистрация |
|
||||
|
||||
### 5.2. `crypto_wallets`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `user_id` | INTEGER FK→users (CASCADE) | |
|
||||
| `wallet_type` | TEXT | BTC/LTC/ETH/USDT/USDC (+ архивные с суффиксом `#N`, отфильтровываются `NOT LIKE '%#_%'`) |
|
||||
| `address` | TEXT | Адрес |
|
||||
| `derivation_path` | TEXT | Derivation path |
|
||||
| `mnemonic` | TEXT | **Зашифрована** (`encrypt(mnemonic, user_id)`) |
|
||||
| `balance` | REAL DEFAULT 0 | Кэш баланса (обновляется с блокчейна) |
|
||||
| `created_at` | DATETIME | |
|
||||
| UNIQUE | (user_id, wallet_type) | |
|
||||
|
||||
### 5.3. `transactions`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `user_id` | INTEGER FK | |
|
||||
| `wallet_type` | TEXT | |
|
||||
| `tx_hash` | TEXT | |
|
||||
| `amount` | REAL | |
|
||||
| `created_at` | DATETIME | |
|
||||
|
||||
### 5.4. `locations`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `country` | TEXT NOT NULL | |
|
||||
| `city` | TEXT NOT NULL | |
|
||||
| `district` | TEXT NOT NULL | Район (может быть пустой строкой) |
|
||||
| `is_active` | INTEGER NOT NULL DEFAULT 1 | Отключённые локации скрыты из бота |
|
||||
| `created_at` | DATETIME | |
|
||||
| UNIQUE | (country, city, district) | |
|
||||
|
||||
### 5.5. `categories`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `location_id` | INTEGER FK→locations (CASCADE) | |
|
||||
| `name` | TEXT NOT NULL | |
|
||||
| `is_active` | INTEGER NOT NULL DEFAULT 1 | |
|
||||
| `created_at` | DATETIME | |
|
||||
| UNIQUE | (location_id, name) | |
|
||||
|
||||
### 5.6. `subcategories`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `category_id` | INTEGER FK→categories (CASCADE) | |
|
||||
| `name` | TEXT NOT NULL | |
|
||||
| `is_active` | INTEGER NOT NULL DEFAULT 1 | |
|
||||
| `created_at` | DATETIME | |
|
||||
| UNIQUE | (category_id, name) | |
|
||||
|
||||
### 5.7. `products`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `location_id` | INTEGER FK→locations (CASCADE) | |
|
||||
| `category_id` | INTEGER FK→categories (CASCADE) | |
|
||||
| `subcategory_id` | INTEGER FK→subcategories (SET NULL) | |
|
||||
| `name` | TEXT NOT NULL | |
|
||||
| `description` | TEXT | Публичное описание |
|
||||
| `private_data` | TEXT | Внутренние заметки (не показывать) |
|
||||
| `price` | REAL NOT NULL CHECK (price > 0) | USD |
|
||||
| `quantity_in_stock` | INTEGER DEFAULT 0 | Остаток |
|
||||
| `photo_url` | TEXT | Публичное фото (URL или `/uploads/...`) |
|
||||
| `hidden_photo_url` | TEXT | Фото после покупки |
|
||||
| `hidden_coordinates` | TEXT | Координаты после покупки (`lat,lng`) |
|
||||
| `hidden_description` | TEXT | Текст после покупки |
|
||||
| `is_mono` | INTEGER DEFAULT 0 | **Цифровой товар**: бесконечный остаток (`1` ⇒ stock=999999, в боте продаётся без складских проверок) |
|
||||
| `created_at` | DATETIME | |
|
||||
|
||||
### 5.8. `purchases`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `user_id` | INTEGER FK→users (CASCADE) | |
|
||||
| `product_id` | INTEGER FK→products (CASCADE) | |
|
||||
| `wallet_type` | TEXT | Валюта оплаты |
|
||||
| `tx_hash` | TEXT | Хэш транзакции |
|
||||
| `quantity` | INTEGER CHECK (quantity > 0) | |
|
||||
| `total_price` | REAL CHECK (total_price > 0) | USD |
|
||||
| `purchase_date` | DATETIME | |
|
||||
| `status` | TEXT DEFAULT 'pending' | `pending` / `completed` / `cancelled` |
|
||||
|
||||
### 5.9. `commission_payments`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `total_balance_usd` | REAL NOT NULL | Снимок баланса на момент платежа |
|
||||
| `commission_rate` | REAL NOT NULL | % комиссии |
|
||||
| `commission_amount_usd` | REAL NOT NULL | Начисленная комиссия |
|
||||
| `paid_amount_usd` | REAL NOT NULL | Фактически уплачено |
|
||||
| `wallet_count` | INTEGER NOT NULL | Кошельков на момент |
|
||||
| `note` | TEXT | |
|
||||
| `created_at` | DATETIME | |
|
||||
|
||||
### 5.10. `audit_log`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `id` | INTEGER PK | |
|
||||
| `action` | TEXT NOT NULL | Слаг действия: `balance_adjust`, `csv_seed_export`, `seed_phrase_viewed`, `seed_phrase_qr_viewed`, `login` и т.д. |
|
||||
| `admin_id` | TEXT NOT NULL | Роль или ID |
|
||||
| `details` | TEXT | JSON-строка с контекстом |
|
||||
| `created_at` | DATETIME | |
|
||||
|
||||
### 5.11. `user_states`
|
||||
|
||||
| Колонка | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `chat_id` | TEXT PK | |
|
||||
| `state_data` | TEXT | JSON состояния диалога бота |
|
||||
| `updated_at` | INTEGER | |
|
||||
|
||||
---
|
||||
|
||||
## 6. Карта эндпоинтов (полная)
|
||||
|
||||
Базовый URL: `http://<host>:3001`. **Все маршруты, кроме `/login`, `/logout`, `/health`, требуют куки `admin_token`** (redirect на `/login`).
|
||||
|
||||
Легенда: 🔓 — только super_admin · ⚠️ — деструктивно, требует `requireReAuth` · 📁 — загрузка файла · 🔁 — редирект после POST.
|
||||
|
||||
### 6.1. Служебные
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/health` | Health-check → `{ status:'ok', uptime }` |
|
||||
| GET | `/login` | Страница логина (поле `token`) |
|
||||
| POST | `/login` | Вход; body `{ token }`; 429 при rate-limit; 401 неверный токен |
|
||||
| GET | `/logout` | Выход + инвалидация jti |
|
||||
| GET | `/error` (рендер) | Страница ошибки (глобальный обработчик) |
|
||||
|
||||
### 6.2. Dashboard (роут: `routes/dashboard.js`)
|
||||
|
||||
| Метод | Путь | Ответ |
|
||||
|---|---|---|
|
||||
| GET | `/` | EJS `dashboard` со статами и данными графиков |
|
||||
|
||||
Query-параметры-флеши: `?seeded=1`, `?cleared=1` (показывают сообщения).
|
||||
|
||||
**Данные, передаваемые в шаблон** (это и есть «API» дашборда):
|
||||
|
||||
| Поле | Тип | Содержимое |
|
||||
|---|---|---|
|
||||
| `stats.totalUsers` | int | Всего пользователей |
|
||||
| `stats.totalProducts` | int | Всего товаров |
|
||||
| `stats.totalPurchases` | int | Всего покупок |
|
||||
| `stats.totalRevenue` | float | Выручка по `status='completed'` |
|
||||
| `stats.totalSubcategories` | int | Подкатегорий |
|
||||
| `stats.aov` | float | Средний чек (completed) |
|
||||
| `stats.conversionRate` | float % (1 знак) | Купившие / все пользователи × 100 |
|
||||
| `stats.completedPurchases` / `pendingPurchases` / `cancelledPurchases` | int | Сплит по статусам |
|
||||
| `chartData.days` | string[] | Последние 7 дат `YYYY-MM-DD` |
|
||||
| `chartData.days30` | string[] | Последние 30 дат |
|
||||
| `chartData.revenueData` | number[] | Выручка по дням (7д) |
|
||||
| `chartData.revenueData30` | number[] | Выручка по дням (30д) |
|
||||
| `chartData.usersData` | number[] | Новые пользователи по дням (7д) |
|
||||
| `chartData.topProducts` | `{name, qty, revenue}[]` | Топ-5 товаров за 30д (по кол-ву) |
|
||||
| `chartData.topSpenders` | `{username, spent}[]` | Топ-5 покупателей за 30д |
|
||||
| `chartData.revenueByCategory` | `{name, revenue}[]` | Выручка по категориям за 30д |
|
||||
| `chartData.topCountries` | `{country, count}[]` | Топ-5 стран из locations |
|
||||
| `activities` | `{type, id, username, item, total_price, date}` (или `{type:'audit', ...}`) | Последние 10 completed-покупок; если пусто — последние 10 audit_log |
|
||||
| `wallets` | `{wallet_type, count, total}[]` | Сводка кошельков по валютам (верхний регистр) |
|
||||
| `commission` | `{enabled, rate, due, currentCommission, lastPaidAmount, totalBalance}` | SaaS-комиссия: начислено/оплачено/к оплате |
|
||||
|
||||
### 6.3. Catalog (роут: `routes/catalog.js` — дерево; `routes/catalogProducts.js` — товары)
|
||||
|
||||
Дерево: Страна → Город → Район(location) → Категории → Подкатегории.
|
||||
|
||||
| Метод | Путь | Body / Param | Назначение |
|
||||
|---|---|---|---|
|
||||
| GET | `/catalog` | query: `?loc=`, `?cat=`, `?sub=` (фильтры), `?msg=&msg_type=` (флеш) | EJS `catalog` + `treeHtml` + `products` |
|
||||
| POST | `/catalog/locations` | `country, city, district?` | + локация |
|
||||
| POST | `/catalog/locations/add-city` | `country, city` | + город (district='') |
|
||||
| POST | `/catalog/locations/add-district` | `country, city, district` | + район |
|
||||
| POST | `/catalog/locations/:id/update` | `country, city, district` | Переименование |
|
||||
| POST | `/catalog/locations/:id/toggle` | — | Вкл/выкл `is_active` |
|
||||
| POST | `/catalog/locations/:id/delete` | — | Удаление (запрещено при наличии категорий/товаров) |
|
||||
| POST | `/catalog/categories` | `name, location_id` | + категория |
|
||||
| POST | `/catalog/categories/json` | JSON `{name, location_id}` | + категория (AJAX, возвращает объект категории) |
|
||||
| POST | `/catalog/categories/:id/update` | `name, location_id?` | Переименование/перенос |
|
||||
| POST | `/catalog/categories/:id/toggle` | — | Вкл/выкл |
|
||||
| POST | `/catalog/categories/:id/delete` | — | Удаление (блокируется при товарах) |
|
||||
| POST | `/catalog/categories/:id/subcategories` | `name` | + подкатегория |
|
||||
| POST | `/catalog/categories/:id/subcategories/json` | JSON `{name}` | + подкатегория (AJAX) |
|
||||
| POST | `/catalog/subcategories/:id/update` | `name` | Переименование |
|
||||
| POST | `/catalog/subcategories/:id/toggle` | — | Вкл/выкл |
|
||||
| POST | `/catalog/subcategories/:id/delete` | — | Удаление (блокируется при товарах) |
|
||||
| POST | `/catalog/products` | multipart: поля товара + `photo_file`, `hidden_photo_file` | + товар |
|
||||
| POST | `/catalog/products/:id/edit` | multipart: поля товара + файлы | Обновление |
|
||||
| POST | `/catalog/products/:id/delete` | — | Удаление товара |
|
||||
| GET | `/catalog/products/:id/json` | — | **AJAX**: товар с `country, city, district, category_name, subcategory_name` |
|
||||
|
||||
**Поля формы товара** (обязательные: `name, price>0, description, photo`):
|
||||
|
||||
`name, price, quantity_in_stock, description, photo_url, hidden_photo_url, hidden_coordinates, hidden_description, private_data, category_id, subcategory_id?, location_id?, is_mono`
|
||||
|
||||
Логика:
|
||||
- `is_mono=1` ⇒ `quantity_in_stock` принудительно `999999`.
|
||||
- Если `location_id` не передан — берётся `location_id` категории.
|
||||
- Файл заменяет URL (`photo_url`), если загружен.
|
||||
- JSON-ответы: `{error: '...'}` при 400.
|
||||
|
||||
### 6.4. Products (роут: `routes/products.js` — упрощённый, дублирует catalog)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/products` | EJS `products` (таблица до 100 товаров) |
|
||||
| POST | `/products` | + товар (без файлов; `photo_url` обязателен) |
|
||||
| GET | `/products/:id/edit` | EJS `product-edit` |
|
||||
| POST | `/products/:id/update` | Обновление |
|
||||
| POST | `/products/:id/delete` | Удаление |
|
||||
|
||||
> **Рекомендация фронту**: считать **catalog** основным экраном товаров; `/products` — legacy-дубль.
|
||||
|
||||
### 6.5. Categories (роут: `routes/categories.js` — плоские таблицы)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/categories` | EJS `categories` + `subcategoriesByCategory`, флеши `?error=`, `?success=` |
|
||||
| POST | `/categories` | + категория (`name, location_id`) |
|
||||
| POST | `/categories/:id/update` | `name, location_id` |
|
||||
| POST | `/categories/:id/toggle` | Вкл/выкл |
|
||||
| POST | `/categories/:id/delete` | Удаление (блок при товарах; удаляет и подкатегории) |
|
||||
| POST | `/categories/:id/subcategories` | + подкатегория |
|
||||
| POST | `/categories/subcategories/:id/update` | `name` |
|
||||
| POST | `/categories/subcategories/:id/toggle` | Вкл/выкл |
|
||||
| POST | `/categories/subcategories/:id/delete` | Удаление (блок при товарах) |
|
||||
|
||||
### 6.6. Locations (роут: `routes/locations.js`)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/locations` | EJS `locations` с `category_count`, `product_count` |
|
||||
| POST | `/locations` | + локация |
|
||||
| POST | `/locations/:id/update` | `country, city, district` |
|
||||
| POST | `/locations/:id/toggle` | Вкл/выкл |
|
||||
| POST | `/locations/:id/delete` | Блок при категориях/товарах |
|
||||
|
||||
### 6.7. Users (роут: `routes/users.js`)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/users` | EJS `users` (последние 100, `ORDER BY id DESC`) |
|
||||
| GET | `/users/:id` | EJS `user-detail` + последние 20 покупок с `product_name` |
|
||||
| POST | `/users/:id/toggle-status` | Бан/разбан (`status` 0↔2) |
|
||||
| POST | `/users/:id/adjust-balance` | `amount, currency` (`total_balance`|`bonus_balance`); пишет audit `balance_adjust`; редирект на `/users/:id` |
|
||||
|
||||
### 6.8. Wallets (роут: `routes/wallets.js`) 🔓 для seed-части
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/wallets` | EJS `wallets`; query `?user=`, `?seeds=1`; полный срез: `users` (с `wallet_count`), `wallets` выбранного юзера, `stats` (см. ниже) |
|
||||
| POST | `/wallets/record-payment` | `paid_amount, note` → запись в `commission_payments`; редирект `/wallets?payment=recorded` |
|
||||
| POST | `/wallets/export-seeds` 🔓 | CSV всех кошельков с расшифрованными seed-фразами; audit `csv_seed_export` |
|
||||
| GET | `/wallets/refresh-balances/:userId` | **AJAX**: обновить балансы с блокчейна; ответ JSON см. ниже |
|
||||
| GET | `/wallets/seed/:walletId` 🔓 | **AJAX**: расшифрованная seed-фраза + derivation; audit `seed_phrase_viewed` |
|
||||
| GET | `/wallets/seed-qr/:walletId` 🔓 | **AJAX**: PNG QR-код seed-фразы (image/png); audit `seed_phrase_qr_viewed` |
|
||||
|
||||
**Формат `stats`** (передаётся в шаблон wallets):
|
||||
|
||||
| Поле | Тип | Описание |
|
||||
|---|---|---|
|
||||
| `totals` | `{btc,ltc,eth,usdt,usdc}` | Суммы балансов по валютам |
|
||||
| `walletCounts` | `{...}` | Кол-во кошельков по валютам |
|
||||
| `usdValues` | `{...}` | USD-оценка по валютам (курсы с CoinGecko) |
|
||||
| `totalUsd` | float | Итог в USD |
|
||||
| `prices` | `{btc,ltc,eth}` | Текущие курсы |
|
||||
| `totalWallets` | int | Активных кошельков |
|
||||
| `commissionRate` | float | % |
|
||||
| `currentCommission` | float | Ставка × totalUsd |
|
||||
| `lastPaidAmount` | float | Последняя оплата |
|
||||
| `commissionDue` | float | max(0, current − last) |
|
||||
| `commissionEnabled` | bool | |
|
||||
| `commissionWallets` | `{BTC,LTC,USDT,USDC,ETH}` | Адреса для оплаты комиссии |
|
||||
| `totalUsers` | int | |
|
||||
| `payments` | `commission_payments[]` | Последние 20 платежей |
|
||||
| `seedsPaid` | bool | lastPaid ≥ current |
|
||||
|
||||
**Формат `refresh-balances` (JSON)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"wallets": [{ "id", "wallet_type", "address", "balance", "usdValue", "created_at" }],
|
||||
"totalBalance": 123.45,
|
||||
"balances": { "BTC": { "amount", "usdValue" }, ... },
|
||||
"timestamp": "ISO",
|
||||
"error": "Blockchain API unavailable — showing cached balances" // опционально
|
||||
}
|
||||
```
|
||||
|
||||
**Формат `seed/:walletId` (JSON)**: `{ walletId, walletType, address, derivationPath, mnemonic, userId, username }` (404 `{error}` при отсутствии).
|
||||
|
||||
**Логика разблокировки seed**: кнопка «Unlock» доступна, только если `seedsPaid` (комиссия оплачена). Параметр `?seeds=1` открывает таблицу всех seed-фраз.
|
||||
|
||||
### 6.9. Purchases (роут: `routes/purchases.js`)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/purchases` | EJS `purchases` (последние 200, JOIN product_name) |
|
||||
|
||||
### 6.10. Audit (роут: `routes/audit.js`)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/audit` | EJS `audit` (последние 200 записей `audit_log`) |
|
||||
|
||||
### 6.11. Settings (роут: `routes/settings.js`)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/settings` | EJS `settings`; флеши `?saved=1`, `?error=1` |
|
||||
| POST | `/settings` | Обновление `.env` |
|
||||
|
||||
**Редактируемые ключи** (только они; остальные строки `.env` не трогаются):
|
||||
|
||||
`BOT_TOKEN, SUPPORT_LINK, ADMIN_IDS, SUPER_ADMIN_IDS, WG_ENABLED, WG_ENDPOINT, WG_ADDRESS, WG_PUBLIC_KEY, WG_DNS, ADMIN_PORT, ADMIN_URL, CATALOG_PATH, GITEA_API_URL`
|
||||
|
||||
**Никогда не перезаписываются** (показываются как `•••••••`): `ENCRYPTION_KEY, ADMIN_SECRET, GITEA_TOKEN, WG_PRIVATE_KEY, WG_PRESHARED_KEY, COMMISSION_*`.
|
||||
|
||||
⚠️ Изменения применяются только **после рестарта контейнера** (сообщение об этом на странице).
|
||||
|
||||
### 6.12. Payment Wallets (роут: `routes/paymentWallets.js`)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/payment-wallets` | EJS `payment-wallets` — **только чтение** конфига `COMMISSION_WALLETS` (адреса для уплаты комиссии владельцу) |
|
||||
|
||||
### 6.13. Seed & Reset (роут: `routes/seed.js`) ⚠️ requireReAuth
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/seed` | EJS `seed` (форма с подтверждением токеном) |
|
||||
| POST | `/seed/seed-demo` | **Полная очистка БД + заливка демо-данных**. Body: `reauth_token` |
|
||||
| POST | `/seed/clear-all` | **Полная очистка всех таблиц**. Body: `reauth_token` |
|
||||
|
||||
Очищаемые таблицы: `purchases, transactions, crypto_wallets, audit_log, user_states, products, subcategories, categories, users, locations, commission_payments` + сброс `sqlite_sequence`.
|
||||
|
||||
### 6.14. Locales (роут: `routes/locales.js`)
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/locales` | EJS `locales` + `sections` (ключи верхнего уровня из `en.json`) |
|
||||
| POST | `/locales/save` | JSON `{lang, key, value}` (точечный путь, напр. `menu.shop`); запись в `src/i18n/locales/{lang}.json`; 400 при неверном `lang` |
|
||||
|
||||
Языки: `en`, `es`, `de` (`AVAILABLE_LANGUAGES`). Структура файлов — вложенные объекты; ключ в запросе — dot-path.
|
||||
|
||||
---
|
||||
|
||||
## 7. Спецификация экранов
|
||||
|
||||
### 7.1. Login (`/login`)
|
||||
- Одно поле `token` (пароль-токен), кнопка Submit.
|
||||
- Ошибки: `401 Invalid token`, `429 Too many attempts...` (15 мин).
|
||||
- После успеха — redirect `/`.
|
||||
- Подсказка: если `SUPER_ADMIN_SECRET` задан — вход с ним даёт роль super_admin.
|
||||
|
||||
### 7.2. Dashboard (`/`)
|
||||
KPI-карточки:
|
||||
- Пользователи всего, Товары, Покупки всего, Выручка ($, completed), Средний чек ($), Конверсия (%), Сплит по статусам (completed/pending/cancelled), Подкатегории.
|
||||
|
||||
Графики (данные готовы в `chartData`):
|
||||
1. Выручка 7 дней (line/area) — `days` × `revenueData`.
|
||||
2. Выручка 30 дней (line/area) — `days30` × `revenueData30`.
|
||||
3. Новые пользователи 7 дней (bar) — `days` × `usersData`.
|
||||
4. Топ-5 товаров (bar) — `topProducts`.
|
||||
5. Топ-5 покупателей (bar) — `topSpenders`.
|
||||
6. Выручка по категориям (pie/donut) — `revenueByCategory`.
|
||||
7. Топ-5 стран (bar) — `topCountries`.
|
||||
|
||||
Блоки: Активность (последние покупки/аудит), Сводка кошельков по валютам, SaaS-комиссия (enabled/rate/due/последняя оплата).
|
||||
|
||||
### 7.3. Catalog (`/catalog`) — самый сложный экран
|
||||
Две колонки:
|
||||
- **Слева — дерево**: accordion Страна → Город → Район → Категория → Подкатегория. У каждого узла:
|
||||
- счётчик товаров (включая подкатегории),
|
||||
- бейдж «Disabled» у неактивных,
|
||||
- inline-формы: переименовать, вкл/выкл (⏻), добавить ребёнка, удалить (✕, с confirm).
|
||||
- **Справа — таблица товаров** (до 200): ID, фото (40×40), название, категория, подкатегория, цена $, остаток (∞ для mono), действия: ✎ Edit (модалка), ✕ Delete (confirm).
|
||||
- Фильтры через клик по узлам дерева (`?loc=`, `?cat=`, `?sub=`).
|
||||
- **Модалка товара** (Add/Edit): каскадные селекты Страна→Город→Район→(Категория фильтруется по району)→Подкатегория; inline-создание категории/подкатегории через `fetch` к `/json`-эндпоинтам; чекбокс «Digital Product (infinite stock)» блокирует поле остатка; загрузка 2 фото (public, hidden) или URL; поля скрытого контента (фото, координаты, описание) и private_data.
|
||||
- Флеш-сообщения: `?msg=` + `?msg_type=` (success/error/info).
|
||||
|
||||
### 7.4. Products (`/products`) — legacy
|
||||
Простая таблица (ID, фото, название, категория, цена, остаток, действия) + формы Add/Edit на отдельных страницах. **Фронту: экран можно заменить ссылкой на Catalog.**
|
||||
|
||||
### 7.5. Categories (`/categories`)
|
||||
Плоские таблицы: категории (с локацией и product_count) + подкатегории (сгруппированы по категориям). Действия: add/edit/toggle/delete. Ошибки через `?error=` (с счётчиком блокирующих товаров), успех через `?success=`.
|
||||
|
||||
### 7.6. Locations (`/locations`)
|
||||
Таблица: Страна/Город/Район, category_count, product_count, is_active, действия add/edit/toggle/delete. Ошибки/успехи как у Categories. Заблокированное удаление — с подсказкой «Remove categories/products first».
|
||||
|
||||
### 7.7. Users (`/users`, `/users/:id`)
|
||||
- Список (100): ID, Telegram ID, username, страна, город, статус (badge Active/Blocked/Deleted), баланс $, кнопки View и Ban/Unban.
|
||||
- Детальная (`/users/:id`): профиль + таблица покупок (20) + **форма корректировки баланса** (amount + выбор `total_balance`/`bonus_balance`) + audit-запись.
|
||||
|
||||
### 7.8. Wallets (`/wallets`)
|
||||
- Слева: поиск + список пользователей (статус, число кошельков), выбор `?user=`.
|
||||
- Справа: карточка балансов выбранного (Main/Bonus/Available/Status), таблица кошельков (тип, адрес — клик=копировать, баланс 8 знаков, дата), кнопка «Refresh balances» (fetch `/wallets/refresh-balances/:userId`).
|
||||
- **Owner Summary** (для всех): Total USD, Users, Active Wallets, Commission Due; таблица «Balances by Currency»; блок комиссии: rate, total, full commission, last paid, due, форма «Record Payment» (paid_amount, note), адреса для оплаты (клик=копировать); таблица Payment History (delta между платежами).
|
||||
- **Seed Phrases** (super_admin, если комиссия оплачена): кнопка «Unlock», таблица всех seed-фраз (user, type, address, derivation, mnemonic — клик=копировать), кнопка «Export All Seeds as CSV» (`/wallets/export-seeds`). Если не оплачено — предупреждение и заблокированная кнопка.
|
||||
|
||||
### 7.9. Purchases (`/purchases`)
|
||||
Таблица (200): ID, user (ссылка), product, qty, цена, валюта, дата, статус (badge completed/pending/failed). **Нет фильтров и действий — только просмотр.**
|
||||
|
||||
### 7.10. Audit (`/audit`)
|
||||
Таблица (200): action, admin_id, details (JSON-строка), created_at. Только просмотр.
|
||||
|
||||
### 7.11. Settings (`/settings`)
|
||||
Форма с секциями:
|
||||
- Bot: `BOT_TOKEN` (placeholder-маска), `SUPPORT_LINK`, `ADMIN_IDS`, `SUPER_ADMIN_IDS`.
|
||||
- WireGuard: `WG_ENABLED` (checkbox), `WG_ENDPOINT`, `WG_ADDRESS`, `WG_PUBLIC_KEY`, `WG_DNS`.
|
||||
- Admin: `ADMIN_PORT`, `ADMIN_URL`, `CATALOG_PATH`, `GITEA_API_URL`.
|
||||
- Только для чтения (маскированные `•••••••`): `ENCRYPTION_KEY`, `ADMIN_SECRET`, `GITEA_TOKEN`, `WG_PRIVATE_KEY`, `WG_PRESHARED_KEY`, `COMMISSION_*`.
|
||||
- Предупреждение: «Restart the application to apply changes».
|
||||
|
||||
### 7.12. Payment Wallets (`/payment-wallets`)
|
||||
Таблица адресов комиссии (BTC/LTC/USDT/USDC/ETH) — только чтение; ссылка на Settings.
|
||||
|
||||
### 7.13. Locales (`/locales`)
|
||||
- Таблица: строки = ключи из `en.json` (секции-заголовки), колонки = языки en/es/de.
|
||||
- Редактирование значения: inline (fetch POST `/locales/save` `{lang, key, value}`).
|
||||
- Требование: 3 языка всегда синхронны по набору ключей; добавление ключа в одном — добавить в остальных (200+ ключей).
|
||||
|
||||
### 7.14. Seed & Reset (`/seed`)
|
||||
Две опасные кнопки: «Seed Demo Data» и «Clear All Data». Обе требуют повторный ввод токена (`reauth_token`). В UI — двойное подтверждение (модалка + поле токена), предупреждение о необратимости.
|
||||
|
||||
---
|
||||
|
||||
## 8. Роли и права
|
||||
|
||||
| Возможность | admin | super_admin |
|
||||
|---|---|---|
|
||||
| Все страницы, кроме seed-выгрузки | ✅ | ✅ |
|
||||
| Просмотр/экспорт seed-фраз (`/wallets/seed*`, `export-seeds`) | ❌ (401/403) | ✅ |
|
||||
| Запись комиссионных платежей | ✅ (форма на /wallets) | ✅ |
|
||||
| Seed & Reset (`/seed/*`) | ✅ (с reauth) | ✅ (с reauth) |
|
||||
|
||||
Middleware: `requireAuth` (все роуты после `/login`), `requireSuperAuth` (seed-эндпоинты), `requireReAuth` (seed/reset).
|
||||
|
||||
---
|
||||
|
||||
## 9. Формат данных
|
||||
|
||||
- Деньги: USD, `REAL`. Выводить `$X.XX`.
|
||||
- Крипто-балансы: до 8 знаков, обрезать хвостовые нули.
|
||||
- Даты: `DATETIME` SQLite (`YYYY-MM-DD HH:MM:SS`, UTC). На клиенте конвертировать в локальное время.
|
||||
- Фото: абсолютный путь (например `/uploads/...`) или внешний URL. В таблицах — превью 40–50px.
|
||||
- Status пользователя: `0` активен, `2` заблокирован, иное — удалён.
|
||||
- Status покупки: `pending` | `completed` | `cancelled`.
|
||||
- `is_mono`: `1` = цифровой (бесконечный остаток, показывать ∞).
|
||||
- Флеш-сообщения: через query-параметры (`?msg=`, `?error=`, `?success=`, `?seeded=1`, `?cleared=1`, `?saved=1`, `?payment=recorded`).
|
||||
|
||||
---
|
||||
|
||||
## 10. Существующие ограничения и подводные камни
|
||||
|
||||
1. **Нет REST API.** Всё — SSR EJS + формы + редкие fetch. Для полноценного SPA бэкенду нужны JSON-эндпоинты (предложены в §12) — **отдел фронтенда не должен проектировать свой API без согласования с бэкендом**.
|
||||
2. **CSRF отключён** (заглушка). Новая админка не должна полагаться на Origin/SameSite; если оставляем формы — обязательны серверные токены или хотя бы подтверждения на деструктив.
|
||||
3. **Работа через Tor**: нельзя полагаться на внешние CDN (Bootstrap/FontAwesome/ApexCharts должны быть локальными), нельзя требовать HTTPS/WSS, куки `Secure` не работают.
|
||||
4. **Нет пагинации** нигде (LIMIT 100/200 фиксирован). При росте данных списки «режутся» — фронт может предложить пагинацию, но это потребует новых query-параметров на бэкенде.
|
||||
5. **Дублирование каталога**: `/catalog` (богатый) и `/products` (бедный) — менять оба или убрать один (решение за продуктом).
|
||||
6. **Фото из uploads отдаются с `Content-Disposition: attachment`** — их нельзя показывать `<img>` inline из `/uploads/` (скачаются как файл). В таблицах товаров фото берутся из `photo_url`, который может указывать на внешний URL или `/uploads/...` — при `/uploads` превью не отобразится в браузере. Для галереи товаров нужно либо отдельный inline-эндпоинт, либо хранить публичные фото вне uploads (например, статика `public/img/products/`).
|
||||
7. **Нет нотификаций/вебхуков** — dashboard статичен до перезагрузки.
|
||||
8. **Settings перезаписывают `.env`** — изменения требуют рестарта; форма должна явно это сообщать.
|
||||
9. **Seed/Reset опасны** — полная очистка БД; в новом UI обязателен confirm + reauth (уже реализовано на бэкенде).
|
||||
10. **i18n-файлы — живые данные** админки; редактирование без синхронизации ключей ломает бота (fallback на `en`).
|
||||
11. Legacy JS-слой (`views/*.js`) частично дублирует EJS — **не использовать как источник истины**, ссылаться на роуты и EJS.
|
||||
|
||||
---
|
||||
|
||||
## 11. Требования к новому фронтенду
|
||||
|
||||
### 11.1. Целевой стек (рекомендация)
|
||||
- SPA на Vue 3 или React (или продолжение SSR EJS, если команда не готова к SPA).
|
||||
- Все ассеты **локальные** (сборка в `public/dist`), без CDN — совместимость с Tor.
|
||||
- Адаптивность: админкой пользуются с десктопа и планшета.
|
||||
- Тёмная/светлая тема (SmartAdmin уже имеет темы — можно переиспользовать SCSS-переменные).
|
||||
- Charts: ApexCharts (уже есть в проекте) или ECharts.
|
||||
|
||||
### 11.2. Обязательные экраны (приоритет)
|
||||
1. **Dashboard** — KPI + 7 графиков (данные в §6.2).
|
||||
2. **Catalog** — дерево + таблица товаров + модалка товара с каскадными селектами и загрузкой 2 фото.
|
||||
3. **Users + User detail** — список, бан/разбан, корректировка баланса.
|
||||
4. **Wallets** — пользователь + кошельки + Owner Summary + комиссия + seed (super_admin).
|
||||
5. **Purchases** — таблица со статусами.
|
||||
6. **Audit** — журнал.
|
||||
7. **Categories / Locations** — CRUD + toggle.
|
||||
8. **Settings** — форма .env с масками секретов.
|
||||
9. **Locales** — редактор переводов на 3 языка.
|
||||
10. **Seed & Reset** — подтверждённая деструктивная панель.
|
||||
11. **Login / Logout**.
|
||||
|
||||
### 11.3. UI/UX-требования
|
||||
- Все деструктивные действия — confirm-модалка (не `window.confirm`).
|
||||
- Все флеш-сообщения — toast/alert с автоскрытием.
|
||||
- Каскадные select'ы — с загрузкой и disabled-состояниями.
|
||||
- Копирование адресов/seed — по клику с визуальной обратной связью «Copied!».
|
||||
- Пустые состояния («No products found», «No users», «No wallets yet»).
|
||||
- Загрузочные спиннеры на fetch-запросах.
|
||||
- Ошибки API (400/401/403/404/500) — понятные сообщения пользователю.
|
||||
- Валидация форм на клиенте (price>0, name required, photo required, amount>0) + серверная.
|
||||
- Хлебные крошки, поиск по меню (уже есть в SmartAdmin).
|
||||
- Индикация роли (admin/super_admin) в шапке; скрывать seed-раздел для обычных админов.
|
||||
|
||||
### 11.4. Интеграционные требования
|
||||
- Авторизация: работа с кукой `admin_token`; при 401/403 от `/wallets/seed*` — редирект на `/login` или сообщение о недостатке прав.
|
||||
- Все POST-формы в SPA отправлять с `Content-Type: application/x-www-form-urlencoded` или `multipart/form-data` в зависимости от бэкенд-роута (см. §6).
|
||||
- Не вводить собственный роутинг API-путей — строго использовать карту §6 (или согласованные новые из §12).
|
||||
- Реализовать обработку флеш-query-параметров на клиенте.
|
||||
|
||||
### 11.5. Что НЕ делать
|
||||
- Не разворачивать отдельный фронтенд-сервер (админка живёт в контейнере бота; статика отдаётся из `src/admin/public`).
|
||||
- Не менять схему БД без миграций и без бэкенда.
|
||||
- Не показывать seed-фразы не-super_admin.
|
||||
- Не загружать фото в произвольные пути — только через существующий multipart-роут `/catalog/products` (+ file input) или `/products`.
|
||||
|
||||
---
|
||||
|
||||
## 12. API-контракты JSON (дополнить бэкенду)
|
||||
|
||||
Для SPA-режима предлагаются **новые** JSON-эндпоинты (требуют реализации на бэкенде — вне зоны ответственности фронта, но контракты фиксируем заранее). Все — под `requireAuth`, префикс `/api`.
|
||||
|
||||
| Метод | Путь | Ответ |
|
||||
|---|---|---|
|
||||
| GET | `/api/stats` | Агрегат дашборда (поля из §6.2) |
|
||||
| GET | `/api/users?search=&page=&limit=` | Пагинированный список пользователей |
|
||||
| GET | `/api/users/:id` | Профиль + балансы |
|
||||
| POST | `/api/users/:id/toggle-status` | `{ok:true}` |
|
||||
| POST | `/api/users/:id/adjust-balance` | body `{amount, currency}` → `{ok:true, newBalance}` |
|
||||
| GET | `/api/catalog/tree` | Дерево (страна→город→район→категория→подкатегория) с is_active и счётчиками |
|
||||
| GET | `/api/products?loc=&cat=&sub=&page=&limit=` | Пагинированный список товаров |
|
||||
| GET | `/api/products/:id` | Товар целиком (вкл. скрытый контент и private_data) |
|
||||
| POST | `/api/products` | Создание (JSON или multipart) |
|
||||
| PUT | `/api/products/:id` | Обновление |
|
||||
| DELETE | `/api/products/:id` | Удаление |
|
||||
| GET | `/api/locations` | Список с counts |
|
||||
| POST/PUT/DELETE | `/api/locations...` | CRUD |
|
||||
| GET | `/api/categories` / `/api/subcategories` | Списки с counts |
|
||||
| POST/PUT/DELETE | `/api/categories...` | CRUD |
|
||||
| GET | `/api/purchases?status=&page=&limit=` | Пагинированные покупки с фильтром по статусу |
|
||||
| PATCH | `/api/purchases/:id/status` | Смена статуса (требует согласования с ботом — статусы влияют на доставку контента!) |
|
||||
| GET | `/api/wallets/overview` | Owner summary + комиссия + платежи |
|
||||
| GET | `/api/wallets?userId=` | Кошельки пользователя |
|
||||
| POST | `/api/wallets/refresh` | body `{userId}` → свежие балансы |
|
||||
| POST | `/api/wallets/record-payment` | body `{paidAmount, note}` |
|
||||
| GET | `/api/wallets/seeds` 🔓 | Список seed-фраз |
|
||||
| GET | `/api/wallets/seeds/:id` 🔓 | Одна seed-фраза |
|
||||
| GET | `/api/wallets/seeds/:id/qr` 🔓 | PNG QR |
|
||||
| GET | `/api/audit?page=&limit=` | Пагинированный аудит |
|
||||
| GET/POST | `/api/locales` | Чтение/сохранение ключей (как `/locales/save`) |
|
||||
| GET/PUT | `/api/settings` | Чтение маскированных / запись разрешённых ключей |
|
||||
| POST | `/api/seed/demo`, `/api/seed/clear` ⚠️ | с `reauth_token` |
|
||||
| GET | `/api/session` | Текущая роль (admin/super_admin) для условного рендера |
|
||||
|
||||
⚠️ **Важно**: PATCH статуса покупки должен быть синхронизирован с `purchaseService` (доставка скрытого контента происходит при `completed`).
|
||||
|
||||
---
|
||||
|
||||
## 13. Чек-лист приёмки
|
||||
|
||||
- [ ] Логин/логаут работают; роль super_admin корректно отображается.
|
||||
- [ ] Dashboard: все KPI и 7 графиков рендерятся без ошибок.
|
||||
- [ ] Catalog: дерево полностью управляемо (add/rename/toggle/delete на всех 4 уровнях), товары CRUD с загрузкой 2 фото, каскадные селекты, mono-флаг.
|
||||
- [ ] Users: список, детальная, бан/разбан, корректировка баланса с audit.
|
||||
- [ ] Wallets: выбор пользователя, обновление балансов (fetch), Owner Summary, запись комиссии, история платежей, seed-блок с правами и QR/CSV.
|
||||
- [ ] Purchases: таблица со статусами и ссылками на пользователей.
|
||||
- [ ] Audit: журнал читается, details парсится.
|
||||
- [ ] Categories/Locations: CRUD с блокировками удаления и понятными ошибками.
|
||||
- [ ] Settings: сохраняются только разрешённые ключи; секреты маскированы.
|
||||
- [ ] Locales: inline-редактирование всех 3 языков, dot-path ключей.
|
||||
- [ ] Seed & Reset: двойное подтверждение, reauth-токен, предупреждения.
|
||||
- [ ] Все деструктивные действия с confirm; все флеши; все пустые состояния.
|
||||
- [ ] Работает через Tor (без CDN-зависимостей), тёмная тема.
|
||||
- [ ] Нет утечек seed/private_data/масок в DOM для не-super_admin.
|
||||
- [ ] Нет XSS: все данные экранированы на клиенте (в т.ч. из `JSON` в `<script>`).
|
||||
|
||||
---
|
||||
|
||||
*Документ подготовлен на основе фактического кода: `src/admin/server.js`, `src/admin/routes/*.js`, `src/admin/auth.js`, `src/admin/views/*.ejs`, `src/migrations/*.js`, `src/config/*.js`, `src/services/*.js`. При изменении бэкенда — перегенерировать разделы 6–7.*
|
||||
@@ -6,7 +6,8 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js"
|
||||
"dev": "nodemon src/index.js",
|
||||
"lint": "find src -name '*.js' -print0 | xargs -0 -n1 node --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"archiver": "^7.0.1",
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Sync Agent Models
|
||||
*
|
||||
* Synchronizes agent definitions across:
|
||||
* - kilo.jsonc (Kilo Code official config)
|
||||
* - kilo-meta.json (metadata for sync)
|
||||
* - .kilo/agents/*.md (agent definitions)
|
||||
* - .kilo/KILO_SPEC.md (documentation)
|
||||
* - AGENTS.md (project reference)
|
||||
*
|
||||
* Run: node scripts/sync-agents.js [--check | --fix]
|
||||
*
|
||||
* --check: Report discrepancies without fixing
|
||||
* --fix: Update all files to match kilo-meta.json
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const KILO_JSONC = path.join(ROOT, 'kilo.jsonc');
|
||||
const KILO_META = path.join(ROOT, 'kilo-meta.json');
|
||||
const AGENTS_DIR = path.join(ROOT, '.kilo', 'agents');
|
||||
const KILO_SPEC = path.join(ROOT, '.kilo', 'KILO_SPEC.md');
|
||||
const AGENTS_MD = path.join(ROOT, 'AGENTS.md');
|
||||
|
||||
/**
|
||||
* Load kilo-meta.json (source of truth for sync)
|
||||
*/
|
||||
function loadKiloMeta() {
|
||||
const content = fs.readFileSync(KILO_META, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load kilo.jsonc (Kilo Code config)
|
||||
*/
|
||||
function loadKiloJsonc() {
|
||||
try {
|
||||
const content = fs.readFileSync(KILO_JSONC, 'utf-8');
|
||||
// Remove single-line comments
|
||||
let cleaned = content.replace(/\/\/.*$/gm, '');
|
||||
// Remove multi-line comments
|
||||
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||
// Remove trailing commas before } or ]
|
||||
cleaned = cleaned.replace(/,(\s*[}\]])/g, '$1');
|
||||
return JSON.parse(cleaned);
|
||||
} catch (error) {
|
||||
console.warn('Warning: Could not parse kilo.jsonc:', error.message);
|
||||
console.warn('Skipping kilo.jsonc validation.');
|
||||
return { agent: {} };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract frontmatter from agent md file
|
||||
*/
|
||||
function parseFrontmatter(content) {
|
||||
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!match) return {};
|
||||
|
||||
const frontmatter = {};
|
||||
const lines = match[1].split('\n');
|
||||
let currentKey = null;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith(' ') && currentKey) {
|
||||
// Continuation of multi-line value (like permission)
|
||||
continue;
|
||||
}
|
||||
const colonIndex = line.indexOf(':');
|
||||
if (colonIndex > 0) {
|
||||
const key = line.slice(0, colonIndex).trim();
|
||||
let value = line.slice(colonIndex + 1).trim();
|
||||
|
||||
if (value.startsWith('"') && value.endsWith('"')) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
frontmatter[key] = value;
|
||||
currentKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
return frontmatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update frontmatter in agent md file
|
||||
*/
|
||||
function updateFrontmatter(content, updates) {
|
||||
const match = content.match(/^(---\n[\s\S]*?\n---\n)/);
|
||||
if (!match) return content;
|
||||
|
||||
let frontmatter = match[1];
|
||||
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
const regex = new RegExp(`^${key}:.*$`, 'm');
|
||||
if (regex.test(frontmatter)) {
|
||||
frontmatter = frontmatter.replace(regex, `${key}: ${value}`);
|
||||
} else {
|
||||
frontmatter = frontmatter.replace('---\n', `---\n${key}: ${value}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
return content.replace(match[1], frontmatter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check agent files match kilo-meta.json
|
||||
*/
|
||||
function checkAgents(meta) {
|
||||
const violations = [];
|
||||
|
||||
for (const [name, agent] of Object.entries(meta.agents)) {
|
||||
const filePath = path.join(ROOT, agent.file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
violations.push({
|
||||
type: 'missing-file',
|
||||
agent: name,
|
||||
file: agent.file,
|
||||
message: `Agent file not found: ${agent.file}`
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const frontmatter = parseFrontmatter(content);
|
||||
|
||||
if (frontmatter.model !== agent.model) {
|
||||
violations.push({
|
||||
type: 'model-mismatch',
|
||||
agent: name,
|
||||
file: agent.file,
|
||||
expected: agent.model,
|
||||
actual: frontmatter.model,
|
||||
message: `${name}: expected model ${agent.model}, got ${frontmatter.model}`
|
||||
});
|
||||
}
|
||||
|
||||
if (agent.mode && frontmatter.mode !== agent.mode) {
|
||||
violations.push({
|
||||
type: 'mode-mismatch',
|
||||
agent: name,
|
||||
file: agent.file,
|
||||
expected: agent.mode,
|
||||
actual: frontmatter.mode,
|
||||
message: `${name}: expected mode ${agent.mode}, got ${frontmatter.mode}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check kilo.jsonc matches kilo-meta.json (optional, may fail on JSONC parsing)
|
||||
*/
|
||||
function checkKiloJsonc(meta) {
|
||||
// Skip JSONC validation - it's auto-generated from agent files anyway
|
||||
// The source of truth is in the .md files and kilo-meta.json
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix agent files to match kilo-meta.json
|
||||
*/
|
||||
function fixAgents(meta) {
|
||||
const fixes = [];
|
||||
|
||||
for (const [name, agent] of Object.entries(meta.agents)) {
|
||||
const filePath = path.join(ROOT, agent.file);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
fixes.push({ agent: name, action: 'skipped', reason: 'file not found' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const frontmatter = parseFrontmatter(content);
|
||||
|
||||
const updates = {};
|
||||
if (frontmatter.model !== agent.model) {
|
||||
updates.model = agent.model;
|
||||
}
|
||||
if (agent.mode && frontmatter.mode !== agent.mode) {
|
||||
updates.mode = agent.mode;
|
||||
}
|
||||
if (agent.color && frontmatter.color !== agent.color) {
|
||||
updates.color = agent.color;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
const newContent = updateFrontmatter(content, updates);
|
||||
fs.writeFileSync(filePath, newContent, 'utf-8');
|
||||
fixes.push({
|
||||
agent: name,
|
||||
action: 'updated',
|
||||
updates: Object.keys(updates)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update KILO_SPEC.md tables
|
||||
*/
|
||||
function updateKiloSpec(meta) {
|
||||
let content = fs.readFileSync(KILO_SPEC, 'utf-8');
|
||||
|
||||
// Build agents table
|
||||
const agentRows = Object.entries(meta.agents)
|
||||
.map(([name, agent]) => {
|
||||
const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
||||
return `| \`@${displayName}\` | ${agent.description.split('.')[0]}. | ${agent.model} |`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const agentsTable = `### Pipeline Agents\n\n| Agent | Role | Model |\n|-------|------|-------|\n${agentRows}`;
|
||||
|
||||
// Replace agents section
|
||||
content = content.replace(
|
||||
/### Pipeline Agents\n\n\| Agent \| Role \| Model \|[\s\S]*?(?=\n\n\*\*Note)/,
|
||||
agentsTable + '\n\n'
|
||||
);
|
||||
|
||||
// Build commands table
|
||||
const commandRows = Object.entries(meta.commands)
|
||||
.filter(([_, cmd]) => cmd.model)
|
||||
.map(([name, cmd]) => {
|
||||
return `| \`/${name}\` | ${cmd.description.split('.')[0]}. | ${cmd.model} |`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
const commandsTable = `### Workflow Commands\n\n| Command | Description | Model |\n|---------|-------------|-------|\n${commandRows}`;
|
||||
|
||||
// Replace commands section
|
||||
content = content.replace(
|
||||
/### Workflow Commands\n\n\| Command \| Description \| Model \|[\s\S]*?(?=\n\n###)/,
|
||||
commandsTable + '\n\n'
|
||||
);
|
||||
|
||||
fs.writeFileSync(KILO_SPEC, content, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update AGENTS.md
|
||||
*/
|
||||
function updateAgentsMd(meta) {
|
||||
let content = fs.readFileSync(AGENTS_MD, 'utf-8');
|
||||
|
||||
// Build category tables
|
||||
const categories = {
|
||||
core: '### Core Development',
|
||||
quality: '### Quality Assurance',
|
||||
meta: '### Meta & Process',
|
||||
cognitive: '### Cognitive Enhancement',
|
||||
testing: '### Testing'
|
||||
};
|
||||
|
||||
const triggers = {
|
||||
'requirement-refiner': 'Issue status: new',
|
||||
'history-miner': 'Status: planned',
|
||||
'system-analyst': 'Status: researching',
|
||||
'sdet-engineer': 'Status: designed',
|
||||
'lead-developer': 'Status: testing',
|
||||
'frontend-developer': 'When UI work needed',
|
||||
'backend-developer': 'When backend needed',
|
||||
'go-developer': 'When Go backend needed',
|
||||
'devops-engineer': 'When deployment/infra needed',
|
||||
'code-skeptic': 'Status: implementing',
|
||||
'the-fixer': 'When review fails',
|
||||
'performance-engineer': 'After code-skeptic',
|
||||
'security-auditor': 'After performance',
|
||||
'visual-tester': 'When UI changes',
|
||||
'orchestrator': 'Manages all agent routing',
|
||||
'release-manager': 'Status: releasing',
|
||||
'evaluator': 'Status: evaluated',
|
||||
'prompt-optimizer': 'When score < 7',
|
||||
'product-owner': 'Manages issues',
|
||||
'agent-architect': 'When gaps identified',
|
||||
'capability-analyst': 'When starting new task',
|
||||
'workflow-architect': 'New workflow needed',
|
||||
'markdown-validator': 'Before issue creation',
|
||||
'browser-automation': 'E2E testing needed',
|
||||
'planner': 'Complex tasks',
|
||||
'reflector': 'After each agent',
|
||||
'memory-manager': 'Context management'
|
||||
};
|
||||
|
||||
for (const [cat, heading] of Object.entries(categories)) {
|
||||
const agents = Object.entries(meta.agents)
|
||||
.filter(([_, a]) => a.category === cat)
|
||||
.map(([name, agent]) => {
|
||||
const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
|
||||
return `| \`@${displayName}\` | ${agent.description.split('.')[0]} | ${triggers[name] || 'Manual invocation'} |`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
if (agents) {
|
||||
const table = `${heading}\n| Agent | Role | When Invoked |\n|-------|------|--------------|\n${agents}`;
|
||||
|
||||
const regex = new RegExp(`${heading}[\\s\\S]*?(?=###|$)`);
|
||||
if (regex.test(content)) {
|
||||
content = content.replace(regex, table + '\n\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(AGENTS_MD, content, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update lastSync timestamp
|
||||
*/
|
||||
function updateLastSync(meta) {
|
||||
meta.lastSync = new Date().toISOString();
|
||||
fs.writeFileSync(KILO_META, JSON.stringify(meta, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const checkOnly = args.includes('--check');
|
||||
const fixMode = args.includes('--fix');
|
||||
|
||||
console.log('=== Agent Sync Tool ===\n');
|
||||
console.log('Source of truth: kilo-meta.json\n');
|
||||
|
||||
const meta = loadKiloMeta();
|
||||
|
||||
// Check agents
|
||||
console.log('Checking agent files...');
|
||||
let violations = checkAgents(meta);
|
||||
|
||||
// Check kilo.jsonc
|
||||
console.log('Checking kilo.jsonc...');
|
||||
violations = violations.concat(checkKiloJsonc(meta));
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.log(`\n⚠️ Found ${violations.length} violations:\n`);
|
||||
|
||||
for (const v of violations) {
|
||||
console.log(` [${v.type}] ${v.agent}: ${v.message}`);
|
||||
if (v.expected) {
|
||||
console.log(` Expected: ${v.expected}`);
|
||||
console.log(` Actual: ${v.actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (fixMode) {
|
||||
console.log('\n🔧 Fixing agent files...');
|
||||
const fixes = fixAgents(meta);
|
||||
|
||||
for (const f of fixes) {
|
||||
console.log(` ✓ ${f.agent}: ${f.action} (${f.updates?.join(', ') || 'n/a'})`);
|
||||
}
|
||||
|
||||
console.log('\n📝 Updating KILO_SPEC.md...');
|
||||
updateKiloSpec(meta);
|
||||
console.log(' ✓ KILO_SPEC.md updated');
|
||||
|
||||
console.log('\n📝 Updating AGENTS.md...');
|
||||
updateAgentsMd(meta);
|
||||
console.log(' ✓ AGENTS.md updated');
|
||||
|
||||
updateLastSync(meta);
|
||||
console.log('\n✅ Sync complete!');
|
||||
} else if (checkOnly) {
|
||||
console.log('\n❌ Check failed. Run with --fix to resolve.');
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
console.log('\n✅ All agents in sync!');
|
||||
|
||||
if (fixMode) {
|
||||
updateKiloSpec(meta);
|
||||
updateAgentsMd(meta);
|
||||
updateLastSync(meta);
|
||||
console.log('✅ Documentation updated');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,3 +0,0 @@
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "apaw-web-testing",
|
||||
"name": "telegram-shop-web-testing",
|
||||
"version": "2.0.0",
|
||||
"description": "Web application testing suite for APAW - Visual regression, link checking, form testing, console error detection",
|
||||
"description": "Web application testing suite for Telegram Shop - Visual regression, link checking, form testing, console error detection",
|
||||
"main": "scripts/visual-test-pipeline.js",
|
||||
"scripts": {
|
||||
"test": "node scripts/visual-test-pipeline.js",
|
||||
@@ -19,7 +19,7 @@
|
||||
"playwright",
|
||||
"kilo-code"
|
||||
],
|
||||
"author": "APAW Team",
|
||||
"author": "Telegram Shop Team",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pixelmatch": "^5.3.0",
|
||||
|
||||
Reference in New Issue
Block a user