# Runbook

## Быстрый health-check

### Proxmox web gateway

Если на host `<GATEWAY_HOST>` развёрнут `nginx` gateway, базовые проверки такие:

```sh
systemctl status nginx --no-pager
nginx -t
curl -I -sS -H 'Host: <PUBLIC_GATEWAY_FQDN>' http://127.0.0.1/
curl -k -fsS -H 'Host: <PUBLIC_GATEWAY_FQDN>' https://127.0.0.1/healthz
curl -k -I -sS -H 'Host: <PUBLIC_GATEWAY_FQDN>' https://127.0.0.1/ | head
curl -k -u "$(awk -F= '/^user=/{u=$2}/^password=/{p=$2}END{print u\":\"p}' <GATEWAY_CREDENTIALS_FILE>)" \
  -H 'Host: <PUBLIC_GATEWAY_FQDN>' -fsS https://127.0.0.1/ | grep -F '<PUBLIC_GATEWAY_FQDN>'
```

Playbook для повторного rollout:

```sh
ANSIBLE_HOST_KEY_CHECKING=False \
ansible-playbook -i ansible/inventory.ini ansible/deploy_proxmox_web_gateway.yml
```

### Public gateway через pfSense

Публичная схема:

```text
Internet -> <PUBLIC_GATEWAY_FQDN> -> pfSense WAN <WAN_IP> -> NAT 80/443 -> nginx <GATEWAY_HOST>
```

Нормальное состояние:

- `https://<PUBLIC_GATEWAY_FQDN>/healthz` -> `200 ok` без auth;
- `https://<PUBLIC_GATEWAY_FQDN>/` без auth -> `401`;
- `http://<PUBLIC_GATEWAY_FQDN>/healthz` -> `301` на HTTPS;
- после Basic Auth:
  - `/` -> gateway index;
  - `/r/file1c/brief` -> 1C brief;
  - `/r/grafana/api/health` -> Grafana health;
  - `/r/aw/api/0/info` -> AW server info.

Gateway credential хранится только на `<GATEWAY_HOST>`:

```sh
sudo cat <GATEWAY_CREDENTIALS_FILE>
```

pfSense NAT backup перед автоматической правкой:

```sh
ls -1t /opt/infra-admin/backups/pfsense-gateway-nat-*.json | head
```

Проверка pfSense REST/API и pending firewall changes:

```sh
set -a
. <OPERATOR_HOME>/.config/tsj-bot/pfsense.env.readonly
set +a
curl -ksS -H "X-API-Key: $PFSENSE_API_KEY" "$PFSENSE_URL/api/v2/firewall/apply"
```

NAT должен содержать `WAN tcp 443 -> <GATEWAY_HOST>:443` и `WAN tcp 80 -> <GATEWAY_HOST>:80`.
WAN rules должны содержать pass на `<GATEWAY_HOST>:80` и `<GATEWAY_HOST>:443`.

### На Proxmox

```sh
pct status <CT_ID>
pct config <CT_ID>
pct exec <CT_ID> -- systemctl is-active activitywatch-server.service
pct exec <CT_ID> -- curl -fsS http://127.0.0.1:5600/api/0/info
```

### На host-based инсталляции

Для подтвержденного размещения на `<GATEWAY_HOST>`:

```sh
ps -ef | grep -E 'aw-server-rust|pfsense-aw-poller' | grep -v grep
curl -fsS http://127.0.0.1:5600/api/0/info
ss -ltnp | grep 5600
```

Ожидаемо должны быть видны:

- `aw-server-rust` с `--webpath /opt/aw-webui-ru`;
- `pfsense-aw-poller.py --config /etc/aw-pfsense/poller.json`.

### Внутри CT

```sh
systemctl status activitywatch-server.service --no-pager
journalctl -u activitywatch-server.service -n 100 --no-pager
curl -fsS http://127.0.0.1:5600/api/0/info
ss -ltnp | grep 5600
```

### Расширенный DLP transport health-check

