# Runbook: перевод AWatch-rus на Rust / ADK-Rust

Дата фиксации: `2026-06-01`

Цель: постепенно заменить хрупкие Python/shell operational scripts на
самодостаточные Rust-бинарники, не ломая текущий production-контур AWatch-rus.

Этот runbook является рабочим планом миграции. Если фактический runtime
расходится с этим документом, сначала фиксируется baseline, затем обновляется
runbook.

Текущий resume snapshot проекта хранится в
`.ai/runtime/detmir-current-session.md`. Перед продолжением миграции после
перерыва или context reset сначала читать его, затем `RUNBOOK.md` и
`<OPERATOR_CODEX_HOME>/skills/detmir-rust-migration/references/current-state.md`.

## 1. Целевое состояние

В результате миграции DetMir должен иметь:

- единый Rust workspace в `adk-rust/`;
- один standalone-бинарник или crate на каждую операционную функцию;
- стабильные machine-readable JSON-контракты вместо парсинга human text;
- одинаковые exit codes для старых и новых команд;
- systemd units/timers, не зависящие от ноутбука, venv, pip и локальных путей;
- безопасный auto-heal с dry-run, lock, cooldown, allowlist и rollback;
- ADK-compatible envelopes там, где вывод передается агентам/боту/LLM.

Rust не должен использоваться как самоцель. Если компонент надежнее оставить в
Ansible, PowerShell или Playwright, он остается там до появления практической
причины для переноса.

## 2. Текущая отправная точка

Уже создано:

- `adk-rust/Cargo.toml` - workspace;
- `adk-rust/crates/detmir-status` - read-only status module with text, JSON and ADK JSON output;
- `/usr/local/bin/detmir-status` на Proxmox, проверен против
  `/var/lib/detmir-ai/latest-state.json`;
- `/usr/local/bin/detmir-adk-status` на Proxmox, проверен против
  `/var/lib/detmir-ai/latest-state.json` как compatibility binary.

Ключевые текущие legacy-компоненты:

| Компонент | Текущая реализация | Риск переноса |
|---|---|---|
| `detmir-status` | Rust binary + thin compatibility wrapper | выполнено |
| `detmir-check` | Rust binary + thin compatibility wrapper | выполнено |
| `detmir-dlp` | Rust SSH wrapper + thin compatibility wrapper | выполнено |
| `detmir-auto` | Rust production via systemd drop-in + legacy script retained | switched |
| `detmir-heal-safe` | Rust binary deployed + legacy script retained | switched for Rust auto |
| `tsj_guardian_watchdog.sh` | Rust service via systemd drop-in + legacy shell retained | switched |
| Telegram `/status`/decision backend | Rust helper + permanent Python bot runtime | backend only |
| `aw-rus-healthd` | Rust production unit, Python entrypoint removed from repo | done |
| `dlp-health-check` | Rust production binary, Python entrypoint removed from repo | done |
| DLP aggregator | Rust production via systemd drop-in + legacy Python retained | switched |
| AW DLP Influx exporter | Rust production via systemd drop-in + legacy Python retained | switched |
| AW worktime Influx exporter | Rust production via systemd drop-in + legacy Python retained | switched |
| AW worktime prewarm | Rust production via systemd drop-in + legacy shell retained | switched |
| AW worktime API | Rust production via systemd drop-in + legacy Python retained | switched |
| DLP syslog forwarder | Rust production via systemd drop-in + legacy Python retained | switched |
| DLP webhook sender | Rust production via systemd drop-in + legacy Python retained | switched |
| DLP CEF exporter | Rust production via systemd drop-in + legacy Python retained | switched |
| `tsj_guardian_bot.py` | Permanent Python Telegram runtime | не переносить |
| AW remaining worktime modules | mostly Rust production; inspect leftovers before next item | средний |
| deploy/install scripts | shell/Ansible/PowerShell | высокий, переносить последними |

## 3. Архитектура workspace

Целевая структура:

```text
adk-rust/
  Cargo.toml
  Cargo.lock
  RUNBOOK.md
  crates/
    detmir-core/
    detmir-aw-client/
    detmir-systemd/
    detmir-state/
    detmir-status/
    detmir-check/
    detmir-dlp/
    detmir-auto/
    detmir-heal-safe/
    tsj-guardian-status/
    tsj-guardian-watchdog/
```

Назначение shared crates:

| Crate | Назначение |
|---|---|
| `detmir-core` | ошибки, CLI output, timeouts, retry, exit codes, config loading |
| `detmir-aw-client` | ActivityWatch HTTP API, buckets, events, timestamps |
| `detmir-systemd` | безопасный wrapper вокруг `systemctl`, allowlist, dry-run |
| `detmir-state` | чтение/запись `/var/lib/detmir-ai`, atomic writes, retention |
| `detmir-report` | Markdown/JSON reports, ADK envelopes, report bundle assembly |

Бинарные модули должны быть маленькими и собираться из shared crates. Логику не
дублировать между `check`, `status`, `auto` и ботом.

## 4. Общие контракты для каждого Rust-модуля

Каждый новый модуль обязан иметь:

- `--json` для машинного вывода;
- `--pretty` или обычный text output для человека, если команда операторская;
- `--config <path>` либо documented env vars;
- `--dry-run` для любых действий, которые меняют состояние;
- `--timeout` там, где есть сеть/SSH/HTTP;
- `--no-color` если вывод может попадать в systemd/Telegram/report;
- стабильные exit codes:
  - `0` - OK;
  - `1` - usage/config/runtime error;
  - `2` - проверка выполнена, но состояние WARN/FAIL;
  - `3` - action запрещен safety policy;
- structured logs через `tracing` для daemon/action-команд;
- unit tests на чистую логику;
- fixture tests на реальные JSON samples;
- README с примером локального и серверного запуска.

## 5. Safety policy

Нельзя сразу заменять управляющие скрипты без shadow-mode.

Обязательные правила:

- сначала read-only parity, потом mutation;
- старый и новый модуль должны некоторое время работать параллельно;
- новый модуль не получает право писать state, рестартить сервисы или удалять
  файлы до прохождения acceptance gates;
- все state writes только через atomic temp file + rename;
- heal-команды только по allowlist units;
- no implicit sudo: если нужен `sudo`, он должен быть явно виден в deploy/unit;
- lock file обязателен для `auto` и `heal`;
- cooldown обязателен для restart/start actions;
- каждый risky action пишет audit entry;
- rollback должен быть одной командой systemd/service symlink switch.

## 6. Фазы миграции

### Phase 0. Baseline и фиксация контрактов

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

Действия:

1. Снять текущие outputs legacy-команд:
   - `detmir-status`;
   - `detmir-check --json`;
   - `detmir-dlp`;
   - `detmir-auto` на зеленом контуре;
   - `detmir-heal-safe` в dry-safe сценарии или на mock host.
2. Сохранить sanitized fixtures в `adk-rust/fixtures/`.
3. Зафиксировать JSON schemas:
   - status summary;
   - DetMir check result;
   - DLP health result;
   - auto run state;
   - heal action log.
4. Зафиксировать exit code matrix старых команд.
5. Зафиксировать runtime paths:
   - `/var/lib/detmir-ai`;
   - `/usr/local/bin`;
   - relevant systemd units/timers.

Гейт готовности:

```bash
cd adk-rust
cargo fmt --all -- --check
cargo check --workspace
```

Результат фазы: миграция не начинается вслепую; есть baseline для сравнения.

### Phase 1. Rust foundation

Цель: создать shared crates, чтобы не плодить разные реализации HTTP, времени,
ошибок, JSON и systemd.

Модули:

- `detmir-core`;
- `detmir-state`;
- `detmir-aw-client`;
- `detmir-systemd`.

Минимальная функциональность:

- RFC3339/UTC timestamp parsing;
- HTTP JSON client with timeout/retry;
- TCP check;
- atomic JSON write;
- retention cleanup;
- exit-code helper;
- common `StatusLevel`: `OK`, `WARN`, `FAIL`;
- bucket mode enum: `fresh`, `inactive_ok`, `event_driven`;
- systemd read-only checks;
- systemd action allowlist, но без включения mutation by default.

Гейт:

```bash
cd adk-rust
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
```

### Phase 2. Read-only operator modules

Цель: заменить самые безопасные команды, не меняющие состояние.

Порядок:

1. `detmir-status`
   - объединить с совместимым `detmir-adk-status`;
   - читать `/var/lib/detmir-ai/latest-state.json`;
   - выдавать text, JSON, ADK JSON;
   - заменить shell wrapper после parity.
2. `detmir-check`
   - перенести Python checks в Rust;
   - сохранить семантику `FRESH`, `INACTIVE`, `EVENT-DRIVEN`, `STALE`, `DEAD`;
   - сохранить `summary.bucket_ok/stale/dead/service_failures/service_warnings`;
   - не менять thresholds без отдельного решения.
3. `detmir-dlp`
   - сначала переписать SSH wrapper;
   - затем отдельно переносить `dlp-health-check.py` на AW server.

Shadow-mode для `detmir-check`:

```bash
detmir-check --json > /tmp/detmir-check.old.json
adk-rust/target/release/detmir-check --json > /tmp/detmir-check.new.json
jq -S '.summary' /tmp/detmir-check.old.json > /tmp/old.summary.json
jq -S '.summary' /tmp/detmir-check.new.json > /tmp/new.summary.json
diff -u /tmp/old.summary.json /tmp/new.summary.json
```

Гейт:

- old/new agree on `ok`;
- old/new agree on summary counters;
- new module returns same exit code class;
- no network side effects;
- server run succeeds from systemd-like environment.

### Phase 3. State/report orchestration

Цель: заменить `detmir-auto` без немедленной замены heal.

Первый Rust `detmir-auto` должен:

- брать lock;
- создавать run directory;
- запускать `detmir-check` и `detmir-dlp`;
- писать `summary-before.json`;
- писать `state-*.json`;
- обновлять symlinks:
  - `latest-run`;
  - `latest-state.json`;
  - `latest-report.md`;
- выполнять retention cleanup;
- уметь `--no-heal`;
- по умолчанию на первом этапе не выполнять heal, а только писать
  `would_heal=true`.

Pollinations/LLM report:

- не должен быть hard dependency для статуса;
- failure LLM report не должен ломать state update;
- raw JSON summary всегда должен сохраняться даже при ошибке report generation.

Гейт:

- Rust `detmir-auto --no-heal` дает такой же `latest-state.json` по смыслу;
- symlink updates atomic enough for readers;
- timer можно прогнать вручную без изменения heal behavior;
- `detmir-status` видит новое state без изменений.

Текущее состояние:

- `detmir-auto-rust` установлен на Proxmox как shadow binary
  `/usr/local/bin/detmir-auto-rust`;
- `detmir-auto-rust` умеет вызывать Rust heal через `--enable-heal` или
  `DETMIR_AUTO_HEAL=1`, но shadow unit явно запускается с `--no-heal`;
- production `detmir-auto.service` переключен на `/usr/local/bin/detmir-auto-rust`
  через drop-in `/etc/systemd/system/detmir-auto.service.d/20-rust-switch.conf`;
- legacy `/usr/local/bin/detmir-auto` сохранен для rollback;
- отдельный `detmir-auto-rust-shadow.timer` отключен после успешного
  production timer observation; unit-файлы и shadow state сохранены;
- shadow пишет только в `/var/lib/detmir-ai/shadow/detmir-auto-rust`;
- `detmir-auto-rust-shadow.service` использует `SuccessExitStatus=2`, чтобы
  найденный shadow FAIL фиксировался в JSON, но не загрязнял
  `systemctl --failed`;
- перед стартом shadow service проверяет, что production `detmir-auto.service`
  не активен;
- последняя systemd shadow-проверка после retry в AW client:
  `severity=OK`, `check_rc=0`, `dlp_rc=0`, buckets `8/0/0`, DLP `22/0/0`.
- production Rust start проверен: `ExecStart=/usr/local/bin/detmir-auto-rust
  --command-timeout-seconds 180`, latest-state `OK`, `systemctl --failed`
  пустой.
- scheduled production timer cycle проверен после switch: process status `0`,
  latest-state `OK`, report generated, `systemctl --failed` пустой.

### Phase 4. Safe heal

Цель: перенести `detmir-heal-safe`, но только после read-only parity.

Обязательные guardrails:

- `--dry-run` default на первых deploy;
- allowlist units:
  - `activitywatch-server.service`;
  - `aw-worktime-api.service`;
  - `aw-worktime-ui-bridge.timer`;
  - `activitywatch-dlp-aggregator.timer` только если unit существует;
- запрет wildcard restart;
- `systemctl reset-failed` только для allowlist;
- cooldown между restart попытками;
- audit log в run directory;
- hard timeout на SSH/systemctl;
- never touch Windows/RDP recovery from this module.

Гейт:

- dry-run показывает exact actions;
- на зеленом контуре не рестартит ничего;
- на mock/failing unit рестартит только allowlisted unit;
- после heal всегда запускается повторный check;
- exit code не маскирует FAIL.

Текущее состояние:

- создан crate `detmir-heal-safe`;
- binary развернут на Proxmox как `/usr/local/bin/detmir-heal-safe-rust`;
- production `/usr/local/bin/detmir-heal-safe` пока не заменен;
- production Rust auto использует `/usr/local/bin/detmir-heal-safe-rust` через
  `DETMIR_HEAL_BIN`;
- default mode: dry-run, mutation только через `--apply`;
- optional timer start выключен по умолчанию, включается только
  `--start-optional`;
- green dry-run проверен: restart/start не планируются;
- green apply проверен: выполнены только `reset-failed` и DLP health snapshot,
  контур остался `OK`, `systemctl --failed` пустой.
- red/mock heal test проверен на Proxmox в isolated state-dir:
  `summary-before=FAIL`, heal вызван, check/dlp повторены, final state `OK`,
  process rc `0`.

### Phase 5. Watchdog и lightweight services

Цель: заменить простые shell watchdogs и wrappers.

Порядок:

1. `tsj-guardian-watchdog.sh` -> `tsj-guardian-watchdog`;
2. lightweight service wrappers;
3. bounded cleanup/retention jobs.

Особое внимание:

- bot heartbeat file parsing;
- duplicate `gost` instance dedupe;
- service restart only on stale/missing heartbeat;
- no killing unrelated processes.

Гейт:

- dry-run mode показывает, какой PID был бы убит;
- process matching покрыт тестами;
- systemd timer/service rollback сохранен.

Текущее состояние:

- создан crate `tsj-guardian-watchdog`;
- binary развернут на Proxmox как
  `/usr/local/bin/tsj-guardian-watchdog-rust`;
- production `tsj-guardian-watchdog.service` переключен на Rust через drop-in
  `/etc/systemd/system/tsj-guardian-watchdog.service.d/20-rust-switch.conf`;
- legacy `/opt/infra-admin/tsj-bot/tsj_guardian_watchdog.sh` сохранен;
- dry-run выявил bug legacy-подхода: `pgrep -f` не матчился как literal из-за
  `+` в `gost` pattern;
- Rust версия использует literal process scan через `ps`;
- one-shot apply удалил лишний duplicate `gost` PID и оставил systemd MainPID;
- service mode работает как:

```bash
/usr/local/bin/tsj-guardian-watchdog-rust --apply --loop-forever --interval-seconds 60
```

Rollback:

```bash
sudo rm -f /etc/systemd/system/tsj-guardian-watchdog.service.d/20-rust-switch.conf
sudo systemctl daemon-reload
sudo systemctl restart tsj-guardian-watchdog.service
```

### Phase 6. AW server health and DLP modules

Цель: перенести серверные Python health checks/exporters.

Порядок:

1. `aw-server/aw-rus-healthd.py` - выполнено, production через
   `aw-rus-healthd.service.d/20-rust-switch.conf`;
2. `scripts/dlp-health-check.py` - выполнено, `/usr/local/bin/dlp-health-check`
   заменён Rust-бинарником;
3. `scripts/aggregate_dlp_events.py` - выполнено, production через
   `activitywatch-dlp-aggregator.service.d/20-rust-switch.conf`;
4. DLP integrations:
   - syslog forwarder - выполнено, production через
     `aw-dlp-syslog-forwarder.service.d/20-rust-switch.conf`;
   - webhook sender - выполнено, production через
     `aw-dlp-webhook-sender.service.d/20-rust-switch.conf`;
   - CEF exporter - выполнено, production через
     `aw-dlp-cef-exporter.service.d/20-rust-switch.conf`;
5. `aw-server/aw-dlp-influx-exporter.py` - выполнено, production через
   `aw-dlp-influx-exporter.service.d/20-rust-switch.conf`;
6. `aw-server/aw-worktime-influx-exporter.py` - выполнено, production через
   `aw-worktime-influx-exporter.service.d/20-rust-switch.conf`;
7. `aw-server/aw-worktime-prewarm.sh` - выполнено, production через
   `aw-worktime-prewarm.service.d/20-rust-switch.conf`;
8. `aw-server/aw-worktime-ui-bridge.py` - выполнено, production через
   `aw-worktime-ui-bridge.service.d/20-rust-switch.conf`;
9. `aw-server/aw-worktime-autoheal.sh` - выполнено, production через
   `aw-worktime-autoheal.service.d/20-rust-switch.conf`;
10. remaining worktime modules.

Правила:

- exporters должны сохранять Prometheus/Influx output format;
- API endpoints не менять без compatibility layer;
- для каждого exporter сначала golden output fixture;
- service unit меняется только после side-by-side run.

Гейт:

- old/new metrics names match;
- dashboard datasource health не ломается;
- unit tests старого поведения перенесены или сохранены;
- `systemctl --failed` clean после deploy.

### Phase 7. Telegram bot backend helpers

Цель: не переносить Telegram bot runtime на Rust. Python остается постоянным
production runtime для Telegram polling/sending, proxy, retries и side effects.
На Rust выносятся только backend helpers, status aggregation, decision/gating и
безопасные read-only/action contracts, которые бот вызывает как внешние команды.

Порядок:

1. `[done]` Бот продолжает жить на Python.
2. `[started]` Команды `/status`, `/detmir`, health summaries начинают читать Rust JSON.
   Первым вынесен `tsj-guardian-status`: read-only helper для строки
   `detmir_auto` из `/var/lib/detmir-ai/latest-state.json`; вторым шагом тот же
   helper начал рендерить `aw_rus_slo` из `AW_RUS_SLO_SUMMARY_CMD`.
3. Recovery/actions вызывают Rust binaries с dry-run/audit.
4. `[decision]` Bot runtime на Rust не переносится; новые улучшения Telegram
   делаются в Python runtime или через Rust backend helpers.

Почему так:

- Telegram runtime, proxy, retries и async edge cases уже стабильно покрыты
  Python-кодом и тестами;
- перенос runtime даст мало пользы и высокий риск регрессий;
- максимальная польза от Rust сначала в backend-командах и safety contracts.

Гейт:

- `/status` показывает те же строки или лучше;
- smoke message проходит;
- watchdog активен;
- restart bot не теряет config/secrets;
- секреты не попадают в logs/tests.

### Phase 8. Deploy/install scripts

Цель: переносить последними, только после стабилизации runtime binaries.

Что может остаться не на Rust:

- Ansible playbooks;
- Windows PowerShell collector deployment;
- Playwright browser smoke;
- packaging scripts, если они надежны и редко исполняются.

Что имеет смысл перенести:

- validation CLI;
- install-kit consistency checker;
- local report generators;
- deterministic packaging helpers.

Гейт:

- install-kit output byte/content expected;
- rollback installer path documented;
- production deploy не зависит от laptop-only paths.

## 7. Deployment workflow

Сборка:

```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo build --release --workspace
```

Установка одного бинарника на Proxmox:

```bash
install -o root -g root -m 0755 \
  target/release/<binary> \
  /usr/local/bin/<binary>
```

Через Ansible:

```bash
ansible proxmox -i ansible/inventory.ini -m copy -a \
  'src=/mnt/usb_hdd2/Projects/ActivityWatch-Russian/adk-rust/target/release/<binary> dest=/usr/local/bin/<binary> owner=root group=root mode=0755 backup=yes'
```

Проверка на сервере:

```bash
<binary> --version || true
<binary> --help
<binary> <safe-read-only-command> --json
systemctl --failed
```

## 8. Rollback workflow

Для каждого заменяемого компонента должен быть rollback:

1. Старый script остается на сервере как `<name>.legacy` или в package backup.
2. systemd unit меняется через drop-in или controlled template.
3. Перед switch:
   - сохранить `systemctl cat <unit>`;
   - сохранить checksum старого binary/script;
   - сохранить latest known good command.
4. Rollback command документируется в PR/run note.

Текущий rollback для `detmir-auto` после Rust switch:

```bash
sudo rm -f /etc/systemd/system/detmir-auto.service.d/20-rust-switch.conf
sudo systemctl daemon-reload
sudo systemctl restart detmir-auto.service
```

Перед switch сохранены:

```bash
/var/lib/detmir-ai/switch-backups/detmir-auto.service.before-rust-20260531-192809.txt
/var/lib/detmir-ai/switch-backups/detmir-auto.sha256.before-rust-20260531-192809.txt
```

Пример:

```bash
cp -a /usr/local/bin/detmir-check /usr/local/bin/detmir-check.legacy
install -o root -g root -m 0755 target/release/detmir-check /usr/local/bin/detmir-check
detmir-check --json
```

Если новый binary провален:

```bash
mv /usr/local/bin/detmir-check.legacy /usr/local/bin/detmir-check
systemctl restart detmir-auto.timer || true
```

## 9. Acceptance checklist

Перед заменой legacy-команды:

- [ ] old/new CLI documented;
- [ ] old/new JSON compared on real fixture;
- [ ] exit codes match by class;
- [ ] unit tests pass;
- [ ] integration smoke pass on target host;
- [ ] no secrets in logs;
- [ ] systemd environment tested;
- [ ] rollback command known;
- [ ] docs updated;
- [ ] operator command example added.

Перед включением mutation/recovery:

- [ ] `--dry-run` verified;
- [ ] allowlist verified;
- [ ] lock verified;
- [ ] cooldown verified;
- [ ] audit log verified;
- [ ] green contour causes no action;
- [ ] failing contour causes only intended action;
- [ ] post-action check verified;
- [ ] rollback tested.

## 10. Quality gates

Локально:

```bash
cd adk-rust
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
```

Production readiness checklist:

- [ ] public defaults are still sanitized: no live hosts, IPs, domains, tokens,
      evidence paths, case IDs, hashes, or operator home paths in tracked files;
- [ ] live values are supplied only through private inventory/env or server-side
      runtime env;
- [ ] enabled Influx exporters have live URL, org, bucket, token and hosts;
- [ ] private production config passes:

  ```bash
  scripts/check_production_inventory_placeholders.sh private-config/runtime.env private-config/ansible-vars.yml
  ```

  Run this guard only on private production override files. Public tracked
  default/example files may intentionally contain `HOST-EXAMPLE` and TEST-NET
  values for release hygiene.

- [ ] `ansible-playbook -i ansible/inventory.ini ansible/deploy_aw_server.yml --syntax-check` passes;
- [ ] `aw-worktime-influx-exporter.service` and `aw-dlp-influx-exporter.service`
      complete once and write points;
- [ ] `detmir-readiness --json` returns `status=OK`;
- [ ] readiness bundle is generated with
      `--output-dir /var/lib/activitywatch/health/readiness-bundle`;
- [ ] readiness bundle integrity passes:

  ```bash
  cd /var/lib/activitywatch/health/readiness-bundle
  sha256sum -c sha256sums.txt
  openssl dgst -sha256 -verify public-key.pem \
    -signature sha256sums.txt.sig sha256sums.txt
  ```

- [ ] readiness signing public-key fingerprint for the shipped/customer
      contour is fixed and documented:

  ```text
  READINESS_PUBLIC_KEY_SHA256=<READINESS_PUBLIC_KEY_SHA256_FINGERPRINT>
  ```

  Verification command:

  ```bash
  sha256sum /var/lib/activitywatch/health/readiness-bundle/public-key.pem
  jq -r '.signature.public_key_fingerprint_sha256' \
    /var/lib/activitywatch/health/readiness-bundle/detmir-readiness-status.json
  ```

- [ ] `detmir-readiness-status.json` and `detmir-readiness.prom` expose the
      latest OK/WARN/FAIL state independently from systemd unit result;
