docs(detmir): plan operator portal gui
This commit is contained in:
@@ -1549,6 +1549,9 @@ systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg
|
|||||||
|
|
||||||
Отложить:
|
Отложить:
|
||||||
|
|
||||||
|
- новый этап: сделать `detmir-portal` - единый read-only Rust web portal для
|
||||||
|
оператора, руководителя и владельца. Детальный план:
|
||||||
|
`docs/DETMIR_PORTAL_GUI_PLAN_RU.md`;
|
||||||
- перенос Telegram bot runtime снят с плана: Python остается постоянным
|
- перенос Telegram bot runtime снят с плана: Python остается постоянным
|
||||||
runtime, Rust используется только для backend helpers;
|
runtime, Rust используется только для backend helpers;
|
||||||
- перенос оставшихся install/runtime scripts на Rust;
|
- перенос оставшихся install/runtime scripts на Rust;
|
||||||
|
|||||||
@@ -0,0 +1,802 @@
|
|||||||
|
# DetMir Portal GUI Plan
|
||||||
|
|
||||||
|
Документ предназначен для следующего агента, в том числе менее сильного. Не
|
||||||
|
надо угадывать архитектуру: идти по фазам, проверять каждый слой, не ломать
|
||||||
|
текущий контур.
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Сделать единый web GUI для работы с контуром DetMir:
|
||||||
|
|
||||||
|
- оператор видит техническое состояние и последние проблемы;
|
||||||
|
- менеджер видит работу сотрудников и отклонения без технических терминов;
|
||||||
|
- владелец видит короткую управленческую картину: работа, риски, 1С, ИБ,
|
||||||
|
проблемные зоны;
|
||||||
|
- текущие Grafana/AW/DLP/1C экраны не удаляются, а становятся источниками и
|
||||||
|
deep links;
|
||||||
|
- первый production MVP только read-only.
|
||||||
|
|
||||||
|
## Жесткие ограничения
|
||||||
|
|
||||||
|
- pfSense не трогать.
|
||||||
|
- Telegram runtime остается Python.
|
||||||
|
- Не менять маршрутизацию, DNS, VPN, NAT и default routes.
|
||||||
|
- Не удалять Grafana dashboards, AW DB, SQLite DB, backups.
|
||||||
|
- Не добавлять write/action endpoints в первом MVP.
|
||||||
|
- Не выводить секреты в HTML, JSON, journald, git diff.
|
||||||
|
- Любая кнопка с мутацией только после отдельной фазы и отдельной проверки.
|
||||||
|
- Портал должен работать автономно на сервере, без зависимости от ноутбука.
|
||||||
|
|
||||||
|
## Текущий фундамент
|
||||||
|
|
||||||
|
Уже есть зеленые источники, которые портал должен использовать:
|
||||||
|
|
||||||
|
- `detmir-status --json` на Proxmox;
|
||||||
|
- `detmir-check --json` на Proxmox, включая `grafana-data`;
|
||||||
|
- `detmir-grafana-check` в Grafana CT 201;
|
||||||
|
- AW Worktime API на `10.10.10.13:5610`;
|
||||||
|
- ActivityWatch API на `10.10.10.13:5600`;
|
||||||
|
- DLP health/case/policy services на AW server;
|
||||||
|
- 1C analytics API на `10.10.10.2:8710`;
|
||||||
|
- внешний gateway `https://dm.iri1968.dpdns.org/`;
|
||||||
|
- nginx Basic Auth на gateway.
|
||||||
|
|
||||||
|
## Целевая архитектура MVP
|
||||||
|
|
||||||
|
Новый Rust crate:
|
||||||
|
|
||||||
|
```text
|
||||||
|
adk-rust/crates/detmir-portal/
|
||||||
|
```
|
||||||
|
|
||||||
|
Production binary:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/local/bin/detmir-portal
|
||||||
|
```
|
||||||
|
|
||||||
|
Production service на Proxmox host:
|
||||||
|
|
||||||
|
```text
|
||||||
|
detmir-portal.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Bind:
|
||||||
|
|
||||||
|
```text
|
||||||
|
127.0.0.1:8720
|
||||||
|
```
|
||||||
|
|
||||||
|
External route через существующий nginx gateway:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://dm.iri1968.dpdns.org/portal/
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему Proxmox host:
|
||||||
|
|
||||||
|
- там уже живут `detmir-status`, `detmir-check`, `detmir-auto`;
|
||||||
|
- там есть доступ к `pct exec 201` для Grafana-check artifact;
|
||||||
|
- там уже стоит nginx gateway;
|
||||||
|
- не нужна новая CT/platform операция.
|
||||||
|
|
||||||
|
## Rust Stack
|
||||||
|
|
||||||
|
Предпочтение: использовать текущий стиль проекта.
|
||||||
|
|
||||||
|
Минимальный MVP:
|
||||||
|
|
||||||
|
- `tiny_http` для HTTP server, как в `worktime-api`, `dlp-policy-engine`,
|
||||||
|
`dlp-case-management`;
|
||||||
|
- `reqwest blocking` для HTTP к AW/Grafana/1C/DLP;
|
||||||
|
- `serde`/`serde_json` для typed models;
|
||||||
|
- static HTML/CSS/JS встроить в binary через `include_str!`;
|
||||||
|
- без npm/build pipeline на первом этапе.
|
||||||
|
|
||||||
|
Не использовать на MVP:
|
||||||
|
|
||||||
|
- React/Vite/Next;
|
||||||
|
- отдельный frontend build;
|
||||||
|
- database для портала;
|
||||||
|
- websocket;
|
||||||
|
- сложный RBAC;
|
||||||
|
- write actions.
|
||||||
|
|
||||||
|
## Основные URL Портала
|
||||||
|
|
||||||
|
HTML:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /portal/
|
||||||
|
GET /portal/operator
|
||||||
|
GET /portal/manager
|
||||||
|
GET /portal/owner
|
||||||
|
GET /portal/incidents
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON API:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /portal/api/health
|
||||||
|
GET /portal/api/summary
|
||||||
|
GET /portal/api/operator
|
||||||
|
GET /portal/api/manager
|
||||||
|
GET /portal/api/owner
|
||||||
|
GET /portal/api/incidents
|
||||||
|
GET /portal/api/links
|
||||||
|
```
|
||||||
|
|
||||||
|
Service-local direct URLs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /
|
||||||
|
GET /operator
|
||||||
|
GET /manager
|
||||||
|
GET /owner
|
||||||
|
GET /incidents
|
||||||
|
GET /api/health
|
||||||
|
GET /api/summary
|
||||||
|
```
|
||||||
|
|
||||||
|
Портал должен корректно работать за prefix `/portal/`. Не хардкодить абсолютные
|
||||||
|
пути вида `/api/...` в JS; использовать относительные `api/...` или вычислять
|
||||||
|
base path.
|
||||||
|
|
||||||
|
## Data Contract
|
||||||
|
|
||||||
|
### `/api/health`
|
||||||
|
|
||||||
|
Минимальный JSON:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"generated_at_utc": "2026-06-02T18:00:00Z",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"sources": {
|
||||||
|
"detmir_status": true,
|
||||||
|
"detmir_check": true,
|
||||||
|
"grafana_check": true,
|
||||||
|
"worktime_api": true,
|
||||||
|
"dlp_health": true,
|
||||||
|
"one_c": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/api/summary`
|
||||||
|
|
||||||
|
Единый верхнеуровневый статус:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"severity": "OK",
|
||||||
|
"operator_ok": true,
|
||||||
|
"headline": "Контур работает штатно",
|
||||||
|
"generated_at_utc": "2026-06-02T18:00:00Z",
|
||||||
|
"blocks": {
|
||||||
|
"collection": {"status": "OK", "text": "Данные свежие"},
|
||||||
|
"grafana": {"status": "OK", "text": "7 панелей, данные актуальны"},
|
||||||
|
"dlp": {"status": "OK", "text": "22 проверки OK"},
|
||||||
|
"worktime": {"status": "OK", "text": "Есть данные за сегодня"},
|
||||||
|
"one_c": {"status": "OK", "text": "API 1C analytics отвечает"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `/api/operator`
|
||||||
|
|
||||||
|
Для оператора:
|
||||||
|
|
||||||
|
- `detmir_status`;
|
||||||
|
- `detmir_check.summary`;
|
||||||
|
- `grafana_data`;
|
||||||
|
- failed units count;
|
||||||
|
- freshness по buckets;
|
||||||
|
- последние WARN/FAIL;
|
||||||
|
- ссылки на Grafana/AW/worktime.
|
||||||
|
|
||||||
|
### `/api/manager`
|
||||||
|
|
||||||
|
Для менеджера:
|
||||||
|
|
||||||
|
- сотрудники за сегодня;
|
||||||
|
- активное время;
|
||||||
|
- доказанная работа;
|
||||||
|
- приложения;
|
||||||
|
- последние действия;
|
||||||
|
- простые отклонения:
|
||||||
|
- данных нет;
|
||||||
|
- данные устарели;
|
||||||
|
- активность ниже ожидаемой;
|
||||||
|
- слишком много событий DLP.
|
||||||
|
|
||||||
|
Источник: сначала AW Worktime API `/reports/worktime/today`. Не ходить напрямую
|
||||||
|
в SQLite.
|
||||||
|
|
||||||
|
### `/api/owner`
|
||||||
|
|
||||||
|
Для владельца:
|
||||||
|
|
||||||
|
- 4-6 крупных карточек:
|
||||||
|
- "Работа сегодня";
|
||||||
|
- "Риски ИБ";
|
||||||
|
- "1С / финансы";
|
||||||
|
- "Сбор данных";
|
||||||
|
- "Инциденты";
|
||||||
|
- "Что требует внимания";
|
||||||
|
- короткие выводы в человеческом языке;
|
||||||
|
- links на глубокие Grafana/1C/AW страницы.
|
||||||
|
|
||||||
|
### `/api/incidents`
|
||||||
|
|
||||||
|
MVP read-only список:
|
||||||
|
|
||||||
|
- DLP incidents/cases;
|
||||||
|
- DetMir health failures;
|
||||||
|
- Grafana data failures;
|
||||||
|
- stale collectors;
|
||||||
|
- 1C analytics warnings.
|
||||||
|
|
||||||
|
Пока без кнопок "закрыть", "назначить", "эскалировать".
|
||||||
|
|
||||||
|
## Источники Данных
|
||||||
|
|
||||||
|
### Proxmox local commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
detmir-status --json
|
||||||
|
detmir-check --json
|
||||||
|
systemctl --failed --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Правило:
|
||||||
|
|
||||||
|
- command timeout максимум 10 секунд;
|
||||||
|
- ошибка источника не должна валить HTTP server;
|
||||||
|
- ошибка источника должна попасть в JSON как `status: "FAIL"` или
|
||||||
|
`source_error`.
|
||||||
|
|
||||||
|
### Grafana check artifact
|
||||||
|
|
||||||
|
Через `detmir-check` уже есть агрегированный `grafana-data`.
|
||||||
|
|
||||||
|
Для подробного operator view можно дополнительно читать:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo -n /usr/sbin/pct exec 201 -- cat /var/lib/detmir-grafana-check/latest.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Если это не работает, не делать repair. Просто показать ошибку.
|
||||||
|
|
||||||
|
### AW Worktime API
|
||||||
|
|
||||||
|
Основной URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://10.10.10.13:5610/reports/worktime/today
|
||||||
|
```
|
||||||
|
|
||||||
|
Правило:
|
||||||
|
|
||||||
|
- `Connection: close`;
|
||||||
|
- timeout 10-15 секунд;
|
||||||
|
- не кешировать пустой отчет как успешный;
|
||||||
|
- если API временно не ответил, показать stale/source error.
|
||||||
|
|
||||||
|
### DLP
|
||||||
|
|
||||||
|
Минимум:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh aw-server 'sudo -n /usr/local/bin/dlp-health-check --json'
|
||||||
|
```
|
||||||
|
|
||||||
|
Лучше после MVP сделать HTTP client к case/policy APIs, но не обязательно в
|
||||||
|
первом проходе.
|
||||||
|
|
||||||
|
### 1C Analytics
|
||||||
|
|
||||||
|
Минимум:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://10.10.10.2:8710/api/health
|
||||||
|
http://10.10.10.2:8710/manager/brief
|
||||||
|
http://10.10.10.2:8710/manager/actions
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `/manager/brief` HTML, для MVP не парсить его глубоко. Дать link и health
|
||||||
|
card.
|
||||||
|
|
||||||
|
## UI Правила
|
||||||
|
|
||||||
|
Это рабочий портал, не landing page.
|
||||||
|
|
||||||
|
- Первый экран сразу показывает состояние контура.
|
||||||
|
- Без маркетингового hero.
|
||||||
|
- Без декоративных gradient/orb/background.
|
||||||
|
- Не делать карточки внутри карточек.
|
||||||
|
- Dense, спокойный, операционный интерфейс.
|
||||||
|
- Использовать 8px radius или меньше для cards/panels.
|
||||||
|
- Текст не должен вылезать из блоков.
|
||||||
|
- Цвета статусов:
|
||||||
|
- OK: зеленый;
|
||||||
|
- WARN: желтый/янтарный;
|
||||||
|
- FAIL: красный;
|
||||||
|
- UNKNOWN: серый.
|
||||||
|
- Не использовать одну сплошную синюю/фиолетовую палитру.
|
||||||
|
- Главные роли должны быть tabs/segmented control:
|
||||||
|
- Оператор;
|
||||||
|
- Руководитель;
|
||||||
|
- Владелец;
|
||||||
|
- Инциденты.
|
||||||
|
- Иконки можно использовать inline SVG только если нет frontend dependency.
|
||||||
|
Если позже появится frontend dependency, использовать lucide icons.
|
||||||
|
|
||||||
|
## Экран 1: Operator Console
|
||||||
|
|
||||||
|
Цель: за 10 секунд понять, жив ли контур.
|
||||||
|
|
||||||
|
Блоки:
|
||||||
|
|
||||||
|
1. Верхняя строка:
|
||||||
|
- общий статус;
|
||||||
|
- время последней проверки;
|
||||||
|
- `ok_for_operator`;
|
||||||
|
- кнопка-ссылка "Открыть Grafana";
|
||||||
|
- кнопка-ссылка "Открыть AW".
|
||||||
|
|
||||||
|
2. Health grid:
|
||||||
|
- DetMir;
|
||||||
|
- ActivityWatch;
|
||||||
|
- Worktime API;
|
||||||
|
- Grafana Data;
|
||||||
|
- DLP;
|
||||||
|
- 1C.
|
||||||
|
|
||||||
|
3. Data freshness:
|
||||||
|
- buckets OK/STALE/DEAD;
|
||||||
|
- Grafana freshness;
|
||||||
|
- DLP counters.
|
||||||
|
|
||||||
|
4. Problems:
|
||||||
|
- список WARN/FAIL;
|
||||||
|
- для каждого: источник, текст, время, ссылка.
|
||||||
|
|
||||||
|
5. Deep links:
|
||||||
|
- DetMir ActivityWatch dashboard;
|
||||||
|
- Worktime report;
|
||||||
|
- AW UI;
|
||||||
|
- Grafana dashboards;
|
||||||
|
- 1C brief.
|
||||||
|
|
||||||
|
MVP без кнопок restart/heal.
|
||||||
|
|
||||||
|
## Экран 2: Manager View
|
||||||
|
|
||||||
|
Цель: понять работу сотрудников без технических терминов.
|
||||||
|
|
||||||
|
Блоки:
|
||||||
|
|
||||||
|
1. Сегодня:
|
||||||
|
- всего активного времени;
|
||||||
|
- число пользователей;
|
||||||
|
- последний сбор данных.
|
||||||
|
|
||||||
|
2. Сотрудники:
|
||||||
|
- имя;
|
||||||
|
- активное время;
|
||||||
|
- последнее действие;
|
||||||
|
- основные приложения;
|
||||||
|
- статус данных.
|
||||||
|
|
||||||
|
3. Приложения:
|
||||||
|
- 1С;
|
||||||
|
- браузер;
|
||||||
|
- Проводник;
|
||||||
|
- прочие.
|
||||||
|
|
||||||
|
4. Отклонения:
|
||||||
|
- нет данных;
|
||||||
|
- неактивен;
|
||||||
|
- слишком старый сбор;
|
||||||
|
- DLP signal.
|
||||||
|
|
||||||
|
Не показывать:
|
||||||
|
|
||||||
|
- bucket ids;
|
||||||
|
- raw JSON;
|
||||||
|
- systemd unit names;
|
||||||
|
- stack traces.
|
||||||
|
|
||||||
|
## Экран 3: Owner View
|
||||||
|
|
||||||
|
Цель: дать владельцу управленческий ответ, а не технический dashboard.
|
||||||
|
|
||||||
|
Блоки:
|
||||||
|
|
||||||
|
1. "Компания сегодня":
|
||||||
|
- работа идет / есть сбои / есть риски.
|
||||||
|
|
||||||
|
2. "Люди и работа":
|
||||||
|
- активность;
|
||||||
|
- заметные отклонения;
|
||||||
|
- кто требует внимания.
|
||||||
|
|
||||||
|
3. "Безопасность":
|
||||||
|
- DLP OK/WARN/FAIL;
|
||||||
|
- открытые кейсы;
|
||||||
|
- критичные сработки.
|
||||||
|
|
||||||
|
4. "1С и финансы":
|
||||||
|
- health 1C analytics;
|
||||||
|
- ссылка на financial/reporting board;
|
||||||
|
- ссылка на actions.
|
||||||
|
|
||||||
|
5. "Что сделать":
|
||||||
|
- 3-5 коротких рекомендаций.
|
||||||
|
|
||||||
|
Рекомендации в MVP должны быть rule-based, не AI:
|
||||||
|
|
||||||
|
- если `ok_for_operator=false`: "Проверить технический контур";
|
||||||
|
- если Grafana stale: "Обновить/проверить Grafana data pipeline";
|
||||||
|
- если DLP fail: "Открыть DLP обзор";
|
||||||
|
- если worktime rows empty: "Проверить RDP collectors".
|
||||||
|
|
||||||
|
## Экран 4: Incidents
|
||||||
|
|
||||||
|
MVP read-only.
|
||||||
|
|
||||||
|
Таблица:
|
||||||
|
|
||||||
|
- статус;
|
||||||
|
- тип;
|
||||||
|
- источник;
|
||||||
|
- краткое описание;
|
||||||
|
- время;
|
||||||
|
- ссылка.
|
||||||
|
|
||||||
|
Типы:
|
||||||
|
|
||||||
|
- `health`;
|
||||||
|
- `grafana`;
|
||||||
|
- `dlp`;
|
||||||
|
- `worktime`;
|
||||||
|
- `one_c`;
|
||||||
|
- `collector`.
|
||||||
|
|
||||||
|
Нельзя делать в MVP:
|
||||||
|
|
||||||
|
- закрытие инцидента;
|
||||||
|
- изменение severity;
|
||||||
|
- отправка в Telegram;
|
||||||
|
- запуск heal.
|
||||||
|
|
||||||
|
## Phase 0: Baseline Before Coding
|
||||||
|
|
||||||
|
Команды:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
||||||
|
git status --short
|
||||||
|
export CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target
|
||||||
|
cd ansible
|
||||||
|
export no_proxy='localhost,127.0.0.1,192.168.100.18,10.10.10.13,10.10.10.2,10.10.10.0/24,192.168.100.0/24'
|
||||||
|
export NO_PROXY="$no_proxy"
|
||||||
|
ansible proxmox -i inventory.ini -m shell -a 'detmir-status --json'
|
||||||
|
ansible proxmox -i inventory.ini -m shell -a 'detmir-check --json'
|
||||||
|
```
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- рабочее дерево понятно;
|
||||||
|
- текущий DetMir не сломан;
|
||||||
|
- нет попытки чинить unrelated проблемы.
|
||||||
|
|
||||||
|
## Phase 1: Create Rust Crate
|
||||||
|
|
||||||
|
Files:
|
||||||
|
|
||||||
|
```text
|
||||||
|
adk-rust/Cargo.toml
|
||||||
|
adk-rust/crates/detmir-portal/Cargo.toml
|
||||||
|
adk-rust/crates/detmir-portal/src/main.rs
|
||||||
|
adk-rust/crates/detmir-portal/src/static/index.html
|
||||||
|
adk-rust/crates/detmir-portal/src/static/app.css
|
||||||
|
adk-rust/crates/detmir-portal/src/static/app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Minimum CLI:
|
||||||
|
|
||||||
|
```text
|
||||||
|
detmir-portal --bind 127.0.0.1:8720
|
||||||
|
detmir-portal --bind 127.0.0.1:8720 --json-smoke
|
||||||
|
detmir-portal --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Cargo deps:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
anyhow.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
tiny_http.workspace = true
|
||||||
|
```
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd adk-rust
|
||||||
|
cargo fmt --all -- --check
|
||||||
|
CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target cargo test -p detmir-portal
|
||||||
|
CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target cargo clippy -p detmir-portal --all-targets -- -D warnings
|
||||||
|
CARGO_TARGET_DIR=/home/igor/.cache/detmir-adk-rust-target cargo build --release -p detmir-portal
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 2: Backend Aggregation
|
||||||
|
|
||||||
|
Implement typed structs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
PortalSummary
|
||||||
|
PortalBlock
|
||||||
|
OperatorView
|
||||||
|
ManagerView
|
||||||
|
OwnerView
|
||||||
|
IncidentItem
|
||||||
|
SourceStatus
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement source functions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
read_detmir_status()
|
||||||
|
read_detmir_check()
|
||||||
|
read_grafana_check()
|
||||||
|
fetch_worktime_today()
|
||||||
|
fetch_one_c_health()
|
||||||
|
fetch_dlp_health()
|
||||||
|
build_incidents()
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- every source has timeout;
|
||||||
|
- every source returns typed `SourceStatus`;
|
||||||
|
- never panic on bad source JSON;
|
||||||
|
- never expose credentials;
|
||||||
|
- if one source fails, portal still responds with degraded status.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
detmir-portal --json-smoke
|
||||||
|
curl -fsS http://127.0.0.1:8720/api/health | jq .
|
||||||
|
curl -fsS http://127.0.0.1:8720/api/summary | jq .
|
||||||
|
curl -fsS http://127.0.0.1:8720/api/operator | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 3: Static UI MVP
|
||||||
|
|
||||||
|
Implement one HTML app with tabs.
|
||||||
|
|
||||||
|
Required visible text:
|
||||||
|
|
||||||
|
- "Оператор";
|
||||||
|
- "Руководитель";
|
||||||
|
- "Владелец";
|
||||||
|
- "Инциденты";
|
||||||
|
- "Контур";
|
||||||
|
- "Данные";
|
||||||
|
- "Риски";
|
||||||
|
- "Работа сегодня".
|
||||||
|
|
||||||
|
Required behavior:
|
||||||
|
|
||||||
|
- load `/api/summary`;
|
||||||
|
- load active tab API;
|
||||||
|
- show loading state;
|
||||||
|
- show source error state;
|
||||||
|
- refresh every 60 seconds;
|
||||||
|
- links open existing systems.
|
||||||
|
|
||||||
|
No build step.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsS http://127.0.0.1:8720/ | grep -F 'Оператор'
|
||||||
|
curl -fsS http://127.0.0.1:8720/ | grep -F 'Руководитель'
|
||||||
|
curl -fsS http://127.0.0.1:8720/ | grep -F 'Владелец'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 4: Browser Verification
|
||||||
|
|
||||||
|
Use Playwright only after service works by curl.
|
||||||
|
|
||||||
|
Desktop viewport:
|
||||||
|
|
||||||
|
```text
|
||||||
|
1440x1000
|
||||||
|
```
|
||||||
|
|
||||||
|
Mobile viewport:
|
||||||
|
|
||||||
|
```text
|
||||||
|
390x844
|
||||||
|
```
|
||||||
|
|
||||||
|
Check:
|
||||||
|
|
||||||
|
- page is nonblank;
|
||||||
|
- no text overlap;
|
||||||
|
- tabs work;
|
||||||
|
- all API requests return 200;
|
||||||
|
- no console errors;
|
||||||
|
- status and cards visible;
|
||||||
|
- external links are present.
|
||||||
|
|
||||||
|
Save screenshots under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.playwright-cli/
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not commit screenshots unless explicitly requested.
|
||||||
|
|
||||||
|
## Phase 5: Deployment
|
||||||
|
|
||||||
|
Add playbook:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ansible/deploy_detmir_portal.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy to Proxmox host:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/local/bin/detmir-portal
|
||||||
|
/etc/systemd/system/detmir-portal.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Service:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=DetMir Operator Portal
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
EnvironmentFile=-/etc/detmir-portal.env
|
||||||
|
ExecStart=/usr/local/bin/detmir-portal --bind 127.0.0.1:8720
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Nginx route:
|
||||||
|
|
||||||
|
```text
|
||||||
|
location /portal/ {
|
||||||
|
proxy_pass http://127.0.0.1:8720/;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- preserve existing Basic Auth;
|
||||||
|
- do not bypass gateway auth;
|
||||||
|
- do not expose raw internal ports publicly.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl is-active detmir-portal
|
||||||
|
curl -fsS http://127.0.0.1:8720/api/health
|
||||||
|
curl -k -I -H 'Host: dm.iri1968.dpdns.org' https://127.0.0.1/portal/
|
||||||
|
```
|
||||||
|
|
||||||
|
External:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://dm.iri1968.dpdns.org/portal/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Phase 6: Integrate Into Health Gates
|
||||||
|
|
||||||
|
Add `detmir-portal-check` only after MVP is stable, or add a mode inside
|
||||||
|
`detmir-portal`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
detmir-portal --check
|
||||||
|
```
|
||||||
|
|
||||||
|
It should verify:
|
||||||
|
|
||||||
|
- service can aggregate all required sources;
|
||||||
|
- `/api/health` is OK;
|
||||||
|
- HTML includes role tabs;
|
||||||
|
- gateway route returns 401 without auth or 200 with auth.
|
||||||
|
|
||||||
|
Then add artifact requirement:
|
||||||
|
|
||||||
|
```text
|
||||||
|
scripts/check_detmir_rust_release_artifacts.sh -> detmir-portal
|
||||||
|
```
|
||||||
|
|
||||||
|
Add required service check to `detmir-check` only after production service is
|
||||||
|
stable for at least one run.
|
||||||
|
|
||||||
|
## Phase 7: Rollback
|
||||||
|
|
||||||
|
Rollback must be simple:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable --now detmir-portal.service
|
||||||
|
sudo rm -f /usr/local/bin/detmir-portal
|
||||||
|
sudo rm -f /etc/systemd/system/detmir-portal.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
If nginx was changed:
|
||||||
|
|
||||||
|
- keep backup before edit;
|
||||||
|
- restore previous gateway config;
|
||||||
|
- `nginx -t`;
|
||||||
|
- `systemctl reload nginx`.
|
||||||
|
|
||||||
|
Never rollback by deleting unrelated gateway routes.
|
||||||
|
|
||||||
|
## Phase 8: Post-MVP Enhancements
|
||||||
|
|
||||||
|
Only after read-only portal is stable:
|
||||||
|
|
||||||
|
1. role-aware views based on gateway username;
|
||||||
|
2. incident comments;
|
||||||
|
3. acknowledge/assign incident;
|
||||||
|
4. safe "run check now";
|
||||||
|
5. safe "open Telegram status";
|
||||||
|
6. PDF/HTML daily owner report;
|
||||||
|
7. historical trends;
|
||||||
|
8. AI summary with strict source citations;
|
||||||
|
9. action buttons with explicit allowlist and audit log.
|
||||||
|
|
||||||
|
## Definition Of Done For MVP
|
||||||
|
|
||||||
|
MVP is done only when all are true:
|
||||||
|
|
||||||
|
- `detmir-portal` crate exists and builds in release;
|
||||||
|
- unit tests pass;
|
||||||
|
- clippy passes with `-D warnings`;
|
||||||
|
- portal serves HTML and JSON locally;
|
||||||
|
- portal deployed as systemd service on Proxmox;
|
||||||
|
- gateway URL works:
|
||||||
|
`https://dm.iri1968.dpdns.org/portal/`;
|
||||||
|
- browser screenshots checked desktop and mobile;
|
||||||
|
- no secrets in HTML/JSON/journald;
|
||||||
|
- `detmir-status` stays OK after deployment;
|
||||||
|
- runbook updated;
|
||||||
|
- git commit pushed.
|
||||||
|
|
||||||
|
## Recommended First Implementation Slice
|
||||||
|
|
||||||
|
Do not start with all screens. First slice:
|
||||||
|
|
||||||
|
1. create `detmir-portal` crate;
|
||||||
|
2. implement `/api/health`;
|
||||||
|
3. implement `/api/summary`;
|
||||||
|
4. implement one static `/` page with four tabs but only Operator tab filled;
|
||||||
|
5. run local curl tests;
|
||||||
|
6. deploy service internally on Proxmox;
|
||||||
|
7. add `/portal/` gateway route;
|
||||||
|
8. browser-test external URL;
|
||||||
|
9. only then fill Manager and Owner views.
|
||||||
|
|
||||||
|
This keeps risk low and gives a visible result quickly.
|
||||||
Reference in New Issue
Block a user