docs(detmir): plan operator portal gui

This commit is contained in:
igor04091968
2026-06-02 20:17:47 +03:00
parent f513836396
commit ac2901c0dc
2 changed files with 805 additions and 0 deletions
+3
View File
@@ -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 остается постоянным
runtime, Rust используется только для backend helpers;
- перенос оставшихся install/runtime scripts на Rust;
+802
View File
@@ -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.