- [ ] `detmir-readiness.timer` is enabled for daily bundle generation;
- [ ] DetMir portal readiness endpoints return latest/bundle/verify data;
- [ ] `detmir-check --json`, `detmir-status --json` and Grafana check are green;
- [ ] rollback path for changed binaries/env files is known.

Full readiness procedure is documented in `docs/PRODUCTION_READINESS_RU.md`.

На Proxmox:

```bash
detmir-readiness --json
detmir-status --json
detmir-check --json
detmir-dlp
detmir-auto --no-heal
systemctl --failed
```

На AW server:

```bash
aw-rus-healthd --json
dlp-health-check --json
/usr/local/bin/aw-slo-monitor-rust --state-dir /var/lib/activitywatch/slo-rust-shadow --json
systemctl --failed
```

Для Telegram:

```bash
systemctl is-active tsj-guardian-bot tsj-guardian-watchdog gost-tg
```

## 11. Приоритет ближайших работ

Немедленный порядок:

1. `[done]` Создать `detmir-core`, `detmir-state`, `detmir-aw-client`.
2. `[done]` Объединить `detmir-status` и `detmir-adk-status` в один Rust binary.
3. `[done]` Перенести `detmir-check`.
4. `[done]` Сделать shadow compare `detmir-check` old/new.
5. `[done]` Перенести `detmir-dlp` wrapper.
6. `[done]` Начать `detmir-auto --no-heal`.
7. `[done]` Накопить scheduled shadow/prod parity для `detmir-auto-rust`, затем выполнить controlled switch.
8. `[done]` Перенести `detmir-heal-safe` с dry-run/apply guardrails и подключить к Rust auto.
9. `[done]` Наблюдать timer цикл Rust production и отключить redundant shadow timer.
10. `[done]` Перенести `tsj-guardian-watchdog.sh` на Rust service loop.
11. `[done]` Перенести AW-RUS SLO monitor/summary path на Rust:
    `aw-slo-monitor-rust` пишет `aw-slo-samples.jsonl`,
    `aw-slo-summary.json` и `aw-slo-summary.txt`; production включен через
    `/etc/systemd/system/aw-slo-monitor.service.d/20-rust-switch.conf`.
12. `[done]` Выполнить controlled correction старых ложных SLO bad-сэмплов:
    backup создан, 133 записи с единственной причиной
    `worktime_today_html body too small ... < 5000` исправлены, 59
    не-ложных/неоднозначных bad-сэмплов оставлены в истории.
13. `[done]` Вынести Telegram `/status` aggregation layer в Rust:
    `tsj-guardian-status --status-text` собирает полный текст статуса, Python
    runtime Telegram оставлен и использует старую сборку только как fallback.
14. `[done]` Вынести incident/escalation decision path в Rust:
    `tsj-guardian-status` принимает JSON через stdin и решает suggestions,
    transient quorum/defer и timeout escalation/fallback. Python runtime
    выполняет только side effects и сохраняет fallback на старую логику.
15. `[done]` Вынести operator action routing/gating в Rust:
    `tsj-guardian-status --operator-action-decision` нормализует action aliases,
    назначает handler/risk, блокирует неизвестные action и защищает
    update install/rollback confirm без pending-запроса. Python runtime
    продолжает выполнять команды и сохраняет fallback.
16. `[done]` Вынести DLP policy/mode decision path в Rust:
    `tsj-guardian-status --dlp-policy-decision` определяет monitor/enforce/mixed,
    считает block/total по endpoint groups, строит toggle/target policy plan и
    changed_rules. Python runtime продолжает выполнять API PUT и Windows policy
    sync, fallback на старую Python-логику сохранен.
17. `[done]` Вынести confirmation state machines в Rust:
    `tsj-guardian-status --confirmation-decision` решает TTL/expire, cancel,
    first_confirm и apply code/stage validation для pfSense, OpenVPN,
    Proxmox selection и Proxmox restore. Python runtime продолжает создавать
    pending-запросы и выполнять side effects после `allowed=true`, fallback на
    старую Python-валидацию сохранен.
18. `[done]` Вынести autoheal/action plan extraction в Rust:
    `tsj-guardian-status --autoheal-plan-decision` классифицирует AW-Rus
    failures в Windows collector heal, server-side DLP heal, worktime rebuild,
    SLO no-direct-target и задержку перед повторной проверкой. Python runtime
    продолжает выполнять recovery-команды и повторный probe, fallback на старую
    Python-маршрутизацию сохранен. Production deploy проверен на Proxmox:
    sample decisions OK, `tsj-guardian-bot`, `tsj-guardian-watchdog` и
    `gost-tg` active, `detmir-status` OK / `ok_for_operator=true`, свежий
    journal после правильной выкладки helper без повторных unexpected-argument
    warnings.
19. `[done]` Укрепить deploy artifacts после миграции:
    `deploy_aw_server.yml` переведен на единый `aw_rust_release_dir` для Rust
    binary `stat`/`copy`, чтобы `CARGO_TARGET_DIR` работал одинаково для
    Proxmox/Telegram и AW-server deploy. Добавлен
    `scripts/check_detmir_rust_release_artifacts.sh`; проверено
    `CARGO_TARGET_DIR=<OPERATOR_HOME>/.cache/detmir-adk-rust-target cargo build
    --release --workspace`, artifact check OK, `deploy_aw_server.yml
    --syntax-check` OK.
20. `[done]` Прогнать production AW-server deploy contract и закрыть найденные
    эксплуатационные дефекты:
    - Influx tokens для deploy берутся из окружения; при восстановлении с
      сервера значения не печатать.
    - Legacy DB merge выключен по умолчанию через
      `aw_legacy_db_merge_enabled: false`, потому что backup SQLite на
      12G root FS уперся в `No space left on device`. Повторять merge только
      после отдельного disk plan.
    - Browser smoke по умолчанию принудительно использует `chromium-cli`;
      `node-playwright` на текущем Node ломается `rimraf: callback function
      required`.
    - `aw-rus-healthd-rust` теперь проверяет AW interactive buckets по
      `metadata.end`, а event buckets выбирают свежайшее событие из окна
      `limit=20` с учетом `timestamp + duration`; это устраняет ложные
      stale/warn на длинных ActivityWatch событиях и guard heartbeat ordering.
    - После reset runtime budget `AWatchRusCollectorGuard` и однократного
      перезапуска AW watcher tasks production verification зеленый:
      `aw-rus-healthd-rust` `ok=14 warn=0 fail=0`, `detmir-status` OK,
      `systemctl --failed` на AW и Proxmox пусто, `deploy_aw_server.yml
      --syntax-check` OK.
21. `[done]` Выполнить безопасную уборку AW app data и защитить timers от
    лавинного восстановления:
    - удалены только старые browser-smoke run-директории, manual profile,
      неиспользуемый `/opt/activitywatch/releases/v0.13.2` и неиспользуемый
      `/opt/activitywatch/aw-rus-ops/venv`;
    - текущий `aw-server-rust-v0.13.2`, SQLite DB/WAL/SHM и rollback-critical
      backups не удалялись;
    - root FS улучшен примерно с `96%` до `85%`;
    - AW timers переведены на staggered `OnCalendar`, чтобы после ручного
      restart/daemon-reload не запускать все missed persistent jobs одной
      пачкой;
    - legacy `aw_to_influx_exporter.service` удален из systemd, потому что
      production уже использует Rust DLP/worktime Influx exporters;
    - `aw-rus-healthd-rust` получил timeout для wrapper-команд, чтобы
      `aw-health-check`/`dlp-health-check` не могли подвесить healthd.
22. `[done]` Перенести DLP content analyzer text path в Rust:
    - добавлен `dlp-content-analyzer` crate;
    - CLI совместим с `aw-dlp-content-analyzer --text --dictionary-pack
      --regex-pack`;
    - реализованы dictionary/regex pack matching и checksum validators для
      ИНН, СНИЛС, паспорта РФ;
    - позиции совпадений считаются в Python-compatible character offsets, не
      byte offsets;
    - image/OCR path оставлен через legacy Python fallback, чтобы не тащить
      OCR/Tesseract в Rust на этом шаге;
    - `/usr/local/bin/aw-dlp-content-analyzer` теперь wrapper, который
      предпочитает `/usr/local/bin/aw-dlp-content-analyzer-rust`, но сохраняет
      Python fallback;
    - parity на AW server проверен на `152-fz-pdn`, `contacts`, `secrets`,
      `financial`: match counts, values и offsets совпали;
    - artifact check расширен `dlp-content-analyzer`.
23. `[done]` Перенести DLP admin CLI в Rust:
    - добавлен `dlp-admin-cli` crate;
    - сохранены команды `policies list/active`, `incidents list`,
      `cases list/create`, `health check`;
    - HTTP client использует no-proxy для локальных AW/DLP сервисов;
    - `/usr/local/bin/dlp-admin-cli` установлен как Rust production CLI,
      legacy `/usr/local/bin/dlp-admin-cli.py` больше не является repo/runtime
      path; рабочий CLI: `/usr/local/bin/dlp-admin-cli`;
    - parity на AW server совпал с Python для `health`, `policies active`,
      `incidents list --since-hours 24 --limit 5`, `cases list --limit 5`;
    - production verification зеленый: AW failed units 0,
      `aw-rus-healthd-rust` 14/0/0, `dlp-health-check` 22/0/0,
      `detmir-status` OK, `detmir-check` OK, Proxmox failed units 0;
    - artifact check расширен `dlp-admin-cli`.
24. `[done]` Перенести DLP policy engine service в Rust:
    - добавлен `dlp-policy-engine` crate;
    - сохранены существующая SQLite schema/DB и API: policies
      list/create/get/update/delete, active bundle/version,
      submit/approve/draft/activate, rollback, agent heartbeat/desired,
      audit;
    - checksum policy совместим с Python:
      `json.dumps(..., ensure_ascii=False, sort_keys=True,
      separators=(",", ":"))` + SHA-256;
    - env contract сохранен:
      `AW_DLP_POLICY_ENGINE_BIND_HOST`, `AW_DLP_POLICY_ENGINE_PORT`,
      `AW_DLP_POLICY_ENGINE_DB_PATH`;
    - shadow parity на AW server совпал с Python для list/active/version и
      agent heartbeat/desired;
    - Rust исправляет documented `/api/0/dlp/policies/audit?limit=N`: legacy
      FastAPI возвращал 422 из-за route-order конфликта с `{policy_id}`;
    - production `/usr/local/bin/aw-dlp-policy-engine-rust` подключен через
      `/etc/systemd/system/aw-dlp-policy-engine.service.d/20-rust-switch.conf`;
    - rollback: удалить этот drop-in, `systemctl daemon-reload`, restart
      `aw-dlp-policy-engine.service`;
    - production verification зеленый: active policy `default-policy` v6,
      audit endpoint OK, AW failed units 0, `aw-rus-healthd-rust` 14/0/0,
      `dlp-health-check` 22/0/0, `detmir-status` OK, `detmir-check` OK,
      Proxmox failed units 0;
    - artifact check расширен `dlp-policy-engine`.
25. `[done]` Перенести DLP case management service в Rust:
    - добавлен `dlp-case-management` crate;
    - сохранены существующая SQLite schema/DB и API: `/health`, cases
      list/create/get/update, comments list/create, Hayabusa forensics link;
    - сохранены self-test rejection и evidence SHA-256 contract:
      `json.dumps(..., ensure_ascii=False, sort_keys=True,
      separators=(",", ":"))` + SHA-256;
    - env contract сохранен:
      `AW_DLP_CASE_BIND_HOST`, `AW_DLP_CASE_PORT`, `AW_DLP_CASE_DB_PATH`;
    - local HTTP smoke на temp DB проверил create/list/patch/comment/hayabusa
      link/self-test reject;
    - shadow parity на AW server совпал с Python для health, case list/filter,
      case detail и comments на копии production DB;
    - production `/usr/local/bin/aw-dlp-case-management-rust` подключен через
      `/etc/systemd/system/aw-dlp-case-management.service.d/20-rust-switch.conf`;
    - rollback: удалить этот drop-in, `systemctl daemon-reload`, restart
      `aw-dlp-case-management.service`;
    - production verification зеленый: case list OK/latest id 35,
      AW failed units 0, `aw-rus-healthd-rust` 14/0/0, `dlp-health-check`
      22/0/0, `detmir-status` OK, `detmir-check` OK, Proxmox failed units 0;
    - artifact check расширен `dlp-case-management`.