На AW-server:

```sh
/usr/local/bin/aw-health-check
/usr/local/bin/dlp-health-check --json
```

Что проверяет дополнительно:
- свежесть DLP bucket-ов (`aw-dlp-endpoint-signals_*`, `aw-file-operations_*`);
- наличие transport/self-test telemetry (`queueDepth`, `eventsEnqueued`, `eventsFlushed`, `sendFailures`) в endpoint self-test;
- последние значения transport-счетчиков по каждому `aw-dlp-endpoint-signals_*` bucket;
- runtime-срез `aw-file-operations_*`: последние `collector_health`, счетчики очереди/отправки, последние операции без полного пути;
- runtime-срез `aw-dlp-incidents_*`: по умолчанию только metadata/age без тяжёлого чтения events; при ручном `--incident-sample-limit > 0` — количество реальных incident events в sample, self-test count, severity/action/rule rollup и до 5 последних incidents с коротким `message_excerpt`;
- API-доступность базовых сервисов.

Интерпретация:
- `FAIL` — есть критичная проблема (service/API/stale transport);
- `WARN` — сигнал для оператора (например, bucket еще не активирован на хосте, `queueDepth > 100`, новый рост `sendFailuresDelta >= 1`, нет `collector_health` в sample), но без hard-fail.
- `sendFailures` — накопительный счётчик collector-а; основной health-check предупреждает только по delta относительно сохранённого baseline в `AW_DLP_HEALTH_STATE_DIR`, чтобы старые восстановленные ошибки не висели вечным warning.

Пороги можно переопределить без изменения collector-ов:

```sh
/usr/local/bin/dlp-health-check --json \
  --endpoint-queue-warn-depth 100 \
  --endpoint-send-failure-warn-count 1 \
  --incident-sample-limit 0 \
  --fileops-sample-limit 20 \
  --fileops-queue-warn-depth 100 \
  --fileops-send-failure-warn-count 1
```

Для разового разбора последних DLP incidents можно включить sampling вручную:

```sh
/usr/local/bin/dlp-health-check --json --incident-sample-limit 20
```

Если AW-server медленно отдаёт большой `aw-dlp-incidents_*` bucket, такой запуск может вернуть `incident-runtime: WARN` по timeout. Это не должно использоваться как hard-fail основного мониторинга.

## Проверка RU patch

```sh
grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
ls -l /opt/activitywatch/webui-ru/js/
```

## Telegram bot: DLP режим и форензика

Для оператора `DetMirAuto` каноническое поведение такое:

- Кнопка DLP должна явно показывать текущий режим и целевое действие:
- `DLP сейчас: наблюдение | включить блокировку`
- `DLP сейчас: блокировка | включить наблюдение`
- `DLP сейчас: смешанный | выровнять в блокировку`
- Slash-путь для проверки без переключения: `/dlp_mode`
- Slash-путь для переключения: `/dlp_mode_toggle`

Интерпретация:

- `наблюдение` = endpoint/email правила работают в monitor-поведении (`alert/log`), без жёсткого `block`;
- `блокировка` = endpoint/email правила переведены в `block`;
- `смешанный` = часть endpoint/email правил уже `block`, часть ещё monitor-like; нормальный следующий ход — выровнять policy.

Форензика:

- Кнопка в меню называется `Форензика Windows логов`;
- Это человеко-понятный операторский вход в bounded Hayabusa path;
- Slash-команда остаётся прежней: `/aw_dfir /path/to/package.zip HOST [CASE_ID] [MODE]`

Если Telegram показывает старую клавиатуру:

- Нажать `Статус` или `/start`;
- Бот обязан прислать свежую custom-keyboard с актуальным DLP label;
- Старая кнопка в чате может ещё приходить как текст, но бот должен её принять и после ответа вернуть свежую клавиатуру.

Проверить:

- есть `aw-ru-patch.js`;
- есть `aw-sw-cleanup.js`;
- `index.html` содержит оба include;
- `service-worker.js` заменён cleanup-версией.

