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 остается постоянным
|
||||
runtime, Rust используется только для backend helpers;
|
||||
- перенос оставшихся 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