26. `[done]` Перенести DLP compliance report scheduler/generator в Rust:
    - добавлен `dlp-compliance` crate;
    - сохранены CLI `--month`, `--profile`, `--profiles`, `--stdout-json`;
    - сохранены env fallback для AW API: `AW_DLP_AW_API_BASE`,
      `AW_SERVER_URL`, default `http://127.0.0.1:5600`;
    - сохранены output/template envs и artifact names:
      `<profile>-<YYYY-MM>.html/json`;
    - shadow parity на AW server в temp output dirs совпал с Python для
      `152-fz,pci-dss`: profile/period/aw_api_base/stats и наличие artifacts;
    - production `/usr/local/bin/aw-dlp-compliance-rust` подключен через
      `/etc/systemd/system/aw-dlp-report-scheduler.service.d/20-rust-switch.conf`;
    - rollback: удалить этот drop-in и `systemctl daemon-reload`;
    - production oneshot run успешен: `152-fz-2026-06` и `pci-dss-2026-06`
      html/json созданы, timer active, AW failed units 0,
      `aw-rus-healthd-rust` 14/0/0, `dlp-health-check` 22/0/0,
      `detmir-status` OK, `detmir-check` OK, Proxmox failed units 0;
    - artifact check расширен `dlp-compliance`.
27. `[done]` Перенести Hayabusa case alert/link helpers в Rust:
    - добавлен `hayabusa-tools` crate с бинарниками
      `aw-hayabusa-case-alert-rust` и `aw-hayabusa-link-case-rust`;
    - сохранены CLI/env контракты legacy helpers:
      `--case-id`, `--intake-json`, `--case-api-base`, `--mode`,
      `--link-source`, `AW_HAYABUSA_*`;
    - `case-alert` сохраняет scoring/severity, `top_rules`, comment text,
      case create/update/link path и Telegram alert text;
    - Telegram request errors в Rust не печатают bot token/URL;
    - `/usr/local/bin/aw-hayabusa-case-alert` и
      `/usr/local/bin/aw-hayabusa-link-case` теперь wrapper-команды, которые
      предпочитают Rust binaries и fallback на Python из
      `/opt/activitywatch/aw-rus-ops/hayabusa`;
    - shadow compare на реальном `/opt/hayabusa/state/latest-intake.json` с
      `AW_HAYABUSA_AUTO_CASE_ENABLED=false` и
      `AW_HAYABUSA_TELEGRAM_ENABLED=false` дал `summary_equal=true`;
    - production verification зеленый: AW failed units 0,
      `aw-rus-healthd-rust` 14/0/0, `dlp-health-check` 22/0/0,
      `detmir-status` OK, Proxmox failed units 0;
    - artifact check расширен `aw-hayabusa-case-alert-rust` и
      `aw-hayabusa-link-case-rust`.
28. `[done]` Перенести remaining Hayabusa offline helpers в Rust:
    - `hayabusa-tools` расширен бинарниками
      `aw-hayabusa-from-windows-rust` и `aw-hayabusa-autoprocess-rust`;
    - `from-windows` сохраняет server-side workflow:
      Ansible WinRM export, latest zip discovery, fetch в drop-dir,
      `aw-hayabusa accept`, `process-inbox`, optional link-case и печать
      `LATEST_INTAKE`;
    - `autoprocess` сохраняет drop-dir scanner, lock file,
      `.caseid`/`.meta.json` sidecars, host/mode/link_source inference,
      case-alert capture, sidecar/archive move в report dir;
    - `/usr/local/bin/aw-hayabusa-from-windows` и
      `/usr/local/bin/aw-hayabusa-autoprocess` теперь wrapper-команды,
      которые предпочитают Rust и fallback на Python из
      `/opt/activitywatch/aw-rus-ops/hayabusa`;
    - `aw-hayabusa-drop.service` теперь запускает
      `/usr/local/bin/aw-hayabusa-autoprocess` напрямую, без `python3`, чтобы
      wrapper мог выбрать Rust;
    - no-mutation smoke на AW server: `--help` для всех Hayabusa helpers,
      empty drop-dir для `autoprocess`, `systemctl cat` ExecStart OK;
    - production verification зеленый: AW failed units 0,
      `aw-rus-healthd-rust` 14/0/0, `dlp-health-check` 22/0/0,
      `detmir-status` OK, Proxmox failed units 0;
    - artifact check расширен `aw-hayabusa-from-windows-rust` и
      `aw-hayabusa-autoprocess-rust`.
29. `[done]` Перенести AW ops helper `aw-health-check` в Rust:
    - добавлен crate `aw-health-check`;
    - сохранены проверки legacy shell: systemd services/timer,
      ActivityWatch `/api/0/info`, Worktime `/health`, DLP transport
      freshness через `dlp-health-check --json`, и drift checks для
      `startOfDay`, `always_active_pattern`, `landingpage`;
    - env contract читается из процесса и `/etc/activitywatch/aw-server.env`;
    - production `/usr/local/bin/aw-health-check` теперь Rust-required wrapper:
      вызывает `/usr/local/bin/aw-health-check-rust` и падает с ошибкой, если
      Rust binary отсутствует;
    - shadow parity на AW server совпал с shell по exit code и ключевым
      health-строкам;
    - production verification зеленый: `aw-health-check` OK,
      `aw-rus-healthd-rust` 14/0/0, `dlp-health-check` 22/0/0,
      `detmir-status` OK, `ok_for_operator=true`, failed units 0;
    - artifact check расширен `aw-health-check`.
30. `[done]` Перенести AW ops helper `check-aw-data` в Rust:
    - добавлен crate `check-aw-data`;
    - корневой `check-aw-data.sh` стал Rust-first wrapper с fallback на
      `scripts/legacy/check-aw-data.sh`;
    - на AW server production `/usr/local/bin/check-aw-data` теперь wrapper,
      который предпочитает `/usr/local/bin/check-aw-data-rust`, по умолчанию
      использует `http://127.0.0.1:5600` и fallback на
      `/opt/activitywatch/aw-rus-ops/check-aw-data.sh`;
    - сохранены операторские статусы `FRESH`, `STALE`, `DEAD`, `EMPTY`,
      `EVENT-DRIVEN`, `INACTIVE` и CORS probe;
    - быстрый режим по умолчанию использует `metadata.end` из
      `/api/0/buckets`; глубокое чтение event ids включается флагом
      `--with-event-ids`;
    - production verification: `/usr/local/bin/check-aw-data --no-color`
      завершился за 2s, buckets fresh/event-driven/inactive as expected,
      `CORS: OK (HTTP 200)`;
    - final gates зеленые: AW failed units 0, `aw-rus-healthd-rust` 14/0/0,
      `dlp-health-check` 22/0/0, `detmir-auto --no-heal` OK,
      `detmir-status` OK и `ok_for_operator=true`;
    - artifact check расширен `check-aw-data`.
31. `[done]` Перенести AW maintenance helper `aw-prune-local-state` в Rust:
    - добавлен crate `aw-prune-local-state`;
    - `/usr/local/bin/aw-prune-local-state.sh` теперь Rust-first wrapper,
      который без аргументов запускает Rust binary в legacy-compatible
      `--apply` режиме и fallback на shell из
      `/opt/activitywatch/aw-rus-ops/aw-prune-local-state.sh`;
    - Rust binary по умолчанию работает как dry-run, поддерживает `--json`,
      `--apply`, retention/keep параметры и строгий allowlist путей;
    - safety policy: не удалять SQLite DB вне `backups/db`, не удалять
      `switch-backups`, `before-rust`, `rollback` и корневые state каталоги;
    - cleanup расширен на старые `browser-smoke` run-директории как project
      app data; rollback-critical backups и AW SQLite DB не трогались;
    - production recovery во время проверки: rootfs AW server был 100%
      заполнен, `aw-server-rust` попал в `poisoned lock`; через Rust helper
      удалены старые browser-smoke runs на 347 MB суммарно, затем
      `activitywatch-server.service` восстановлен без удаления DB;
    - final gates зеленые: AW failed units 0, `aw-rus-healthd-rust` 14/0/0,
      `dlp-health-check` 22/0/0, `check-aw-data` OK, `detmir-auto --no-heal`
      OK, `detmir-status` OK и `ok_for_operator=true`;
    - Telegram bot/watchdog временно остановлены по операторской команде и не
      перезапускались в рамках этого шага;
    - artifact check расширен `aw-prune-local-state`.
32. `[done]` Устранить нехватку места на AW server через Proxmox resize:
    - CT `203` (`aw-server`, `<AW_SERVER_HOST>`) rootfs расширен через Proxmox
      `pct resize 203 rootfs +20G`;
    - перед resize сохранен config backup:
      `/var/lib/detmir-ai/switch-backups/ct203-aw-server.before-rootfs-resize-20260602T054826Z.conf`;
    - rootfs изменился с `12G` на `32G`, свободное место стало около `20G`;
    - Proxmox storage `local-btrfs` после resize имеет около `106G`
      доступного места;
    - AW `/api/0/info` и `/api/0/buckets` проверены с AW server и Proxmox;
    - final gates зеленые: AW health 14/0/0, DLP health 22/0/0,
      `detmir-check` service_warnings=0, `detmir-auto --no-heal` OK,
      `detmir-status` OK и `ok_for_operator=true`, failed units 0;
    - Telegram bot/watchdog оставлены `inactive` по операторской команде.
33. `[done]` Остановить неконтролируемый рост `aw-session-events`:
    - read-only SQLite audit показал, что `/var/lib/activitywatch/aw-server-rust/sqlite.db`
      занимает около `6.8G`, `freelist_count=0`; VACUUM сам по себе не
      освободит место, потому что размер занят live events;
    - основной источник роста: `aw-session-events_HOST-EXAMPLE` - около
      `6.9M` строк и `~5GB` payload, с пиками `1.2M-2.4M` process-level
      событий в сутки за 2026-05-29..2026-06-01;
    - production live config на RDP был `pollSeconds=5` и
      `sessionEvents.processEventsEnabled=true`;
    - отключена постоянная process-level публикация:
      `aw_windows_process_events_enabled=false` в production/example/default
      Ansible vars и `sessionEvents.processEventsEnabled=false` в live
      `C:\ProgramData\AWatch-rus\deployment-config.json`;
    - live config backup сохранен на RDP:
      `C:\ProgramData\AWatch-rus\switch-backups\deployment-config.before-disable-process-events-20260602T061836Z.json`;
    - старый `worktime-session-collector.ps1` PID `8476` остановлен, collector
      поднят заново штатными `ActivityWatch Launch [...]` tasks/guard;
    - delta-gate: `metadata.end` bucket `aw-session-events_HOST-EXAMPLE`
      остался `2026-06-02T06:27:11.197Z` через 75 секунд, постоянный поток
      остановлен;
    - базовый сбор не сломан: `detmir-check --json` OK,
      `detmir-status` severity OK, AW failed units 0, Proxmox failed units 0,
      AW API и Worktime API отвечают;
    - SQLite row deletion/compact не выполнялись на этом этапе: сначала
      стабилизирован источник роста, retention/trim старых process events -
      отдельный destructive этап с backup.
34. `[done]` Controlled trim старых process-level `aw-session-events`:
    - перед mutation сохранен полный rollback backup SQLite DB/WAL/SHM:
      `/var/lib/activitywatch/backups/db/aw-sqlite-before-session-events-trim-20260602T064127Z`;
    - на время операции остановлены AW-related timers/services и
      `activitywatch-server.service`, чтобы не было writer'ов к SQLite;
    - scoped delete удалил только события bucket
      `aw-session-events_HOST-EXAMPLE` с `eventType=process_start` или
      `eventType=process_stop`;
    - удалено `6,906,190` шумных process-level событий;
    - сохранены logon events: после trim в `aw-session-events_HOST-EXAMPLE`
      осталось `174` события, recent samples имеют `eventType=logon`;
    - `PRAGMA integrity_check` до и после `VACUUM`: `ok`;
    - DB уменьшилась с `6.8G` до `350M`, rootfs AW server вернулся к
      `38%` использования и около `19G` free;
    - после restart AW был `poisoned lock` из-за timer запросов во время
      старта; выполнен controlled restart при остановленных timers, затем
      `/api/0/info` и `/events?limit=1` вернулись OK;
    - timers включены обратно, failed units на AW и Proxmox: `0`;
    - final gates зеленые: `aw-health-check` OK, `dlp-health-check`
      `22/0/0`, `detmir-check` bucket_ok=8 stale=0 dead=0,
      `detmir-auto --no-heal` OK, `detmir-status` severity OK и
      `ok_for_operator=true`;
    - Telegram bot/watchdog оставлены `inactive` по операторской команде.