## Рабочее время (worktime)

Если в деплое включен `aw_apply_worktime_settings: true`, playbook применяет базовую категоризацию (classes) и view `worktime`.

Контрольные AQL-шаблоны для расчета рабочего времени: `docs/worktime_aql_detmir.md`.

Период рабочего времени задаётся переменными:

- `aw_worktime_from` (например `00:00`)
- `aw_worktime_to` (например `17:00`)

Playbook вычисляет `durationDefault` автоматически (включая смены через полночь) и выставляет:
`/api/0/settings/startOfDay` и `/api/0/settings/durationDefault`.

### Management report API

На `:5610` сейчас есть два server-side отчёта:

- классический `RDP worktime`:
  - `/reports/worktime/today`
  - форматы: `json`, `csv`, `html`
- управленческий `management`:
  - `/reports/worktime/management`
  - форматы: `json`, `csv`, `html`

Управленческий отчёт дополнительно показывает:

- рабочее окно отдельно от календарной активности;
- очередь действий руководителя;
- тренд за несколько дней;
- свежесть источников данных;
- executive summary `Что делать сегодня`.

Практические проверки:

```sh
curl -fsS 'http://127.0.0.1:5610/reports/worktime/today?day=today' | jq '.[:5]'
curl -fsS 'http://127.0.0.1:5610/reports/worktime/management?day=today' | jq '.summary,.executive'
curl -fsS 'http://127.0.0.1:5610/reports/worktime/management?format=html&day=today' | head
curl -fsS 'http://127.0.0.1:5610/reports/worktime/management?day=today&owner=Сменный%20руководитель' | jq '.filters,.summary,.owner_rollups'
curl -fsS 'http://127.0.0.1:5610/reports/worktime/management?day=today&department=Операторы%201С' | jq '.filters,.summary,.department_rollups'
```

Важно:

- первый `management`-запрос после очистки cache может быть тяжёлым, потому что
  API пересчитывает trend и source freshness;
- повторные запросы должны быть быстрыми за счёт cache в
  `/var/lib/activitywatch/worktime-cache`;
- по умолчанию cache живёт `300` секунд и прогревается локально через
  `aw-worktime-autoheal.service`, чтобы manager-страницы не висели на cold-start;
- default warm-path должен ходить в `format=json`, потому что этого достаточно
  для наполнения cache и это заметно легче, чем cold HTML render;
- warm timeout по умолчанию `70` секунд и задаётся через
  `AW_WORKTIME_MANAGEMENT_WARM_TIMEOUT_SECONDS`;
- management report поддерживает scope-фильтры `owner` и `department`; они
  пересчитывают `summary`, `rows`, `actions` и rollups под выбранный срез, а не
  просто прячут строки в HTML;
- `trend` в filtered-view сейчас сознательно отключён: это удерживает latency в
  рабочем диапазоне и не заставляет сервер заново пересчитывать исторический
  ряд на каждый менеджерский клик;
- alias-файл теперь может содержать не только `users`, но и `owners`;
  блок `owners` нужен для manager-facing каталога ответственных:
  `display_name`, `title`, `department`, `contact`, `escalation_to`, `notes`;
- alias-файл для нормализации сотрудников по умолчанию:
  `/etc/activitywatch/worktime-manager-aliases.json`.

## Типовые инциденты

### Hayabusa: операторский сценарий по умолчанию

Текущий production-сценарий уже не требует ручного `accept/process-inbox`.

Нормальный путь для оператора:

1. На Windows-хосте запустить:

```powershell
powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-upload-hayabusa-to-aw-server.ps1 -HoursBack 6 -CaseId 30
```

2. Сервер `<AW_SERVER_HOST>` сам:

- примет `zip` и `.meta.json` в `/opt/activitywatch/aw-rus-ops/drop`;
- запустит `aw-hayabusa`;
- посчитает severity/score;
- создаст case при уровне от `medium`;
- отправит Telegram alert при уровне от `high`.

