# 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 приложений, audit `default_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:

```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://<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:

```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;
- AWatch-rus 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://<AW_SERVER_HOST>: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://<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 секунд понять, жив ли контур.

Блоки:

1. Верхняя строка:
   - общий статус;
   - время последней проверки;
   - `ok_for_operator`;
   - кнопка-ссылка "Открыть Grafana";
   - кнопка-ссылка "Открыть AW".

2. Health grid:
   - AWatch-rus;
   - 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:
   - AWatch-rus 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 <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:

```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=<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:

```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=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:

```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: <PUBLIC_GATEWAY_FQDN>' https://127.0.0.1/portal/
```

External:

```text
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`:

```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 - done for incident action metadata;
3. acknowledge/assign incident - done with audit log;
4. safe "run check now";
5. safe "open Telegram status";
6. PDF/HTML daily owner report - partially done as `/api/reports` plus
   portal Markdown export; PDF/HTML file generation remains future work;
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://<PUBLIC_GATEWAY_FQDN>/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.