35. `[done]` Добавить Rust guard от повторного роста AW SQLite:
    - добавлен crate `aw-db-health`;
    - helper read-only открывает `/var/lib/activitywatch/aw-server-rust/sqlite.db`
      через SQLite read-only flags и не меняет DB;
    - проверяет:
      - размер `sqlite.db` (`warn=2GiB`, `fail=5GiB` по умолчанию);
      - размер `sqlite.db-wal` (`warn=256MiB`, `fail=1GiB`);
      - число строк `aw-session-events_<host>` (`warn=10000`, `fail=100000`);
      - recent process-level events за окно 600 секунд
        (`warn=1`, `fail=100`);
      - latest `eventType/source` для быстрой диагностики;
      - опционально `--windows-config` и
        `sessionEvents.processEventsEnabled`;
    - поддерживает text output и `--json`;
    - `aw-health-check-rust` теперь вызывает `/usr/local/bin/aw-db-health --json`
      и включает DB growth guard в общий AW health path;
    - `scripts/check_detmir_rust_release_artifacts.sh` требует
      `aw-db-health`;
    - `ansible/deploy_aw_server.yml` устанавливает
      `/usr/local/bin/aw-db-health` и обновленный
      `/usr/local/bin/aw-health-check-rust`; установка health helpers
      продублирована до Influx token assert, чтобы DB guard обновлялся даже
      если playbook позже останавливается на пустом внешнем Influx token;
    - production `/usr/local/bin/aw-db-health --json`:
      `ok=true`, `db=352.3MiB`, `wal=4.0MiB`, session rows `174`,
      recent process events `0`, latest `eventType=logon`;
    - production `/usr/local/bin/aw-health-check`: `AW DB growth guard passed`;
    - local gates: `cargo fmt --all -- --check`, `cargo test -p aw-db-health`
      `3 passed`, `cargo test -p aw-health-check` `2 passed`,
      release build OK, artifact check OK;
    - final gates зеленые: `dlp-health-check` `22/0/0`, `detmir-check` OK,
      `detmir-auto --no-heal` OK, `detmir-status` severity OK,
      failed units на AW/Proxmox `0`;
    - Telegram bot/watchdog оставлены `inactive`.
35.1. `[done]` Добавить автономную guarded чистку AW SQLite:
    - добавлен crate `aw-db-maintenance`;
    - helper dry-run по умолчанию, mutation только с `--apply`;
    - allowlist удаления: только bucket `aw-session-events_<host>` и только
      события `eventType=process_start` или `eventType=process_stop`;
    - logon/session events не удаляются;
    - retention по умолчанию `7` дней;
    - перед удалением создается SQLite backup в
      `/var/lib/activitywatch/backups/db/aw-sqlite-before-db-maintenance-*.db`;
      если строк к удалению нет, backup не создается и DB не пишется;
    - удаление chunked, без `VACUUM`, чтобы не создавать длительный downtime;
    - добавлены `aw-server/aw-db-maintenance.service` и
      `aw-server/aw-db-maintenance.timer`;
    - timer включен на AW server: `OnCalendar=Sun *-*-* 03:30:00`,
      `RandomizedDelaySec=15m`, `Persistent=true`;
    - production dry-run: `planned_delete_rows=0`, `deleted_rows=0`;
    - ручной service smoke: `Result=success`, `ExecMainStatus=0`,
      `planned_delete_rows=0`, `deleted_rows=0`, `backup_created=false`;
    - production timer active; ближайший запуск по remote `systemctl
      list-timers`: `Sun 2026-06-07 03:40:05 UTC`;
    - gates: `cargo fmt --all -- --check`, `cargo test -p aw-db-maintenance`
      `3 passed`, `cargo clippy -p aw-db-maintenance --all-targets -- -D
      warnings`, release build OK, artifact check OK, `quality-gate.sh` OK,
      `ansible-playbook deploy_aw_server.yml --syntax-check` OK;
    - final production gates: AW failed units `0`, `aw-db-health` OK
      (`sqlite.db=359.6MiB`, WAL `4.0MiB`, session rows `174`, recent process
      events `0`, latest `eventType=logon`), DetMir status OK with
      `dlp_counts={ok:22,warn:0,fail:0}`, `ok_for_operator=true`.
35.2. `[done]` Закрепить nightly VACUUM для AW SQLite:
    - `aw-db-maintenance` получил отдельный `--vacuum` режим; weekly trim
      остается отдельной задачей и не смешивается с compaction;
    - добавлены `aw-server/aw-db-vacuum.service` и
      `aw-server/aw-db-vacuum.timer`;
    - timer schedule: `OnCalendar=*-*-* 02:10:00`,
      `RandomizedDelaySec=10m`, `Persistent=true`; Ansible не включает timer
      без явного opt-in `aw_db_vacuum_timer_enabled=true`;
    - если opt-in не задан, `deploy_aw_server.yml` оставляет unit-файлы на
      сервере, но держит `aw-db-vacuum.timer` в `disabled/stopped`;
    - nightly job перед VACUUM останавливает `activitywatch-server.service`,
      делает rollback backup SQLite DB, выполняет `VACUUM INTO`, проверяет
      `PRAGMA integrity_check`, сохраняет владельца и режим файла и
      поднимает server обратно;
    - apply-режимы weekly trim и nightly VACUUM используют общий lock
      `/run/aw-db-maintenance.lock`, поэтому одновременный запуск завершается
      fail-closed без записи в SQLite;
    - VACUUM нельзя запускать в рабочее время, во время активного
      расследования/снятия доказательств, backup/restore, неизвестного
      состояния `activitywatch-server.service` или если уже есть maintenance
      lock;
    - dry-run проверки:
      `aw-db-maintenance --vacuum --json` и обычный
      `aw-db-maintenance --json`; они не создают backup и не пишут в DB;
    - включить timer явно:
      `ansible-playbook -i inventory.ini deploy_aw_server.yml -e aw_db_vacuum_timer_enabled=true`;
    - отключить timer:
      `systemctl disable --now aw-db-vacuum.timer`;
    - rollback: остановить `activitywatch-server.service`, заменить
      `/var/lib/activitywatch/aw-server-rust/sqlite.db` из последнего
      `/var/lib/activitywatch/backups/db/aw-sqlite-before-db-vacuum-*.db`,
      вернуть владельца/режим, запустить service, проверить `aw-db-health` и
      `detmir-status`;
    - local gates: `cargo fmt --all -- --check`, `cargo test --workspace`,
      `cargo clippy --workspace --all-targets -- -D warnings`,
      `cargo build --workspace --release`, systemd unit verify,
      `ansible-playbook -i inventory.ini deploy_aw_server.yml --syntax-check`,
      `scripts/check_detmir_rust_release_artifacts.sh`,
      `scripts/quality-gate.sh`;
36. `[done]` Устранить blocker полного AW server deploy на Influx token:
    - проблема: `deploy_aw_server.yml` падал на assert
      `aw_worktime_influx_enabled=true`, потому что локальные env
      `AW_WORKTIME_INFLUX_TOKEN` и `AW_DLP_INFLUX_TOKEN` пустые;
    - runtime `/etc/activitywatch/aw-server.env` на AW server уже содержал оба
      token, поэтому правильный путь - сохранить remote secrets, а не
      отключать exporters;
    - playbook теперь через `slurp` с `no_log: true` читает текущий
      `/etc/activitywatch/aw-server.env`, выбирает effective token:
      local env token если задан, иначе existing remote token;
    - asserts проверяют effective tokens и не печатают секреты;
    - запись `/etc/activitywatch/aw-server.env` использует effective tokens,
      чтобы full deploy не затирал рабочие Influx credentials пустыми
      значениями;
    - полный `ansible-playbook -i inventory.ini deploy_aw_server.yml
      -e aw_rust_release_dir=<OPERATOR_HOME>/.cache/detmir-adk-rust-target/release`
      прошел до конца: `failed=0`, `ok=282`;
    - final gates после deploy зеленые: `aw-db-health` OK,
      `aw-health-check` OK, `dlp-health-check` `22/0/0`,
      `detmir-check` OK, `detmir-auto --no-heal` OK,
      `detmir-status` severity OK, failed units на AW/Proxmox `0`;
    - Telegram bot/watchdog оставлены `inactive`.
37. `[done]` Закрепить safe default для Windows `processEventsEnabled`:
    - исправлены Windows deploy/recovery defaults, чтобы новый deploy,
      ensemble/domain deploy, hardening recovery и общий
      `New-ActivityWatchDeploymentConfig` не включали process-level
      `aw-session-events` без явного параметра;
    - изменены `windows/deploy-single-user.ps1`,
      `windows/deploy-ensemble.ps1`, `windows/deploy-domain-users.ps1`,
      `windows/hardening-recovery.ps1`,
      `windows/ActivityWatch.Windows.Common.psm1` и
      `windows/validate-deployment.ps1`;
    - синхронизированы текстовые копии в
      `install-kit-awindows-20260427-211240/windows/`;
    - invariant: existing live config сохраняется, но missing/new config
      default всегда `sessionEvents.processEventsEnabled=false`; включать
      можно только явно и временно для forensic/debug окна;
    - local gates: PowerShell AST parse для измененных scripts/modules OK,
      `ansible-playbook -i inventory.ini deploy_aw_windows.yml --syntax-check`
      OK;
    - production deploy: обновленные PowerShell файлы скопированы в
      `C:\Program Files\AWatch-rus\windows` с UTF-8 BOM для Windows
      PowerShell 5.1; remote backup перед заменой:
      `C:\ProgramData\AWatch-rus\switch-backups\windows-safe-default-20260602T091213Z`;
    - `hardening-recovery.ps1` выполнен по existing
      `C:\ProgramData\AWatch-rus\deployment-config.json`; после recovery
      config остался `processEventsEnabled=false`, `logonEnabled=true`,
      `pollSeconds=5`, users включают `HOST-EXAMPLE\Администратор`;
    - exact task check: `ActivityWatch Launch [HOST-EXAMPLE_Администратор]`
      существует, ошибочный
      `ActivityWatch Launch [HOST-EXAMPLE_Administrator]` отсутствует,
      `ActivityWatch Recovery` существует;
    - `AWatchRusCollectorGuard` running/automatic; Windows
      `validate-deployment.ps1` вернул `overallOk=True`;
    - production AW DB guard: session rows `174`, recent process events `0`,
      latest `eventType=logon`, DB около `353MiB`;
    - final gates зеленые: `check-aw-data` buckets fresh/event-driven OK,
      `aw-health-check` OK, `dlp-health-check` `22/0/0`,
      `detmir-check` bucket_ok `8`, stale `0`, dead `0`,
      `detmir-auto --no-heal` OK, `detmir-status` severity OK и
      `ok_for_operator=true`, failed units на AW/Proxmox `0`;
    - краткий transient после recovery: один прогон `detmir-auto` увидел
      `file-operations sendFailuresDelta`; повтор через 30 секунд показал
      `sendFailuresDelta=0`, поэтому коррекция collectors не потребовалась;
    - Telegram bot/watchdog оставлены `inactive` по принятому решению.
38. `[done]` Пересобрать и проверить Windows install kit:
    - выполнен штатный `./scripts/rebuild_install_kit.sh`;
    - пересобраны:
      `install-kit-awindows-20260427-211240/`,
      `install-kit-awindows-20260427-211240.zip`,
      `install-kit-awindows-20260427-211240.tar.gz` и `MANIFEST.txt`;
    - `./scripts/check_install_kit_vs_repo.sh`: compared `62`,
      missing `0`, mismatched `0`, PowerShell mismatches `0`;
    - manifest checksum verification: OK, entries `66`;
    - archives verification: zip и tar.gz распакованы во временные каталоги,
      опасные defaults не найдены; подтверждены
      `[bool]$ProcessEventsEnabled = $false`,
      `effectiveProcessEventsEnabled ... else { $false }`,
      `sessionProcessEventsEnabled ... else { $false }`,
      `aw_windows_process_events_enabled: false`;
    - PowerShell AST parse для kit scripts/modules OK;
    - `ansible-playbook -i inventory.example.ini deploy_aw_windows.yml
      --syntax-check` из install-kit ansible каталога OK.