3. Проверить результат:

```bash
systemctl is-active aw-hayabusa-drop.path
systemctl is-failed aw-hayabusa-drop.service || true
aw-hayabusa doctor
aw-hayabusa inventory
cat /opt/hayabusa/state/latest-intake.json
journalctl -u aw-hayabusa-drop.service -n 80 --no-pager
curl -fsS http://127.0.0.1:5602/api/0/dlp/cases/30
```

Ожидаемо:

- `latest-intake.json` имеет `status=ok`;
- `drop` после обработки пустой;
- в case есть `forensics.hayabusa`;
- Telegram alert уже уходит в операторский чат.

Production scheduled task на `SHARKON2025`:

- `ActivityWatch Hayabusa Upload`
- principal `Администратор`, `LogonType=Interactive`, `RunLevel=Highest`
- период `6` часов, lookback `6` часов
- нормальный `LastTaskResult=0`

Если `LastTaskResult=3221225794` (`0xC0000142`) и в `C:\ProgramData\AWatch-rus\logs\hayabusa-upload.log` нет новой строки, скрипт не стартовал. На текущем RDP-хосте это воспроизводится даже минимальной SYSTEM-задачей с `powershell.exe`; пересоздать Hayabusa task как interactive/highest от `Администратор`, не от `SYSTEM`.

Если upload прошёл, но сервер не обработал пакет:

```bash
sudo systemctl reset-failed aw-hayabusa-drop.path aw-hayabusa-drop.service
sudo systemctl start aw-hayabusa-drop.path
sudo systemctl start aw-hayabusa-drop.service
find /opt/activitywatch/aw-rus-ops/drop -maxdepth 1 -type f -ls
find /opt/hayabusa/inbox/incoming -maxdepth 1 -type f -ls
```

`drop` и `incoming` после успешной обработки должны быть пустыми; latest intake должен указывать на последний пакет `SHARKON2025`.

### Hayabusa: manual fallback / production validation end-to-end

Цель: подтвердить один реальный путь

- Windows EVTX export
- перенос пакета на `<AW_SERVER_HOST>`
- intake через `aw-hayabusa`
- генерация отчёта
- привязка bounded metadata к операторскому follow-up

Минимальный production-proven сценарий:

Этот путь нужен только если:

- надо руками прогнать старый пакет;
- надо повторно разобрать archived zip;
- надо отладить сам `aw-hayabusa` без drop-автоматики.

1. На Windows-хосте сделать экспорт:

```powershell
powershell.exe -ExecutionPolicy Bypass -File C:\ProgramData\AWatch-rus\export-evtx-for-hayabusa.ps1 -DaysBack 1
```

2. Проверить, что пакет реально появился:

```powershell
Get-ChildItem 'C:\ProgramData\AWatch-rus\forensics\evtx-exports' |
  Sort-Object LastWriteTime -Descending |
  Select-Object -First 5 Name,Length,LastWriteTime
```

Ожидаемо должен появиться zip вида:

- `HOST-YYYYMMDD-HHMMSS.zip`

3. Перенести zip на `<AW_SERVER_HOST>` в операторскую рабочую зону.

4. На `AW-server` проверить раннер:

```bash
aw-hayabusa doctor
aw-hayabusa inventory
aw-hayabusa profiles
```

5. Принять пакет:

```bash
aw-hayabusa accept --package /path/to/HOST-YYYYMMDD-HHMMSS.zip --host HOST
```

6. Обработать inbox:

```bash
aw-hayabusa process-inbox --mode incident
```

7. Проверить результат:

```bash
aw-hayabusa inventory
readlink -f /opt/hayabusa/state/latest-run
readlink -f /opt/hayabusa/state/latest-HOST
find /opt/hayabusa/reports/HOST -maxdepth 2 -type f | sort
```

Ожидаемо должны быть:

- `summary.html`
- `manifest.json`
- `run.log`
- `timeline.jsonl` или `timeline.csv`
- `logon-summary-*.csv`

