27 KiB
AWatch-rus Portal GUI Plan
Документ предназначен для следующего агента, в том числе менее сильного. Не надо угадывать архитектуру: идти по фазам, проверять каждый слой, не ломать текущий контур.
Статус На 2026-06-02
Read-only MVP выполнен и развернут:
- Rust crate:
adk-rust/crates/detmir-portal; - production service:
detmir-portal.serviceна Proxmox; - bind:
127.0.0.1:8720; - gateway route:
https://<PUBLIC_GATEWAY_FQDN>/portal/; - API:
/api/health,/api/summary,/api/operator,/api/manager,/api/owner,/api/incidents,/api/links; - UI tabs:
Оператор,Руководитель,Владелец,Инциденты ИБ; - verification: local Rust gates OK, Ansible deploy OK, gateway health OK, Playwright desktop/mobile smoke OK.
Следующий агент не должен начинать MVP заново. Работать дальше от deployed
baseline и раздела Phase 8: Post-MVP Enhancements.
Статус На 2026-06-03
Коммерческий post-MVP слой отчетов выполнен и развернут:
- API:
GET /api/reports; - HTML route:
/reportsи/portal/reports; - UI tab:
Отчеты; - отчет содержит KPI для владельца/руководителя: worktime users, active time, active applications, DLP WARN/FAIL, evidence screenshots/items, open issues;
- отчет содержит read-only UEBA-compatible rule-based risk scoring v1:
score,level,confidence,risk_sources,baseline_status,policy_version,baseline_window_days,user_baseline_available,department_baseline_available,deviation_score,baseline_samples,calculated_from,reasons; - отчет и вкладка
РуководительпоказываютИндекс активностикак proxyактивное время / плановое рабочее время; - отчет поддерживает
Взвешенную активностьпри наличии/etc/detmir-portal-workforce-policy.json; публичный пример лежит вconfigs/detmir-workforce-policy.example.json; - пример policy содержит типовые роли
accountant,operator,developer,admin,manager,sales; - JSON портала возвращает объяснение расчета weighted KPI: активную роль, каталог ролей, planned/app/weighted seconds и matched rule по приложениям;
- вкладки
РуководительиОтчетыпоказывают экранПочему такой индекс?с ролью, формулой, плановым временем, app time, weighted time и top приложениями с весом/правилом/вкладом; - Markdown export отчета включает тот же explainability-блок:
Почему такой индекс?, top приложений, auditdefault_weightи drill-down по сотрудникам; - вкладка
Отчетыимеет действиеПечать / PDF; печатный CSS оставляет отчетные секции и скрывает навигацию; - formula hint закреплен прямо в UI:
index = weighted_seconds / planned_seconds × 100; - policy audit показывает приложения, попавшие под
default_weight, потому что это основной источник ошибок классификации ролей; - employee drill-down показывает персональную причину индекса:
active / plan => index; per-user app-weight breakdown пока невозможен, потому что текущий worktime payload отдает приложения только на уровне портфеля; - employee drill-down явно помечен как
не персональный weighted KPI; - для демо/экспертных PDF и Markdown добавлен режим обезличивания:
?anonymize=1на/api/reportsи/api/workforce/policy/explain, плюс кнопкаДемо без именво вкладкеОтчеты; - UEBA score виден в KPI, отдельной секции отчета, Markdown и PDF; веса
настраиваются в
/etc/detmir-portal-ueba-policy.yaml; evidence используется как confidence, а не как отдельный risk reason; слой не выполняет pfSense/NAC/SOAR actions; - baseline skeleton хранится в state каталоге портала как
ueba-baseline-state.json: per-user и per-department samples копятся поreport_date, а deviation включается только после минимальной истории; - вкладка
Руководительполучает этот блок через легкий endpoint/api/workforce/policy/explain, без загрузки полного/api/reports; - contract легкого endpoint защищен unit-тестом
workforce_policy_explain_is_lightweight_payload; - отчет использует Worktime management snapshot и показывает сравнение подразделений/ответственных за текущий день;
- JSON отчета содержит
workforce.department_comparison,workforce.owner_comparison,workforce.trendиworkforce.trend_status; - daily history для
workforce.trendнакапливается вworktime-apiкак агрегированные trend-points; портал только отображает готовый массив; worktime-apiвозвращаетtrend_insights, а портал показывает их в секцииВыводы Workforce;- insights включают текущую недогрузку/перегрузку, рост/падение тренда, просадку относительно нормы, работу вне рабочего окна и выходные;
- пороги интерпретации задаются customer policy
/etc/activitywatch/worktime-interpretation-policy.json, публичный пример:configs/worktime-interpretation-policy.example.json; trend_status=daily_onlyозначает, что месячный тренд еще нельзя интерпретировать как полноценный отчет; дляmonthly_readyнужна накопленная daily history;- Ansible устанавливает initial workforce policy только если runtime-файл отсутствует, чтобы не перетирать клиентские веса ролей;
- отчет содержит Markdown export для передачи руководителю или заказчику;
- формулировка DLP/case показателей зафиксирована как
derived detections/cases, не как вручную подтвержденные инциденты; - Ansible deploy gate теперь проверяет
/api/reports, наличиеkpisи обязательный disclaimer; - playbook больше не пишет TEST-NET defaults в live env, если в ignored inventory доступны реальные hosts.
Цель
Сделать единый web GUI для работы с контуром AWatch-rus:
- оператор видит техническое состояние и последние проблемы;
- менеджер видит работу сотрудников и отклонения без технических терминов;
- владелец видит короткую управленческую картину: работа, риски, 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 на
<AW_SERVER_HOST>:5610; - ActivityWatch API на
<AW_SERVER_HOST>:5600; - DLP health/case/policy services на AW server;
- 1C analytics API на
<GATEWAY_HOST>:8710; - внешний gateway
https://<PUBLIC_GATEWAY_FQDN>/; - nginx Basic Auth на gateway.
Целевая архитектура MVP
Новый Rust crate:
adk-rust/crates/detmir-portal/
Production binary:
/usr/local/bin/detmir-portal
Production service на Proxmox host:
detmir-portal.service
Bind:
127.0.0.1:8720
External route через существующий nginx gateway:
https://<PUBLIC_GATEWAY_FQDN>/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:
GET /portal/
GET /portal/operator
GET /portal/manager
GET /portal/owner
GET /portal/incidents
JSON API:
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:
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:
{
"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
Единый верхнеуровневый статус:
{
"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;
- AWatch-rus health failures;
- Grafana data failures;
- stale collectors;
- 1C analytics warnings.
Пока без кнопок "закрыть", "назначить", "эскалировать".
Источники Данных
Proxmox local commands
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 можно дополнительно читать:
sudo -n /usr/sbin/pct exec 201 -- cat /var/lib/detmir-grafana-check/latest.json
Если это не работает, не делать repair. Просто показать ошибку.
AW Worktime API
Основной URL:
http://<AW_SERVER_HOST>:5610/reports/worktime/today
Правило:
Connection: close;- timeout 10-15 секунд;
- не кешировать пустой отчет как успешный;
- если API временно не ответил, показать stale/source error.
DLP
Минимум:
ssh aw-server 'sudo -n /usr/local/bin/dlp-health-check --json'
Лучше после MVP сделать HTTP client к case/policy APIs, но не обязательно в первом проходе.
1C Analytics
Минимум:
http://<GATEWAY_HOST>:8710/api/health
http://<GATEWAY_HOST>:8710/manager/brief
http://<GATEWAY_HOST>: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 секунд понять, жив ли контур.
Блоки:
-
Верхняя строка:
- общий статус;
- время последней проверки;
ok_for_operator;- кнопка-ссылка "Открыть Grafana";
- кнопка-ссылка "Открыть AW".
-
Health grid:
- AWatch-rus;
- ActivityWatch;
- Worktime API;
- Grafana Data;
- DLP;
- 1C.
-
Data freshness:
- buckets OK/STALE/DEAD;
- Grafana freshness;
- DLP counters.
-
Problems:
- список WARN/FAIL;
- для каждого: источник, текст, время, ссылка.
-
Deep links:
- AWatch-rus ActivityWatch dashboard;
- Worktime report;
- AW UI;
- Grafana dashboards;
- 1C brief.
MVP без кнопок restart/heal.
Экран 2: Manager View
Цель: понять работу сотрудников без технических терминов.
Блоки:
-
Сегодня:
- всего активного времени;
- число пользователей;
- последний сбор данных.
-
Сотрудники:
- имя;
- активное время;
- последнее действие;
- основные приложения;
- статус данных.
-
Приложения:
- 1С;
- браузер;
- Проводник;
- прочие.
-
Отклонения:
- нет данных;
- неактивен;
- слишком старый сбор;
- DLP signal.
Не показывать:
- bucket ids;
- raw JSON;
- systemd unit names;
- stack traces.
Экран 3: Owner View
Цель: дать владельцу управленческий ответ, а не технический dashboard.
Блоки:
-
"Компания сегодня":
- работа идет / есть сбои / есть риски.
-
"Люди и работа":
- активность;
- заметные отклонения;
- кто требует внимания.
-
"Безопасность":
- DLP OK/WARN/FAIL;
- открытые кейсы;
- критичные сработки.
-
"1С и финансы":
- health 1C analytics;
- ссылка на financial/reporting board;
- ссылка на actions.
-
"Что сделать":
- 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
Команды:
cd <PROJECT_ROOT>
git status --short
export CARGO_TARGET_DIR=<OPERATOR_HOME>/.cache/detmir-adk-rust-target
cd ansible
export no_proxy='localhost,127.0.0.1,<WINDOWS_HOST>,<AW_SERVER_HOST>,<GATEWAY_HOST>,<SERVER_SUBNET_CIDR>,<ENDPOINT_SUBNET_CIDR>'
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:
- рабочее дерево понятно;
- текущий AWatch-rus не сломан;
- нет попытки чинить unrelated проблемы.
Phase 1: Create Rust Crate
Files:
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:
detmir-portal --bind 127.0.0.1:8720
detmir-portal --bind 127.0.0.1:8720 --json-smoke
detmir-portal --help
Cargo deps:
anyhow.workspace = true
chrono.workspace = true
clap.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tiny_http.workspace = true
Acceptance:
cd adk-rust
cargo fmt --all -- --check
CARGO_TARGET_DIR=<OPERATOR_HOME>/.cache/detmir-adk-rust-target cargo test -p detmir-portal
CARGO_TARGET_DIR=<OPERATOR_HOME>/.cache/detmir-adk-rust-target cargo clippy -p detmir-portal --all-targets -- -D warnings
CARGO_TARGET_DIR=<OPERATOR_HOME>/.cache/detmir-adk-rust-target cargo build --release -p detmir-portal
Phase 2: Backend Aggregation
Implement typed structs:
PortalSummary
PortalBlock
OperatorView
ManagerView
OwnerView
IncidentItem
SourceStatus
Implement source functions:
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:
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:
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:
1440x1000
Mobile viewport:
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:
.playwright-cli/
Do not commit screenshots unless explicitly requested.
Phase 5: Deployment
Add playbook:
ansible/deploy_detmir_portal.yml
Deploy to Proxmox host:
/usr/local/bin/detmir-portal
/etc/systemd/system/detmir-portal.service
Service:
[Unit]
Description=AWatch-rus 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:
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:
systemctl is-active detmir-portal
curl -fsS http://127.0.0.1:8720/api/health
curl -k -I -H 'Host: <PUBLIC_GATEWAY_FQDN>' https://127.0.0.1/portal/
External:
https://<PUBLIC_GATEWAY_FQDN>/portal/
Phase 6: Integrate Into Health Gates
Add detmir-portal-check only after MVP is stable, or add a mode inside
detmir-portal:
detmir-portal --check
It should verify:
- service can aggregate all required sources;
/api/healthis OK;- HTML includes role tabs;
- gateway route returns 401 without auth or 200 with auth.
Then add artifact requirement:
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:
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:
- role-aware views based on gateway username;
- incident comments - done for incident action metadata;
- acknowledge/assign incident - done with audit log;
- safe "run check now";
- safe "open Telegram status";
- PDF/HTML daily owner report - partially done as
/api/reportsplus portal Markdown export; PDF/HTML file generation remains future work; - historical trends;
- AI summary with strict source citations;
- action buttons with explicit allowlist and audit log.
Definition Of Done For MVP
MVP is done only when all are true:
detmir-portalcrate 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://<PUBLIC_GATEWAY_FQDN>/portal/; - browser screenshots checked desktop and mobile;
- no secrets in HTML/JSON/journald;
detmir-statusstays OK after deployment;- runbook updated;
- git commit pushed.
Recommended First Implementation Slice
Do not start with all screens. First slice:
- create
detmir-portalcrate; - implement
/api/health; - implement
/api/summary; - implement one static
/page with four tabs but only Operator tab filled; - run local curl tests;
- deploy service internally on Proxmox;
- add
/portal/gateway route; - browser-test external URL;
- only then fill Manager and Owner views.
This keeps risk low and gives a visible result quickly.