39. `[done]` Перенести install-kit consistency checker на Rust:
    - добавлен crate `check-install-kit-vs-repo`;
    - `scripts/check_install_kit_vs_repo.sh` теперь Rust-first wrapper:
      ищет `CHECK_INSTALL_KIT_VS_REPO_RUST`,
      `$CARGO_TARGET_DIR/release/check-install-kit-vs-repo`,
      `adk-rust/target/release/check-install-kit-vs-repo`,
      `/usr/local/bin/check-install-kit-vs-repo`, затем использует Python
      fallback;
    - сохранен текстовый output contract:
      `Compared files`, `Missing in repo`, `Mismatched content`,
      `PowerShell mismatches`; добавлен `--json`;
    - поведение усилено: missing/mismatch теперь дают non-zero exit code,
      чтобы рассинхрон install-kit не проходил молча;
    - `scripts/check_detmir_rust_release_artifacts.sh` теперь требует
      `check-install-kit-vs-repo`;
    - после обновления wrapper выполнен `./scripts/rebuild_install_kit.sh`,
      чтобы install kit нес актуальный Rust-first wrapper;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      check-install-kit-vs-repo` (`3 passed`), `cargo clippy -p
      check-install-kit-vs-repo --all-targets -- -D warnings`, release build
      OK, `./scripts/check_install_kit_vs_repo.sh` OK, negative mismatch smoke
      rc `1`, `./scripts/validate_install_kit.sh` OK, artifact check OK.
40. `[done]` Перенести install-kit validator на Rust:
    - добавлен crate `validate-install-kit`;
    - `scripts/validate_install_kit.sh` теперь Rust-first wrapper:
      ищет `VALIDATE_INSTALL_KIT_RUST`,
      `$CARGO_TARGET_DIR/release/validate-install-kit`,
      `adk-rust/target/release/validate-install-kit`,
      `/usr/local/bin/validate-install-kit`, затем использует legacy
      Bash/Python fallback;
    - сохранен stage output:
      `[1/4] Required files presence`,
      `[2/4] Manifest checksum verification`,
      `[3/4] Manifest completeness`,
      `[4/4] Archive composition check`,
      `validate_install_kit: OK`; добавлен `--json`;
    - Rust validator проверяет required files, SHA256 из `MANIFEST.txt`,
      полноту manifest относительно install-kit directory, совпадение состава
      zip/tar.gz и корректный archive prefix;
    - `scripts/rebuild_install_kit.sh` теперь включает
      `scripts/validate_install_kit.sh` в install-kit `scripts/`;
    - `scripts/check_detmir_rust_release_artifacts.sh` теперь требует
      `validate-install-kit`;
    - после wrapper change выполнен `./scripts/rebuild_install_kit.sh`;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      validate-install-kit` (`4 passed`), `cargo clippy -p
      validate-install-kit --all-targets -- -D warnings`, release build OK,
      Rust-first `./scripts/validate_install_kit.sh` OK
      (`MANIFEST complete: 67 files tracked`, `Archives match: 68 files`),
      `./scripts/check_install_kit_vs_repo.sh` OK (`compared 62`,
      mismatches `0`), negative manifest checksum smoke rc `1`, negative
      archive mismatch smoke rc `1`, artifact check OK.
41. `[done]` Перенести InnoSetup installer verifier на Rust:
    - добавлен crate `verify-innosetup-installer`;
    - `scripts/verify_innosetup_installer.sh` теперь Rust-first wrapper:
      ищет `VERIFY_INNOSETUP_INSTALLER_RUST`,
      `$CARGO_TARGET_DIR/release/verify-innosetup-installer`,
      `adk-rust/target/release/verify-innosetup-installer`,
      `/usr/local/bin/verify-innosetup-installer`, затем использует legacy
      Wine/Bash fallback;
    - Rust verifier создает чистый Wine prefix, silent-install'ит
      `windows/installkit/innosetup/AWatch-rus-InstallKit.exe` в
      `C:\AWatchRusExtract`, сверяет payload guard/policy файлов с
      репозиторием и проверяет self-test marker
      `collector guard self-test OK`;
    - первый прогон корректно поймал устаревший installer payload:
      `aw-collector-guard.ps1` и `install-collector-guard-service.ps1`
      отличались от repo после safe-default hardening;
    - installer пересобран штатным `windows/installkit/innosetup/build_with_wine.sh`,
      после чего Rust-first verifier завершился `verify_innosetup_installer:
      OK`;
    - `scripts/check_detmir_rust_release_artifacts.sh` теперь требует
      `verify-innosetup-installer`.
42. `[done]` Перенести install-kit rebuild path на Rust:
    - добавлен crate `rebuild-install-kit`;
    - `scripts/rebuild_install_kit.sh` теперь Rust-first wrapper:
      ищет `REBUILD_INSTALL_KIT_RUST`,
      `$CARGO_TARGET_DIR/release/rebuild-install-kit`,
      `adk-rust/target/release/rebuild-install-kit`,
      `/usr/local/bin/rebuild-install-kit`, затем использует legacy Bash
      fallback;
    - Rust rebuild сохраняет `*.deployment-config.json`, пересобирает
      directory tree, `MANIFEST.txt`, `.zip` и `.tar.gz`;
    - копирование реализовано через read + replace-write, а не `fs::copy`,
      потому что текущий mounted-диск возвращал `Operation not permitted` на
      truncate/copy existing files;
    - install-kit теперь включает сам `scripts/rebuild_install_kit.sh`, чтобы
      комплект был самодостаточным для дальнейшей пересборки;
    - Rust-first rebuild verified через `validate_install_kit`,
      `check_install_kit_vs_repo`, archive script `cmp`, and release artifact
      check.
43. `[done]` Перенести `quality-gate.sh` на Rust-first orchestrator:
    - добавлен crate `quality-gate`;
    - `scripts/quality-gate.sh` теперь Rust-first wrapper:
      ищет `QUALITY_GATE_RUST`,
      `$CARGO_TARGET_DIR/release/quality-gate`,
      `adk-rust/target/release/quality-gate`, `/usr/local/bin/quality-gate`,
      затем использует legacy Bash fallback;
    - Rust orchestrator сохраняет stage contract: Bash syntax,
      ShellCheck when available, Node syntax when available, PowerShell parse
      and collector guard self-test when `pwsh` available, Ansible syntax when
      `ansible-playbook` available;
    - final gates: `cargo fmt --all -- --check`, targeted tests for
      `verify-innosetup-installer`, `rebuild-install-kit`, `quality-gate`,
      `validate-install-kit`, clippy `-D warnings`, release builds,
      Rust-first `quality-gate: OK`, install-kit validate/check OK,
      InnoSetup verifier OK, and DetMir read-only status OK with
      `ok_for_operator=true`.
44. `[done]` Перенести offline Sigma/Hayabusa IOC extraction path на Rust:
    - добавлен crate `extract-ioc-from-sigma`;
    - `scripts/build_dlp_ioc_from_hayabusa.sh` теперь Rust-first wrapper:
      ищет `EXTRACT_IOC_FROM_SIGMA_RUST`,
      `$CARGO_TARGET_DIR/release/extract-ioc-from-sigma`,
      `adk-rust/target/release/extract-ioc-from-sigma`,
      `/usr/local/bin/extract-ioc-from-sigma`, затем использует legacy Python
      `scripts/extract_ioc_from_sigma.py`;
    - Rust binary сохраняет CLI-контракт `--rules-root`, `--out-dir`,
      `--table-name` и output files `ioc_blacklist.json`,
      `ioc_blacklist.csv`, `ioc_blacklist.sql`;
    - extraction contract сохранен: `Image|endswith`,
      `CommandLine|contains`, `OriginalFileName`, `Hashes|SHA256` и SHA256
      values embedded in `Hashes` strings;
    - parity fix: Rust использует Unicode lowercase и PyYAML-compatible
      `True`/`False` rendering для YAML 1.1 boolean-like literals, чтобы не
      расширять IOC set относительно legacy Python;
    - real Hayabusa rules parity:
      `rules_scanned=4963`, `iocs_extracted=13510`,
      `commandline_contains=10192`, `original_filename=457`,
      `process_image_endswith=1171`, `sha256=1690`,
      `missing_in_rust=0`, `extra_in_rust=0`;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      extract-ioc-from-sigma` (`3 passed`), `cargo clippy -p
      extract-ioc-from-sigma --all-targets -- -D warnings`, release build OK,
      `scripts/check_detmir_rust_release_artifacts.sh` OK,
      Rust-first `scripts/quality-gate.sh` OK.
45. `[done]` Перенести `scripts/rdp-worktime-report.sh` на Rust-first helper:
    - добавлен crate `rdp-worktime-report`;
    - `scripts/rdp-worktime-report.sh` теперь Rust-first wrapper:
      ищет `RDP_WORKTIME_REPORT_RUST`,
      `$CARGO_TARGET_DIR/release/rdp-worktime-report`,
      `adk-rust/target/release/rdp-worktime-report`,
      `/usr/local/bin/rdp-worktime-report`, затем использует legacy embedded
      Python fallback;
    - Rust binary сохраняет CLI/env contract: `--day today|yesterday`,
      `--from YYYY-MM-DD --to YYYY-MM-DD`, `AW_BASE_URL`,
      `AW_WORKTIME_HOST`, `AW_WORKTIME_DEFAULT_SAMPLE_SECONDS`,
      `AW_WORKTIME_MAX_SAMPLE_SECONDS`, `OUT_DIR`;
    - output contract сохранен: `rdp-worktime-<FROM>_<TO>.csv`,
      `rdp-worktime-<FROM>_<TO>.json`, stdout paths plus `CSV:`/`JSON:`;
    - calculation contract сохранен: bucket
      `aw-worktime-sessions_<host>`, active samples by `active=true` or
      Russian/English active state, interval merge, sampleSeconds/pollSeconds,
      duration and next-timestamp fallback, canonical `HOST\user` userId;
    - live parity on yesterday against Python fallback:
      `rust_csv_rows=3`, `python_csv_rows=3`, `csv_equal=True`,
      JSON `host/bucket_id/from/to/rows` all equal;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      rdp-worktime-report` (`3 passed`), `cargo clippy -p
      rdp-worktime-report --all-targets -- -D warnings`, release build OK,
      `scripts/check_detmir_rust_release_artifacts.sh` OK,
      `scripts/quality-gate.sh` OK, DetMir read-only status OK with
      `dlp_counts={ok:22,warn:0,fail:0}` and `ok_for_operator=true`.