8. Проверить traceability:

```bash
cat /opt/hayabusa/state/latest-intake.json
find /opt/hayabusa/archive/packages/HOST -maxdepth 1 -type f | sort
find /opt/hayabusa/archive/extracted/HOST -maxdepth 2 -type f | sort
```

Нужно зафиксировать:

- `host`
- `intake_id`
- `sha256`
- `package_path`
- `report_dir`
- `status`

9. Для AW-rus/operator follow-up заносить только bounded metadata:

- `tool=hayabusa`
- `host`
- `mode=incident`
- `status`
- `intake_id`
- `package_path`
- `sha256`
- `report_dir`
- `summary_html`
- `timeline_path`
- `manifest_path`

Не заносить в case / comments:

- сырые EVTX
- полный Sigma output
- полный timeline body

10. Если прогона не получилось, записать tuning backlog:

- пустой/битый zip
- нет EVTX в payload
- слабый audit scope на Windows
- шумный или слишком тяжёлый результат
- неочевидная трассировка от пакета к отчёту

Acceptance для этого сценария:

- есть хотя бы один реальный zip-пакет;
- `aw-hayabusa` провёл intake и analysis без ручной импровизации;
- артефакты трассируются от `HOST` до `report_dir`;
- follow-up не тащит сырые forensic данные в обычные AW buckets.

Example regression proof template:

- `host=HOST-EXAMPLE`
- `case_id=CASE_ID_EXAMPLE`
- `intake_id=INTAKE_ID_EXAMPLE`
- `sha256=SHA256_EXAMPLE`
- `report_dir=/opt/hayabusa/reports/HOST-EXAMPLE/REPORT_RUN_ID_EXAMPLE`
- `latest-intake.json` status: `ok`
- AW-rus case linkage stored under `forensics.hayabusa`

Типовые проблемы, найденные при validation:

- Windows zip с backslash path separators давал `unzip` warning rc=1; wrapper не должен валить intake на таком предупреждении.
- timeline режимы должны использовать `rules/config`, а не корень rules directory.

После live proof держать как regression checks:

```bash
aw-hayabusa doctor
aw-hayabusa inventory
cat /opt/hayabusa/state/latest-intake.json
readlink -f /opt/hayabusa/state/latest-run
```

### DLP не виден в вебе

Быстрый чек сервера:

```sh
curl -fsS http://127.0.0.1:5600/api/0/buckets | jq -r 'keys[] | select(test("^aw-dlp-"))'
curl -fsS http://127.0.0.1:5600/api/0/buckets/aw-dlp-incidents_HOST-EXAMPLE | jq '{end:.metadata.end}'
```

Контролируемый тест ingest:

```sh
TS=$(date -u +%Y-%m-%dT%H:%M:%S.000Z)
PAYLOAD=$(jq -nc --arg ts "$TS" '{timestamp:$ts,duration:0,data:{ruleId:"selftest-dlp-incident",action:"alert",severity:"low",message:"Self-test DLP incident from runbook",signalType:"self_test",username:"AUTOTEST",sessionId:0,hostname:"HOST-EXAMPLE",source:"self-test"}}')
curl -fsS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-incidents_HOST-EXAMPLE/heartbeat?pulsetime=60' -H 'Content-Type: application/json' --data "$PAYLOAD"
curl -fsS 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-incidents_HOST-EXAMPLE/events?limit=5' | jq '.[0].data'
```

Если API видит событие, а bucket-страница в UI показывает старые `First/last event`, нажать `Обновить` на странице bucket и раскрыть `Events`.

### Проверка WAL failover (Windows collectors)

Цель: подтвердить, что при недоступности AW API события не теряются, а буферизуются в локальной очереди и автоматически отправляются после восстановления связи.

На RDP-хосте (`<WINDOWS_HOST>`) в PowerShell под администратором:

1) Проверить/обнулить очереди:

```powershell
$q1 = 'C:\ProgramData\AWatch-rus\file-operations-queue*.jsonl'
$q2 = 'C:\ProgramData\AWatch-rus\dlp-endpoint-signals-queue*.jsonl'
Get-Item $q1,$q2 | Select Name,Length,LastWriteTime
```

2) Временно заблокировать исходящий доступ на AW API (`:5600`):

```powershell
New-NetFirewallRule -DisplayName 'AWatch WAL Test Block 5600' -Direction Outbound -Action Block -Protocol TCP -RemotePort 5600 -ErrorAction SilentlyContinue
Enable-NetFirewallRule -DisplayName 'AWatch WAL Test Block 5600'
```

3) Сгенерировать тест-событие `file-operations`:

```powershell
$p = Join-Path $env:USERPROFILE 'Desktop\aw_wal_test.txt'
Set-Content -LiteralPath $p -Value ('wal-test ' + (Get-Date -Format o))
Start-Sleep -Seconds 5
```

4) Убедиться, что очередь выросла:

```powershell
Get-Item $q1,$q2 | Select Name,Length,LastWriteTime
```

5) Снять блокировку и дождаться flush:

```powershell
Disable-NetFirewallRule -DisplayName 'AWatch WAL Test Block 5600'
Start-Sleep -Seconds 20
Get-Item $q1,$q2 | Select Name,Length,LastWriteTime
```

Ожидаемо:
- на шаге 4 длина как минимум одного queue-файла увеличивается;
- на шаге 5 очередь уменьшается (в идеале до `0` или близко к фоновому уровню).

6) Проверка на AW server:

```sh
curl -fsS 'http://<AW_SERVER_HOST>:5600/api/0/buckets/aw-file-operations_<AW_SERVER_HOST>/events?limit=10' | jq '.[0].data'
```

После теста удалить правило:

```powershell
Remove-NetFirewallRule -DisplayName 'AWatch WAL Test Block 5600' -ErrorAction SilentlyContinue
```

### PowerShell MCP на AWatch-rus Windows host

Для `AWatch-rus` Windows host `<WINDOWS_HOST>` интерактивный путь из Linux/Codex закреплён через `SSH`, а не через `WSMan`.

Быстрый вход:

```bash
cd <PROJECT_ROOT>
bash scripts/install_detmir_powershell_mcp.sh
```

После переоткрытия `pwsh` или новой Codex-session:

```powershell
detmir-win-test
detmir-win-ps 'hostname; Get-Date -Format o'
detmir-win-shell
```

Канонический документ:

- `docs/POWERSHELL_MCP_REMOTE_RU.md`

Правило:

- `WinRM` использовать для `ansible aw_windows` и playbook-ов;
- `SSH` использовать для `powershell-windows` MCP, operator PowerShell и ad-hoc remote execution.

### У пользователей всплывает окно PowerShell

Ожидаемое поведение collector-ов: запуск hidden (`-WindowStyle Hidden`).
Проверка на Windows хосте:

```powershell
Get-CimInstance Win32_Process |
  Where-Object { $_.CommandLine -and ($_.CommandLine -match 'browser-domains-native-collector.ps1' -or $_.CommandLine -match 'dlp-endpoint-signals-collector.ps1') } |
  Select-Object SessionId, ProcessId, CommandLine
```

Если нужно экстренно убрать снимки инцидентов:

1. Поставить `incidentCapture.screenshotEnabled = false` в `deployment-config.json` (для каждого StateRoot).
2. Запустить `Start-ScheduledTask -TaskName 'ActivityWatch Recovery'`.

### Windows host: `Активное время = 0s`, хотя `window`-события есть

Симптом:

- в Activity view за день видно `Worktime = 0s`;
- `Top Window Titles / Top Categories / Category Tree` пустые;
- при этом bucket `aw-watcher-window_HOST-EXAMPLE` содержит свежие события.

Подтвержденная причина:

- watcher `afk` "залип" в `status=afk` без `not-afk`;
- из-за этого дневная сводка не считает подтвержденную активность.