46. `[done]` Перенести Proxmox DetMir contour smoke на Rust-first helper:
    - добавлен crate `aw-contour-smoke`;
    - `scripts/aw-contour-smoke-gateway.sh` теперь Rust-first wrapper:
      ищет `AW_CONTOUR_SMOKE_RUST`,
      `$CARGO_TARGET_DIR/release/aw-contour-smoke`,
      `adk-rust/target/release/aw-contour-smoke`,
      `/usr/local/sbin/aw-contour-smoke`,
      `/usr/local/bin/aw-contour-smoke`, затем использует legacy Bash
      fallback;
    - `scripts/aw-contour-smoke-local.sh` умеет временно доставлять release
      binary на Proxmox для shadow smoke без постоянного platform change;
    - remote parity: Rust и legacy оба вернули `OK=30 WARN=0 FAIL=3 SKIP=0`,
      `rc=2`; все 3 fail совпали и относятся к существующему gateway-auth
      `HTTP 401` на `/go/proxmox-gui`, `/go/file1c-brief`,
      `/go/file1c-actions`, не к Rust regression;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      aw-contour-smoke` (`2 passed`), `cargo clippy -p aw-contour-smoke
      --all-targets -- -D warnings`, release build OK,
      `bash -n` wrappers OK, artifact check OK, `quality-gate.sh` OK.
47. `[done]` Перенести `scripts/diag_and_manual_restart.sh` на Rust-first
    diagnostic helper:
    - добавлен crate `diag-and-manual-restart`;
    - wrapper ищет `DIAG_AND_MANUAL_RESTART_RUST`,
      `$CARGO_TARGET_DIR/release/diag-and-manual-restart`,
      `adk-rust/target/release/diag-and-manual-restart`,
      `/usr/local/bin/diag-and-manual-restart`, затем использует legacy Bash
      fallback;
    - здоровый контур не перезапускается: helper запускает `aw-health-check`
      и `dlp-health-check`, печатает `Diagnostics: healthy. Restart not
      needed.` и выходит `0`;
    - mutation path требует явного подтверждения или `--yes`; live restart не
      запускался, потому что контур был зеленый;
    - gates: fmt/test/clippy/release OK, `bash -n` OK, Rust-first healthy path
      rc `0`, legacy fallback healthy path rc `0`, artifact check OK,
      `quality-gate.sh` OK, DetMir read-only OK.
48. `[done]` Перенести browser smoke launcher на Rust-first helper:
    - добавлен crate `aw-browser-smoke`;
    - `scripts/aw-webui-browser-smoke.sh` теперь Rust-first wrapper:
      ищет `AW_BROWSER_SMOKE_RUST`,
      `$CARGO_TARGET_DIR/release/aw-browser-smoke`,
      `adk-rust/target/release/aw-browser-smoke`,
      `/usr/local/bin/aw-browser-smoke`, затем использует legacy Node
      fallback;
    - Playwright/Chromium логика остается в
      `scripts/aw-webui-browser-smoke.mjs`; Rust отвечает за устойчивый запуск,
      `NODE_PATH`, passthrough args и сохранение child exit code;
    - fake-node проверки подтвердили passthrough `.mjs` path, user args,
      `NODE_PATH` и exit-code propagation для Rust launcher и legacy fallback;
    - live browser smoke через Rust-first wrapper: `ok=true`, страницы
      `aw_webui_home`, `worktime_today_html`,
      `worktime_management_html` все OK;
    - gates: `cargo fmt --all -- --check`, `cargo test -p aw-browser-smoke`
      (`3 passed`), `cargo clippy -p aw-browser-smoke --all-targets -- -D
      warnings`, release build OK, `bash -n` OK, artifact check OK,
      `quality-gate.sh` OK, DetMir read-only OK with
      `dlp_counts={ok:22,warn:0,fail:0}` and `ok_for_operator=true`.
49. `[done]` Перенести `check-aw-full.sh` на Rust-first read-only helper:
    - добавлен crate `check-aw-full`;
    - root wrapper `check-aw-full.sh` ищет `CHECK_AW_FULL_RUST`,
      `$CARGO_TARGET_DIR/release/check-aw-full`,
      `adk-rust/target/release/check-aw-full`, `/usr/local/bin/check-aw-full`,
      затем выполняет embedded legacy Bash fallback;
    - для прямого legacy compare добавлен `CHECK_AW_FULL_FORCE_LEGACY=1`;
    - Rust helper сохраняет операторский контракт: AW server connectivity,
      CORS probe, 8 bucket freshness rows, RDP WinRM/SSH TCP checks, summary
      `FRESH/STALE/DEAD`, recovery hint when stale/dead collectors exist;
    - live parity: Rust-first и legacy оба показали connectivity OK, CORS OK,
      WinRM OK, SSH OK, `FRESH=8 STALE=0 DEAD=0`; bucket statuses совпали,
      один live event id у `Window watcher` ожидаемо сдвинулся между
      последовательными запусками;
    - gates: `cargo fmt --all -- --check`, `cargo test -p check-aw-full`
      (`4 passed`), `cargo clippy -p check-aw-full --all-targets -- -D
      warnings`, release build OK, `bash -n` OK, artifact check OK,
      `quality-gate.sh` OK, DetMir read-only OK with
      `dlp_counts={ok:22,warn:0,fail:0}` and `ok_for_operator=true`.
50. `[done]` Перенести merge engine на Rust:
    - добавлен crate `merge-aw-server-dbs`;
    - рабочий runtime path: `adk-rust/target/release/merge-aw-server-dbs` и
      `/usr/local/bin/merge-aw-server-dbs`;
    - legacy `scripts/merge_aw_server_dbs.py` удален из repo/runtime path;
    - `ansible/deploy_aw_server.yml` устанавливает
      `/usr/local/bin/merge-aw-server-dbs` и использует его без Python;
    - Rust сохраняет контракт `--base`, `--overlay`, `--output`, SQLite
      backup-copy base DB, bucket matching by id/key/name, duplicate event
      suppression by `(starttime,endtime,data)`, JSON summary;
    - offline parity fixture: Rust replacement дал ожидаемый JSON
      `inserted_buckets=1`, `inserted_events=3` и одинаковые SQLite table rows
      (`buckets=3`, `events=5`) на кейсе duplicate event + name-conflict
      bucket;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      merge-aw-server-dbs` (`2 passed`), `cargo clippy -p
      merge-aw-server-dbs --all-targets -- -D warnings`, release build OK,
      artifact check OK, `quality-gate.sh` OK, DetMir read-only OK after DLP baseline retry
      with `dlp_counts={ok:22,warn:0,fail:0}` and `ok_for_operator=true`.
51. `[done]` Перенести `scripts/prod_backup_restore.sh` в safe-by-default
    Rust planner/checker:
    - добавлен crate `prod-backup-restore`;
    - `scripts/prod_backup_restore.sh` теперь Rust-first wrapper:
      ищет `PROD_BACKUP_RESTORE_RUST`,
      `$CARGO_TARGET_DIR/release/prod-backup-restore`,
      `adk-rust/target/release/prod-backup-restore`,
      `/usr/local/bin/prod-backup-restore`;
    - обычный запуск больше не выполняет restore при наличии Rust artifact:
      он печатает plan-only flow и помечает destructive steps;
    - если Rust artifact отсутствует, wrapper отказывается запускать
      destructive legacy случайно и просит собрать Rust planner;
    - старый Python destructive flow удален; `--apply-legacy` больше не
      поддерживается;
    - Rust `--apply` намеренно запрещен на этом этапе;
    - `--check-inputs --json` валидирует env/files/commands без вывода
      секретов, проверяет `AW_SSH_PASSWORD`, `AW_WINRM_PASSWORD`, `sshpass`,
      `ansible-playbook`, inventory и `merge-aw-server-dbs`;
    - план сохраняет порядок restore flow: scp Rust merge binary, remote backup dir,
      DB existence checks, DB backups, stop `activitywatch-server`, merge DB,
      install merged DB, 3 ansible playbooks, final AW validation;
    - safe smoke: plan text OK, JSON valid (`steps=12`, `missing_count=0`),
      destructive steps present and marked, password names/values не попадают в
      planned commands, Rust `--apply` exits rc `1`;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      prod-backup-restore` (`3 passed`), `cargo clippy -p
      prod-backup-restore --all-targets -- -D warnings`, release build OK,
      `bash -n scripts/prod_backup_restore.sh`, artifact check OK,
      `quality-gate.sh` OK, DetMir read-only OK with
      `dlp_counts={ok:22,warn:0,fail:0}` and `ok_for_operator=true`;
      transient `service_warnings=1` disappeared on immediate
      `detmir-check --json` repeat (`service_warnings=0`).
52. `[done]` Перенести `scripts/prod_rollout.sh` в Rust-first safe
    planner/orchestrator:
    - добавлен crate `prod-rollout`;
    - `scripts/prod_rollout.sh` теперь Rust-first wrapper:
      ищет `PROD_ROLLOUT_RUST`, `$CARGO_TARGET_DIR/release/prod-rollout`,
      `adk-rust/target/release/prod-rollout`, `/usr/local/bin/prod-rollout`;
    - обычный запуск больше не стартует production deploy автоматически:
      он показывает plan-only rollout flow;
    - реальный rollout через Rust доступен только явно:
      `scripts/prod_rollout.sh --apply`;
    - старый Bash rollout сохранен только явно:
      `scripts/prod_rollout.sh --apply-legacy`;
    - `--check-inputs --json` валидирует `AW_SSH_PASSWORD`,
      `AW_WINRM_PASSWORD`, `git`, `ansible`, `ansible-playbook`, inventory,
      `quality-gate.sh`, `deploy_aw_server.yml`, `deploy_aw_windows.yml` и
      `post_validate_aw_windows.yml` без вывода секретов;
    - план сохраняет legacy порядок: `quality-gate`, Ansible ping AW server,
      Ansible win_ping AW Windows, `deploy_aw_server --check --diff`, real
      `deploy_aw_server`, `deploy_aw_windows --check --diff`, real
      `deploy_aw_windows`, `post_validate_aw_windows`;
    - mutation в плане помечена только для `deploy-aw-server` и
      `deploy-aw-windows`;
    - apply-runner пишет stdout/stderr каждого шага в
      `.rollout-logs/<timestamp>/`, общий ход - в `rollout.log`;
    - safe smoke: `scripts/prod_rollout.sh --check-inputs --json` вернул
      `mode=plan-only`, `missing_count=0`, `steps=8`, mutations
      `deploy-aw-server` и `deploy-aw-windows`;
    - gates: `cargo fmt --all -- --check`, `cargo test -p prod-rollout`
      (`3 passed`), `cargo clippy -p prod-rollout --all-targets -- -D
      warnings`, release build OK, `bash -n scripts/prod_rollout.sh`,
      artifact check OK.
53. `[done]` Перенести `aw-server/ensure-reliability.sh` в Rust-first
    dry-run/apply helper:
    - добавлен crate `aw-ensure-reliability`;
    - `aw-server/ensure-reliability.sh` теперь Rust-first wrapper:
      ищет `AW_ENSURE_RELIABILITY_RUST`,
      `$CARGO_TARGET_DIR/release/aw-ensure-reliability`,
      `adk-rust/target/release/aw-ensure-reliability`,
      `/usr/local/bin/aw-ensure-reliability`;
    - обычный запуск больше не делает `chown`, `systemctl stop/start`,
      logrotate write или health timer write; он показывает dry-run plan;
    - реальный Rust repair требует explicit `--apply`;
    - старый Bash repair доступен только explicit `--apply-legacy`;
    - Rust helper сохраняет legacy action set: env check,
      ownership/mode repair for `/var/lib/activitywatch`,
      `/var/log/activitywatch`, `/opt/activitywatch`, logrotate install,
      health-check script/timer/service install, ordered AW service restart and
      enable;
    - `deploy_aw_server.yml` optional устанавливает
      `/usr/local/bin/aw-ensure-reliability` до Influx token checks, если
      release artifact доступен;
    - production binary доставлен на AW server, но `--apply` не запускался;
    - production dry-run: `apply=false`, `ok=true`, `missing=0`,
      `executed=0`, `steps=25`;
    - final production gates: AW failed units `0`, `detmir-status` OK with
      `service_warnings=0`, `dlp_counts={ok:22,warn:0,fail:0}`,
      `ok_for_operator=true`;
    - gates: `cargo fmt --all -- --check`, `cargo test -p
      aw-ensure-reliability` (`2 passed`), `cargo clippy -p
      aw-ensure-reliability --all-targets -- -D warnings`, release build OK,
      `bash -n aw-server/ensure-reliability.sh`, artifact check OK,
      `ansible-playbook deploy_aw_server.yml --syntax-check` OK.
54. `[done]` Перевести Linux install scripts в Rust-first safe planner слой:
    - добавлен crate `aw-linux-install`;
    - wrappers теперь Rust-first и dry-run по умолчанию:
      `scripts/install_aw_linux_client.sh`,
      `scripts/install_aw_linux_remote_worker.sh`,
      `scripts/install_aw_linux_web_category_logger.sh`,
      `scripts/install_aw_console_ssh_logger.sh`,
      `scripts/install_aw_pve_webadmin_logger.sh`;
    - real install требует explicit `--apply`;
    - old shell install требует explicit `--apply-legacy`;
    - `aw-linux-install --apply` запускает соответствующий legacy script с
      `--apply-legacy`, сохраняя embedded Python collectors и shell install
      contract;
    - `remote_worker` legacy path исправлен: вложенные client/console/web
      installers вызываются с `--apply-legacy`, чтобы real legacy install не
      превратился в dry-run после оборачивания;
    - PVE webadmin logger не запускался и Proxmox platform не менялся; это
      только safe planner wrapper;
    - gates: `cargo fmt --all -- --check`, `cargo test -p aw-linux-install`
      (`2 passed`), `cargo clippy -p aw-linux-install --all-targets -- -D
      warnings`, release build OK, `sh -n` для всех пяти wrappers OK,
      dry-run JSON для всех пяти wrappers OK, artifact check OK.
55. `[done]` Закрепить проверку актуализации и правильности Grafana данных:
    - добавлен crate `detmir-grafana-check`;
    - check читает Grafana dashboard API, валидирует форму dashboard,
      отсутствие старых `aw_window_event`/`aw_afk_event`, наличие текущих
      worktime measurements и выполняет все panel queries через Grafana
      datasource API;
    - freshness panel считается обязательным: значение свежести должно быть не
      старше `DETMIR_GRAFANA_MAX_FRESHNESS_MINUTES` (production default 360);
    - результат пишется в Grafana CT 201:
      `/var/lib/detmir-grafana-check/latest.json` и `latest.txt`;
    - установлен `detmir-grafana-check.service` и timer каждые 15 минут;
    - `detmir-check` теперь читает последний Grafana-check artifact через
      `sudo -n /usr/sbin/pct exec 201` и считает `grafana-data` required
      service check, так что общий `detmir-status` краснеет при сломанной или
      устаревшей Grafana;
    - добавлен playbook `ansible/deploy_grafana_check.yml`;
    - `scripts/check_detmir_rust_release_artifacts.sh` теперь требует
      `detmir-grafana-check`;
    - production verification: `detmir-grafana-check` OK
      (`ok=13,warn=0,fail=0`), 7/7 panels have rows, freshness около 120 min,
      timer active, Grafana CT failed units 0, Proxmox failed units 0,
      `detmir-check` `service_failures=0`, `detmir-auto --no-heal` rc 0,
      `detmir-status` OK / `ok_for_operator=true`.
56. `[done]` Реализовать read-only MVP `detmir-portal`:
    - добавлен crate `detmir-portal`;
    - portal работает как Rust web service на Proxmox:
      `/usr/local/bin/detmir-portal`, `detmir-portal.service`,
      bind `127.0.0.1:8720`;
    - внешний route добавлен в существующий nginx gateway:
      `https://<PUBLIC_GATEWAY_FQDN>/portal/`;
    - UI содержит вкладки `Оператор`, `Руководитель`, `Владелец`,
      `Инциденты ИБ`;
    - API реализованы: `/api/health`, `/api/summary`, `/api/operator`,
      `/api/manager`, `/api/owner`, `/api/incidents`, `/api/links`;
    - источники read-only: `detmir-status --json`, `detmir-check --json`,
      `systemctl --failed`, AW Worktime API, 1C analytics health;
    - portal не делает write/heal/restart actions и не трогает pfSense,
      Telegram runtime, NAT/DNS/VPN;
    - добавлен deployment playbook `ansible/deploy_detmir_portal.yml`;
    - gateway playbook теперь проверяет `/portal/api/health` с auth;
    - `scripts/check_detmir_rust_release_artifacts.sh` теперь требует
      `detmir-portal`;
    - gates: `cargo fmt --all -- --check`, `cargo test -p detmir-portal`
      (`3 passed`), `cargo clippy -p detmir-portal --all-targets -- -D
      warnings`, release build OK, `ansible-playbook
      deploy_detmir_portal.yml --syntax-check`, `ansible-playbook
      deploy_proxmox_web_gateway.yml --syntax-check`, artifact check OK;
    - production verification: `detmir-portal` active, nginx active,
      `/portal/api/health` OK through gateway auth, all sources true,
      external `/portal/` returns protected `401` without auth, gateway
      `/healthz` returns `ok`, `detmir-status` OK / `ok_for_operator=true`,
      Proxmox failed units 0;
    - browser verification through production tunnel: desktop and mobile
      nonblank, tabs work, API requests 200, console errors 0. Screenshots:
      `.playwright-cli/page-2026-06-02T17-31-49-049Z.png`,
      `.playwright-cli/page-2026-06-02T17-33-50-318Z.png`,
      `.playwright-cli/page-2026-06-02T17-34-04-725Z.png`.
    - portal link repair after Grafana/gateway smoke: quick links now open in a
      separate tab, `worktime_report` points to explicit HTML
      `/reports/worktime/management?format=html&host=HOST-EXAMPLE`,
      `1С действия` is shown in the portal, and gateway `/r/aw-worktime` is
      pinned to the same HTML report. Production smoke: AW UI, Worktime, 1C
      brief, and 1C actions returned `200 text/html`; Grafana links correctly
      redirect to `/login` when no Grafana session exists.
    - incident operator workflow added to portal: incidents now have stable
      IDs, operator state is persisted in
      `/var/lib/detmir-portal/incidents-state.json`, audit events append to
      `/var/lib/detmir-portal/audit.jsonl`, nginx passes `$remote_user` as
      `X-Remote-User`, and the UI exposes `В работу` / `Назначить` actions.
      This layer only records acknowledgement/assignment metadata; it does not
      heal, restart, mutate AW/DLP/1C, or touch pfSense/Telegram. Gates:
      `cargo fmt --all -- --check`, `cargo test -p detmir-portal` (`5 passed`),
      `cargo clippy -p detmir-portal --all-targets -- -D warnings`, release
      build OK. Production smoke: `POST /portal/api/incidents/action` returned
      `200 application/json`, actor resolved to `detmir`, state/audit files
      were written, smoke state was removed after validation, portal health
      stayed `200`, `detmir-status` stayed `OK / ok_for_operator=true`, and
      failed units stayed `0`. Local Playwright smoke against a temporary
      portal instance verified `Инциденты ИБ`, `В работу`, persisted UI state,
      disabled ack button, and zero JS errors.
    - `Инциденты ИБ` tightened to a DLP-focused card: the tab now filters the
      card to DLP/incident/case items, adds a synthetic DLP-count incident when
      `dlp_counts.warn/fail` is non-zero, and shows only DLP/Grafana dashboard
      links (`detmir-dlp-security`, `detmir-dlp-management`,
      `awatch-dlp-overview`, Grafana catalog). Worktime, 1C, AW UI, and other
      non-incident operational links are intentionally not shown in this card.
      Local Playwright smoke verified DLP incident rendering, dashboard links,
      no Worktime/1C text in the card, and zero JS errors.
    - DLP evidence viewing layer added to the portal:
      `detmir-portal` now exposes `/api/dlp/evidence` and safe screenshot
      routes by opaque evidence id. The service reads DLP warehouse SQLite
      read-only, extracts `screenshotSha256`/dimensions from raw event JSON,
      and serves image files only from an allowlisted evidence root after
      canonical path, extension, size, and SHA-256 validation. Evidence views
      and downloads append to `evidence-audit.jsonl`. Production uses an
      AW-server evidence-only service, `/usr/local/bin/detmir-portal-evidence`
      with `detmir-portal-evidence.service`, because the DLP warehouse lives on
      the AW server. Proxmox nginx gateway routes
      `/portal/api/dlp/evidence*` to `<AW_SERVER_HOST>:8721`. Current production
      verification: AW evidence API `ok=true`, gateway evidence route
      `ok=true`, `db_available=true`, 11 DLP evidence rows returned,
      `screenshot_available=0` because current stored rows do not yet contain
      screenshot metadata, both portal/evidence services active, failed units
      0, `detmir-status` `OK / ok_for_operator=true`. Local HTTP smoke
      verified byte-identical screenshot serving and audit logging; local
      Playwright smoke verified the `Доказательства` block, `СКРИН`, `Открыть`,
      `Скачать`, and zero JS errors.
    - DLP evidence screenshot delivery is now automated in production:
      the AW evidence API accepts authenticated uploads at
      `POST /api/dlp/evidence/upload` in evidence-only mode. Uploads require a
      server-generated Bearer token stored outside git, validate base64 body,
      PNG/JPEG magic, max size, and exact SHA-256 before atomic write to
      `/var/lib/activitywatch/dlp-evidence/screenshots/<sha256>.(png|jpg)`.
      Windows RDP host runs `sync-dlp-evidence-artifacts.ps1` through scheduled
      task `ActivityWatch DLP Evidence Sync` every 5 minutes as SYSTEM. The
      sync scans `C:\ProgramData\AWatch-rus\incident-artifacts` plus configured
      artifact roots, uploads new PNG screenshots, and keeps local upload state
      in `C:\ProgramData\AWatch-rus\dlp-evidence-sync-state.json`. Production
      controlled test: a visible PNG was created on Windows, sync uploaded it
      (`uploaded=1`, `failed=0`), a temporary warehouse row made it visible in
      the portal with `screenshot_available=true`, gateway preview returned
      `image/png` with matching SHA, external Playwright verified the portal UI
      and preview, audit recorded upload/view, unauthenticated upload returned
      403, and the synthetic row/files/state were removed. Final state:
      evidence count returned to 11, synthetic hit false, AW/Proxmox failed
      units 0, `detmir-status` OK, sync task Ready with `lastTaskResult=0`.
    - during this deploy, `detmir-grafana-check` was corrected so empty
      detail-only panels for employees/applications are WARN, not FAIL. The
      mandatory freshness/summary panels still fail the check when stale or
      empty. This removed a false red state shortly after midnight when
      `today` detail rows were legitimately empty. Production verification:
      Grafana check `ok=true` with `fail=0`, `detmir-auto --no-heal` rc `0`,
      portal health `true`, `detmir-status` `OK / ok_for_operator=true`, and
      failed units `0`.
    - `docs/THREAT_MODEL_RU.md` added as the current working threat
      model for AWatch-rus. It records the product as an operational
      control and technical audit platform, not a certified DLP/SIEM/EDR/XDR
      or FSTEC SZI. It also records Igor as the declared product owner, lists
      assets, trust zones, attacker/operator-failure classes, implemented
      evidence controls, residual risks, and the hardening roadmap.
    - `docs/RUSSIAN_SOFTWARE_REGISTRY_POSITIONING_RU.md` added as the
      registry/product positioning note. Current decision: lead with AWatch-rus
      as an operational control and IT infrastructure management platform, use
      `09.10` as the primary Russian software registry class target, keep
      DLP/security/evidence/Hayabusa as applied modules, and prepare website,
      operator/admin docs, ownership package, screenshots, and dependency
      inventory before any registry filing.
    - registry proof package skeleton added:
      `docs/ADMIN_GUIDE_RU.md`, `docs/OPERATOR_GUIDE_RU.md`,
      `docs/INSTALL_RU.md`, `docs/ARCHITECTURE_RU.md`,
      `docs/OWNERSHIP_RU.md`, `docs/THIRD_PARTY_LICENSES_RU.md`, and
      `docs/REGISTRY_CHECKLIST_RU.md`. Current naming decision for public
      materials: `AWatch-rus` is the product and repository name. Legacy
      `detmir-*` service, crate and environment identifiers remain technical
      runtime identifiers until a separate compatibility-safe migration is
      approved.

Отложить:

- post-MVP развитие `detmir-portal`: role-aware views, safe check-now action,
  daily owner report, historical trends, AI summary with strict source
  citations, action buttons with allowlist and audit log. Детальный план:
  `docs/PORTAL_GUI_PLAN_RU.md`;
- перенос Telegram bot runtime снят с плана: Python остается постоянным
  runtime, Rust используется только для backend helpers;
- перенос оставшихся install/runtime scripts на Rust;
- переписывание PowerShell Windows collector path;
- любые network/firewall/VPN изменения;
- pfSense полностью frozen/no-touch: текущего потенциала достаточно, NAT/gateway
  tooling не переносить и не дергать без отдельной явной команды.

## 12. Stop conditions

Миграцию остановить и откатить конкретный модуль, если:

- новый binary дает иной `severity` без объяснимой причины;
- меняет exit code class на зеленом или красном контуре;
- пишет неполный/corrupt state;
- вызывает restart на зеленом контуре;
- ломает Telegram `/status`;
- ломает Grafana/Influx/Prometheus metric names;
- требует laptop-only path или интерактивный shell;
- выводит секреты в stdout/journald/report.

## 13. Рабочий принцип

Правильный перенос на Rust - это не переписывание строк один-в-один.

Для каждого legacy script надо сделать:

1. понять контракт;
2. зафиксировать fixture;
3. написать typed model;
4. реализовать read-only parity;
5. включить shadow-mode;
6. заменить production command;
7. только потом добавлять action/mutation;
8. удалить legacy только после периода стабильной эксплуатации.