Быстрый recovery (с Linux admin host):

1. Проверить учетку входа. Рабочую учетную запись задавать как параметр экземпляра: `HOST-EXAMPLE\WINDOWS_USER_EXAMPLE`.
2. Поднять remote execution через `wmiexec.py` с auth-file:

```sh
cat > /tmp/detmir-windows.auth << 'EOF'
username = WINDOWS_USER_EXAMPLE
password = <PASSWORD>
domain = HOST-EXAMPLE
EOF
chmod 600 /tmp/detmir-windows.auth
```

3. Запустить recovery task:

```sh
wmiexec.py -nooutput -A /tmp/detmir-windows.auth <WINDOWS_HOST> \
  "powershell -NoProfile -Command \"Start-ScheduledTask -TaskName 'ActivityWatch Recovery'\""
```

4. Запустить все launch tasks:

```sh
wmiexec.py -nooutput -A /tmp/detmir-windows.auth <WINDOWS_HOST> \
  "powershell -NoProfile -Command \"Get-ScheduledTask | Where-Object TaskName -like 'ActivityWatch Launch *' | ForEach-Object { Start-ScheduledTask -TaskName \$_.TaskName }\""
```

5. Подождать 10-20 секунд и проверить API на AW server (`<AW_SERVER_HOST>:5600`):

```sh
curl -fsS 'http://<AW_SERVER_HOST>:5600/api/0/buckets/aw-watcher-afk_HOST-EXAMPLE/events?limit=30' \
  | jq '{latest:.[0].timestamp, statuses:(group_by(.data.status)|map({status:.[0].data.status,count:length}))}'
```

Ожидаемо после фикса:

- в свежих AFK-событиях появляется `status=not-afk`;
- `aw-watcher-window_HOST-EXAMPLE` продолжает обновляться;
- после обновления страницы UI дневная сводка перестает быть `0s`.

### Сервис не стартует

```sh
systemctl cat activitywatch-server.service
cat /etc/activitywatch/aw-server.env
journalctl -xeu activitywatch-server.service --no-pager
```

Частые причины:

- битый URL релиза;
- неполная распаковка архива;
- занят порт;
- не созданы каталоги или пользователь;
- ошибка в env-файле.

### API отвечает, но UI без русификации

Проверить:

- патч реально вставлен в `index.html`;
- browser cache/service worker очищен;
- reverse proxy не отдаёт старую статику;
- сервис был перезапущен после правок.

Повторное применение:

```sh
bash <CT_BOOTSTRAP_DIR>/apply_webui_ru_patch.sh
systemctl restart activitywatch-server.service
```

### После обновления UI сломался патч

- сравнить `index.html` с backup;
- заново применить patch script;
- проверить словарь в `aw-ru-patch.js`;
- при необходимости откатить только Web UI override.

## Перед любыми изменениями

1. Сделать snapshot или `vzdump`.
2. Сохранить текущий `/etc/activitywatch/aw-server.env`.
3. Сохранить текущий `index.html`.
4. Зафиксировать текущую версию `aw-server-rust`.

## Критерии готовности

- systemd unit стартует без ручного вмешательства;
- API `/api/0/info` отвечает локально;
- UI открывается;
- русификация присутствует;
- rollback-путь понятен оператору.

## Аудит CryptoPro и готовности подписантов

Для повторяемой проверки сертификатов подписантов и встроенных лицензий CryptoPro:

```bash
cd <PROJECT_ROOT>/ansible
ansible-playbook -i inventory.ini audit_cryptopro_windows.yml
```

Итоговый JSON-отчёт сохраняется локально в:

```text
/tmp/aw-rus-cryptopro-audit-<user>/rdp-prod-cryptopro-audit.json
```

Отчёт содержит матрицу по профилям:

- `requestedUser`
- `profileUser`
- `thumbprint`
- `subject`
- `hasPrivateKey`
- `embeddedLicenseOk`
- `embeddedLicenseStatus`
- `container` если виден
- `actionNeeded`
