Compare commits

...
67 Commits
Author SHA1 Message Date
igor04091968 d75b3eb05e Feat: implement automated DLP incident aggregation on server (timer + service) 2026-05-03 01:24:54 +03:00
Devin AI 4359f6d5eb Fix Windows file telemetry playbook wiring 2026-05-02 21:44:18 +00:00
Devin AI 7ea4ebd463 Merge PR #11 DLP incident aggregation prototype 2026-05-02 21:33:42 +00:00
Devin AI 5a4064dc0f Add DLP incident aggregation prototype 2026-05-02 20:55:44 +00:00
igor04091968 c97ffe2cbd Fix: ensure file collector robustness (HttpClient, TLS 1.2, English logs) 2026-05-02 23:42:14 +03:00
Devin AI f45ef0038d Use string JSON body for file collector posts 2026-05-02 20:34:07 +00:00
Devin AI 8088b19dc7 Create file operation bucket before path checks 2026-05-02 20:28:03 +00:00
Devin AI 046aa3ed1d Create file operations bucket on startup 2026-05-02 20:17:43 +00:00
igor04091968 b6f019982d gemeni-3-light 2026-05-02 23:09:54 +03:00
igor04091968andDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> fae2e2ca14 Feat & Fix: implement File Telemetry, restore DB history, and stabilize production
- Added File Operations Collector (Plan A) for Windows endpoints
- Restored historical server DB via merging and moved to durable /var/lib/activitywatch path
- Forced XDG_DATA_HOME and XDG_CONFIG_HOME for aw-server-rust in environment and systemd
- Updated Ansible playbooks to handle new file collector and durable server paths
- Added DB merge and backup-restore automation scripts
- Fixed CORS and RU WebUI persistence in production deployment

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-05-02 20:59:08 +03:00
igor04091968 f436950bda Update after codex restore 2026-05-02 19:36:25 +03:00
igor04091968 d7fedde69d After deploy from hand restore 2026-05-02 17:54:36 +03:00
igor04091968 7f58a49c0a Разворачивание деплоя 2026-05-02 17:28:49 +03:00
Devin AI 2e49310023 Добавить безопасную миграцию prod в AWatch-rus 2026-05-02 11:58:28 +00:00
Devin AI 2ec90b05ae Унифицировать Windows пути AWatch-rus 2026-05-02 09:44:27 +00:00
Devin AI 21f0184115 Согласовать Windows пути с InnoSetup 2026-05-02 09:20:28 +00:00
Devin AI 342ab77f44 Исправить recovery config и разбор RDP sessions 2026-05-02 09:08:14 +00:00
Devin AI 1e92b8b679 Русифицировать и доработать Ansible развёртывание 2026-05-02 09:07:08 +00:00
Devin AI 8c9dfffc7d Ограничить PSScriptAnalyzer ошибками 2026-05-02 08:35:23 +00:00
Devin AI 087883b663 Уточнить настройки PSScriptAnalyzer 2026-05-02 08:34:21 +00:00
Devin AI 4bbf0b8b73 Исправить CI-проверки shellcheck и PSScriptAnalyzer 2026-05-02 08:32:53 +00:00
Devin AI da99d1ac98 Русифицировать DLP PowerShell и исправить имя документа печати 2026-05-02 08:30:37 +00:00
igor04091968 f7cf5556a0 Ofline install InnoSetup 2026-05-02 10:01:39 +03:00
igor04091968 65da55be7a Patch files Innosetup 2026-05-02 04:12:14 +03:00
igor04091968 8278d51840 feat(worktime): add linux remote worker and pve headless tracking 2026-04-30 15:48:09 +03:00
igor04091968 fa4bf96ebf feat(ansible): add full-stack installer playbook 2026-04-28 07:10:44 +03:00
igor04091968 b28cfabd57 fix(windows-phase2): codify SHARKON2025 recovery and harden rollout checks 2026-04-28 07:09:29 +03:00
igor04091968 4936d6bca3 fix(webui): translate activity heading separator 2026-04-27 23:57:20 +03:00
igor04091968 c54b237f80 fix(webui): repair category helper host selection 2026-04-27 23:36:58 +03:00
igor04091968 48223fbeeb chore(install): sync playbooks and installers with pve audit 2026-04-27 23:18:32 +03:00
igor04091968 7f131a6310 fix(webui): add stable pve audit activity view 2026-04-27 22:56:05 +03:00
igor04091968 77591c10ce fix(webui): harden pve-detmir activity route and suppress DLP nav on pve hosts 2026-04-27 22:17:51 +03:00
igor04091968 947717251f fix(webui): route pve-detmir activity to watcher-safe view and add audit links 2026-04-27 21:58:37 +03:00
igor04091968 0da0d880c0 feat(pve): add web-admin audit collector to forward Proxmox actions to AW 2026-04-27 21:52:07 +03:00
igor04091968 5adb22754f fix(linux-logger): parse who -u pts fields and avoid duplicate start with systemd 2026-04-27 21:40:17 +03:00
igor04091968 76004e30fe feat(aw-ui): add dedicated pve-detmir host entry in Activity menu 2026-04-27 21:34:56 +03:00
igor04091968 b48b18f225 fix(ansible): default windows state root to C:\ProgramData\ActivityWatch 2026-04-27 21:31:04 +03:00
igor04091968 5a41c4066b feat(windows): add install kit and stabilize dlp print document matching 2026-04-27 21:22:40 +03:00
igor04091968 fff4c10e64 feat(logging): add console and ssh activity logger 2026-04-27 18:17:52 +03:00
igor04091968 5105155323 feat(linux): add remote client rollout for admin host 2026-04-27 17:29:17 +03:00
igor04091968 ced44c347c docs(runtime): capture confirmed admin-host topology 2026-04-27 17:04:31 +03:00
igor04091968 c70fa019ad fix(grafana): finalize ru naming for overview dashboard 2026-04-27 14:09:46 +03:00
igor04091968 e22bd697f7 chore(repo): ignore python cache artifacts 2026-04-27 04:41:41 +03:00
igor04091968 e80ae5fbc3 feat(pfsense): add AW poller and host group sections 2026-04-27 04:41:32 +03:00
igor04091968 70ffd1f3e8 fix(windows): hide launch tasks behind wscript wrappers 2026-04-27 04:16:25 +03:00
igor04091968 1e9628b01b feat(ansible): add incident screenshot controls for windows phase2 2026-04-27 04:04:37 +03:00
igor04091968 f26012ae52 docs(windows): add DLP screenshot and self-test runbook 2026-04-27 03:53:02 +03:00
igor04091968 63c3d96a19 feat(dlp): capture screenshot on incident 2026-04-27 03:29:19 +03:00
igor04091968 5584144978 fix(windows): suppress duplicate logon markers per session 2026-04-27 03:00:53 +03:00
igor04091968 0ae2ac7ec1 fix(windows): tolerate existing session marker bucket 2026-04-27 02:56:09 +03:00
igor04091968 53acdcb6f4 feat(windows): add logon markers and disable local agent logs 2026-04-27 02:42:04 +03:00
igor04091968 bdf44b868e fix(windows): recover all phase2 user tasks after reboot 2026-04-27 02:32:11 +03:00
igor04091968 aa00d5633f fix(web-collector): avoid PowerShell Host collision 2026-04-27 02:21:47 +03:00
igor04091968 6694e0682d feat(worktime): add from/to worktime window 2026-04-27 01:49:27 +03:00
igor04091968 a61b4cee2b fix(webui): prevent settings crash on missing class.data 2026-04-27 01:47:16 +03:00
igor04091968 428a8623a9 feat(worktime): apply baseline classes via ansible 2026-04-26 20:05:44 +03:00
igor04091968 48bb4e4c4c feat(dlp): surface operator incidents in alerts 2026-04-26 15:42:07 +03:00
igor04091968 bb200cab96 feat(dlp): expose menu and document web review flow 2026-04-26 15:39:40 +03:00
igor04091968 1b968f9382 fix(webui): stabilize and localize timespiral 2026-04-26 10:12:39 +03:00
igor04091968 97fe09a150 fix(ansible): align ensemble rollout with current windows deploy 2026-04-26 09:13:41 +03:00
igor04091968 7ef8166504 feat(aw): expand ru webui patch and monitoring assets 2026-04-26 08:54:41 +03:00
igor04091968 f54284a0f9 feat(docs-playbooks): update phase2 runbooks and add windows winrm rollout playbook 2026-04-25 18:00:13 +03:00
igor04091968 bf27369b2a feat(dlp-phase2): add endpoint signal collector for clipboard usb print and policy wiring 2026-04-25 16:26:07 +03:00
igor04091968 c3a49e658c feat(dlp-phase1): add policy-based incident pipeline and capability gap analysis 2026-04-25 16:11:05 +03:00
igor04091968 016a323afc feat(ansible): add hardened matrix rollout for Proxmox CTs with RU patch 2026-04-25 15:57:30 +03:00
igor04091968 9e53272ce9 feat(ansible): add full Proxmox zero-to-service provisioning playbook 2026-04-25 15:43:31 +03:00
igor04091968 92eaa0635c chore: add local quality-gate script for offline validation 2026-04-25 15:30:34 +03:00
145 changed files with 19190 additions and 311 deletions
+4 -2
View File
@@ -18,7 +18,7 @@ jobs:
- name: Run shellcheck
run: |
find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck
find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck -e SC1007,SC1090,SC2016
powershell-analyzer:
runs-on: ubuntu-latest
@@ -40,7 +40,9 @@ jobs:
"windows/*.psm1",
"windows/*.psd1"
)
$issues = Invoke-ScriptAnalyzer -Path $targets -Recurse -Severity Error,Warning
$issues = $targets | ForEach-Object {
Invoke-ScriptAnalyzer -Path $_ -Recurse -Severity Error
}
if ($issues) {
$issues | Format-Table -AutoSize
throw "PSScriptAnalyzer detected issues."
+4
View File
@@ -1,12 +1,16 @@
# Local secrets
secrets/deploy.secrets.env
secrets/runtime.env
# Runtime / reports
*.log
*.tmp
*.bak
windows/*.report.json
.rollout-logs/
# IDE
.idea/
.vscode/
__pycache__/
+41 -1
View File
@@ -9,10 +9,22 @@
- `docs/runbook.md` — быстрый runbook для оператора.
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки.
- `docs/linux-client.md` — user-space rollout Linux-клиента ActivityWatch на удалённый `AW server`.
- `docs/linux-remote-worker.md` — полный Linux remote-worker stack: GUI, SSH/console и browser admin UI вроде Proxmox `:8006`.
- `docs/console-ssh-logger.md` — логирование только консольных команд и SSH-сессий в AW.
- `docs/dlp-gap-analysis.md` — разрыв до enterprise DLP и roadmap.
- `docs/dlp-aggregator.md` — прототип централизованной агрегации DLP/file-operation событий.
- `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox.
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI.
- `ansible/` — Ansible-ensemble для автоматизированного сервера (Debian/CT).
- `windows/` — PowerShell toolkit: single-user, domain-users, ensemble orchestration, hardening/recovery, validation.
- `pfsense/` — внешний poller для pfSense API и systemd unit под Debian/Ubuntu utility VM.
- `windows/` — PowerShell toolkit: single-user, domain-users, ensemble orchestration, hardening/recovery, validation, Windows/RDP DLP telemetry (`aw-dlp-incidents_*`, `aw-dlp-endpoint-signals_*`) и session-level presence для удалённых Windows/RDP пользователей (`aw-worktime-sessions_*`).
- `scripts/quality-gate.sh` — локальный preflight-пайплайн проверок.
- `scripts/aggregate_dlp_events.py` — сбор `aw-file-operations_*` и `aw-dlp-incidents_*` в SQLite/PostgreSQL.
- `scripts/install_aw_linux_client.sh` — установка Linux bundle + autostart для remote AW server.
- `scripts/install_aw_console_ssh_logger.sh` — user-space установка console/ssh logger.
- `scripts/install_aw_linux_web_category_logger.sh` — user-space классификация browser admin UI по title/class.
- `scripts/install_aw_linux_remote_worker.sh` — полный Linux remote-worker installer.
## Базовый сценарий
@@ -26,6 +38,34 @@
8. Развернуть Windows-клиентов через `windows/deploy-ensemble.ps1`.
9. Проверить итог через `windows/validate-deployment.ps1`.
Для полного Ansible-сценария “с нуля” в Proxmox используйте:
- `ansible/provision_proxmox_ct_and_deploy_aw.yml`
- `ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` (массово по матрице CT)
Для централизованного деплоя Windows/RDP-клиентов через WinRM:
- `ansible/deploy_aw_windows.yml`
Для внешнего pfSense poller'а:
- `ansible/deploy_aw_pfsense_poller.yml`
Для Linux desktop/admin host, который должен слать watcher'ы на удалённый AW server:
- `docs/linux-client.md`
- `scripts/install_aw_linux_client.sh`
Для полного Linux remote-worker сценария:
- `docs/linux-remote-worker.md`
- `scripts/install_aw_linux_remote_worker.sh`
Для режима “только консоль/ssh” без GUI watcher'ов:
- `docs/console-ssh-logger.md`
- `scripts/install_aw_console_ssh_logger.sh`
Скрипты `proxmox/create-ct.sh` и `proxmox/push-aw-artifacts.sh` по умолчанию читают:
- `secrets/deploy.secrets.env`
+17
View File
@@ -0,0 +1,17 @@
#!/bin/sh
set -eu
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
cd "$REPO_DIR"
echo "==> Fetch origin"
git fetch origin
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "==> Pull origin/$CURRENT_BRANCH"
git pull --ff-only origin "$CURRENT_BRANCH"
echo "==> Done"
git status --short --branch
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
set -eu
REPO_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
cd "$REPO_DIR"
BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "${1:-}" = "" ]; then
echo "Usage: $0 \"commit message\"" >&2
exit 1
fi
COMMIT_MESSAGE=$1
echo "==> Git status"
git status --short --branch
echo "==> Stage changes"
git add -A
if git diff --cached --quiet; then
echo "No staged changes to commit."
exit 0
fi
echo "==> Commit"
git commit -m "$COMMIT_MESSAGE"
echo "==> Push origin/$BRANCH"
git push origin "$BRANCH"
echo "==> Done"
+148 -7
View File
@@ -1,29 +1,170 @@
# Ansible ensemble for AWatch-rus
Эта директория содержит минимальный Ansible-ensemble для повторяемого развёртывания ActivityWatch Server в Debian/LXC.
Эта директория содержит Ansible-ensemble для полного развёртывания AWatch-rus:
- деплой на уже существующий Debian host/CT;
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch;
- централизованное развёртывание Windows/RDP collector'ов по WinRM;
- развёртывание внешнего pfSense poller'а на Debian/Ubuntu utility VM.
## Файлы
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_server.yml` — основной playbook.
- `/home/igor/tmp/AWatch-rus/ansible/inventory.example.ini` — шаблон inventory.
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml` — шаблон переменных.
- `ansible/deploy_aw_server.yml` — основной playbook для уже существующего Debian/CT host.
- `ansible/provision_proxmox_ct_and_deploy_aw.yml` — полный playbook для Proxmox.
- `ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый полный playbook (несколько CT).
- `ansible/deploy_aw_windows.yml` — WinRM playbook для развёртывания Windows/RDP collector'ов.
- `ansible/deploy_aw_pfsense_poller.yml` — развёртывание pfSense poller'а.
- `ansible/install_full_stack.yml` — полный установочный playbook (оркестратор всех этапов).
- `ansible/inventory.example.ini` — шаблон inventory.
- `ansible/group_vars/*.example.yml` — шаблоны переменных.
## Быстрый запуск
1. Скопируйте шаблоны:
- `cp /home/igor/tmp/AWatch-rus/ansible/inventory.example.ini /home/igor/tmp/AWatch-rus/ansible/inventory.ini`
- `cp /home/igor/tmp/AWatch-rus/ansible/group_vars/all.example.yml /home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
2. Заполните значения в `inventory.ini` и `group_vars/all.yml`.
3. Запустите:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cd ansible
ansible-playbook -i inventory.ini deploy_aw_server.yml
```
## Секреты (пароли) безопасно
Рекомендуемый способ не хранить пароли в репозитории — перед запуском экспортировать их в переменные окружения:
- Linux `aw_server` (SSH пароль root): `AW_SSH_PASSWORD`
- Windows `aw_windows` (WinRM пароль): `AW_WINRM_PASSWORD`
В `group_vars/aw_server.yml` и `group_vars/windows.yml` они читаются через `lookup('env', ...)`.
## Полный установочный playbook (всё за один запуск)
Если нужно прогнать полный цикл одной командой:
```bash
cd ansible
ansible-playbook -i inventory.ini install_full_stack.yml
```
Что делает:
- `provision_proxmox_ct_and_deploy_aw.yml` (если есть хосты в группе `[proxmox]`);
- `deploy_aw_server.yml` (группа `[aw_server]`);
- `deploy_aw_windows.yml` (группа `[aw_windows]`);
- `deploy_aw_pfsense_poller.yml` (группа `[aw_pfsense_pollers]`).
Пустые группы в `inventory.ini` безопасны: соответствующий play будет пропущен.
## Полный запуск с нуля в Proxmox
1. Подготовьте inventory и vars:
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
- `cp ansible/group_vars/proxmox.example.yml ansible/group_vars/proxmox.yml`
2. Заполните `group_vars/proxmox.yml` и `group_vars/all.yml`.
3. Запустите playbook:
```bash
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml
```
## Массовый запуск (матрица CT)
1. Подготовьте матрицу:
- `cp ansible/group_vars/proxmox-matrix.example.yml ansible/group_vars/proxmox-matrix.yml`
2. Заполните `proxmox-matrix.yml`.
3. Запустите:
```bash
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
```
## Windows/RDP rollout (WinRM)
1. Подготовьте inventory и vars:
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/windows.example.yml ansible/group_vars/windows.yml`
2. Заполните `inventory.ini` (секция `[aw_windows]`) и `group_vars/windows.yml`.
- Для русской локализации Windows часто нужен `ansible_user=Администратор` (а не `Administrator`).
- Если WinRM закрыт, playbook не сможет стартовать и нужно сначала открыть `5985/5986` и `wsman`.
3. Запустите:
```bash
cd ansible
ansible-playbook -i inventory.ini deploy_aw_windows.yml
```
Playbook:
- выгружает полный `windows/*` toolkit на целевой хост в InnoSetup-compatible каталог `C:\Program Files\AWatch-rus\windows`, включая DLP и `worktime-session-collector.ps1`;
- если найден legacy config `C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`, выполняет безопасную миграцию через `migrate-awatch-rus-paths.ps1`: backup, остановка задач, перенос данных, переписывание путей, пересоздание scheduled tasks и validation;
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с policy/rules из AWatch-rus toolkit;
- после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи;
- выполняет API smoke-check bucket `aw-watcher-afk_<COMPUTERNAME>` и ожидает свежие `not-afk` события;
- запускает `validate-deployment.ps1`;
- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию).
Дополнительные флаги:
- `aw_windows_afk_enabled: false` — не запускать `aw-watcher-afk`;
- `aw_windows_window_enabled: false` — не запускать `aw-watcher-window`;
- `aw_windows_incident_capture_enabled: false` — отключить блок incidentCapture;
- `aw_windows_incident_screenshot_enabled: false` — не делать скриншот при DLP-инциденте;
- `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов;
- `aw_windows_deploy_root: 'C:\Program Files\AWatch-rus'` — каталог toolkit, совпадает с InnoSetup `{app}`;
- `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`;
- `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`;
- `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`;
- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`;
- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос;
- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте;
- `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch;
- `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_<COMPUTERNAME>`;
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта.
## Развёртывание pfSense poller
1. Подготовьте vars:
- `cp ansible/group_vars/pfsense-poller.example.yml ansible/group_vars/pfsense-poller.yml`
2. Добавьте inventory group `[aw_pfsense_pollers]`.
3. Запустите:
```bash
cd ansible
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
```
Playbook:
- ставит `python3`;
- копирует `pfsense-aw-poller.py`;
- пишет `/etc/aw-pfsense/poller.json`;
- поднимает `aw-pfsense-poller.service`.
## Результат
- Установлен ActivityWatch Server.
- Создан systemd-unit `activitywatch-server.service`.
- Установлен RU Web UI patch.
- Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя.
- На `#/home` Web UI делит хосты на `Windows RDP` и `Virtual servers + Proxmox`.
- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`.
- Для полного сценария CT создаётся автоматически через `pct create`.
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
## Prod rollout одной командой
Для ручного запуска с dry-run и логированием используйте:
```bash
bash scripts/prod_rollout.sh
```
Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`.
+66
View File
@@ -0,0 +1,66 @@
---
- name: Развернуть pfSense ActivityWatch poller
hosts: aw_pfsense_pollers
become: true
gather_facts: true
vars:
aw_pfsense_install_root: "/opt/aw-pfsense"
aw_pfsense_config_dir: "/etc/aw-pfsense"
aw_pfsense_service_name: "aw-pfsense-poller.service"
tasks:
- name: Установить обязательные пакеты
ansible.builtin.apt:
name:
- python3
state: present
update_cache: true
- name: Создать каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ aw_pfsense_install_root }}"
- "{{ aw_pfsense_config_dir }}"
- name: Установить скрипт pfSense poller
ansible.builtin.copy:
src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.py"
dest: "{{ aw_pfsense_install_root }}/pfsense-aw-poller.py"
mode: "0755"
- name: Установить systemd service
ansible.builtin.copy:
src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.service"
dest: "/etc/systemd/system/{{ aw_pfsense_service_name }}"
mode: "0644"
notify:
- Перезагрузить systemd
- name: Записать конфигурацию pfSense poller
ansible.builtin.copy:
dest: "{{ aw_pfsense_config_dir }}/poller.json"
mode: "0600"
content: "{{ aw_pfsense_poller_config | to_nice_json }}"
notify:
- Перезапустить pfSense poller
- name: Включить и запустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
enabled: true
state: restarted
daemon_reload: true
handlers:
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Перезапустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
state: restarted
+566 -87
View File
@@ -1,5 +1,5 @@
---
- name: Deploy AWatch-rus server
- name: Развернуть сервер AWatch-rus
hosts: aw_server
become: true
gather_facts: true
@@ -9,17 +9,29 @@
aw_release_dir: "{{ aw_release_root }}/{{ aw_server_version }}"
aw_archive_path: "/tmp/activitywatch-{{ aw_server_version }}.zip"
aw_bootstrap_dir: "/tmp/aw-rus-bootstrap"
aw_release_install_dir: "{{ aw_release_root }}/aw-server-rust-{{ aw_server_version }}"
aw_ru_patch_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-ru-patch.js') | hash('sha1') | truncate(12, true, '') }}"
aw_sw_cleanup_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-sw-cleanup.js') | hash('sha1') | truncate(12, true, '') }}"
aw_worktime_classes: "{{ lookup('file', aw_repo_root + '/aw-server/settings/classes-worktime.json') | from_json }}"
aw_default_views: "{{ lookup('file', aw_repo_root + '/aw-server/settings/views-default.json') | from_json }}"
tasks:
- name: Install base packages
- name: Установить базовые пакеты
ansible.builtin.apt:
name:
- curl
- rsync
- unzip
state: present
update_cache: true
- name: Ensure service account exists
- name: Создать системную группу сервиса
ansible.builtin.group:
name: "{{ aw_server_group }}"
system: true
state: present
- name: Создать системную учётную запись сервиса
ansible.builtin.user:
name: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
@@ -28,7 +40,30 @@
system: true
create_home: false
- name: Ensure required directories
- name: Создать обязательные каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_db_path | dirname }}"
- "{{ aw_server_data_dir }}/.config"
- "{{ aw_server_data_dir }}/.config/activitywatch"
- "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust"
- "{{ aw_server_data_dir }}/backups"
- "{{ aw_server_log_dir }}"
- /etc/activitywatch
- "{{ aw_bootstrap_dir }}"
- name: Настроить каталоги ActivityWatch с владельцем сервиса
ansible.builtin.file:
path: "{{ item }}"
state: directory
@@ -36,117 +71,561 @@
group: "{{ aw_server_group }}"
mode: "0755"
loop:
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_db_path | dirname }}"
- "{{ aw_server_data_dir }}/.config"
- "{{ aw_server_data_dir }}/.config/activitywatch"
- "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust"
- "{{ aw_server_data_dir }}/backups"
- "{{ aw_server_log_dir }}"
- /etc/activitywatch
- "{{ aw_bootstrap_dir }}"
- name: Download ActivityWatch release archive
ansible.builtin.get_url:
url: "{{ aw_server_download_url }}"
dest: "{{ aw_archive_path }}"
mode: "0644"
- name: (Check mode) Пропустить установку релиза ActivityWatch
ansible.builtin.debug:
msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped."
when: ansible_check_mode
- name: Unpack ActivityWatch release
ansible.builtin.unarchive:
src: "{{ aw_archive_path }}"
dest: "{{ aw_release_dir }}"
remote_src: true
extra_opts: ["-o"]
- name: Установить релиз ActivityWatch (download/unarchive/install)
when: not ansible_check_mode
block:
- name: Скачать архив релиза ActivityWatch
ansible.builtin.get_url:
url: "{{ aw_server_download_url }}"
dest: "{{ aw_archive_path }}"
mode: "0644"
- name: Discover extracted AW directory
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: directory
patterns: "activitywatch*"
register: aw_release_find
- name: Распаковать релиз ActivityWatch
ansible.builtin.unarchive:
src: "{{ aw_archive_path }}"
dest: "{{ aw_release_dir }}"
remote_src: true
extra_opts: ["-o"]
- name: Set release extracted path
ansible.builtin.set_fact:
aw_release_extracted: "{{ (aw_release_find.files | sort(attribute='path') | map(attribute='path') | list | first) }}"
- name: Найти распакованный каталог ActivityWatch
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
recurse: true
file_type: directory
patterns: "activitywatch*"
register: aw_release_find
- name: Verify extracted directory exists
ansible.builtin.assert:
that:
- aw_release_extracted is defined
- aw_release_extracted | length > 0
fail_msg: "Cannot locate extracted ActivityWatch release directory."
- name: Найти бинарный файл AW server
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
recurse: true
file_type: file
patterns:
- aw-server-rust
- aw-server
register: aw_server_binary_find
- name: Sync release content to /opt/activitywatch
ansible.builtin.command:
cmd: "rsync -a --delete {{ aw_release_extracted }}/ /opt/activitywatch/"
- name: Найти index.html WebUI
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
recurse: true
file_type: file
patterns:
- index.html
register: aw_webui_index_find
- name: Copy bootstrap files from repository
- name: Сохранить пути распакованного релиза (binary + webui index)
ansible.builtin.set_fact:
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
aw_server_binary_path: >-
{{
(
(
(aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list)
| select('match', '.*/aw-server-rust$') | list | first
)
| default(
(
(aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first)
),
true
)
) | default('')
}}
aw_webui_index_path: >-
{{
(
(
(aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list)
| select('search', '/static/index\\.html$') | list | first
)
| default(
(
(aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first)
),
true
)
) | default('')
}}
- name: Сохранить каталог WebUI (dirname index.html)
ansible.builtin.set_fact:
aw_webui_source_path: "{{ aw_webui_index_path | dirname }}"
- name: Проверить, что компоненты релиза найдены
ansible.builtin.assert:
that:
- aw_release_extracted is defined
- aw_release_extracted | length > 0
- aw_server_binary_path is defined
- aw_server_binary_path | length > 0
- aw_webui_source_path is defined
- aw_webui_source_path | length > 0
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
- name: Создать каталог установленного релиза
ansible.builtin.file:
path: "{{ aw_release_install_dir }}"
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Установить бинарный файл AW server
ansible.builtin.copy:
remote_src: true
src: "{{ aw_server_binary_path }}"
dest: "{{ aw_release_install_dir }}/aw-server-rust"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Создать ссылку на активный бинарный файл AW server
ansible.builtin.file:
src: "{{ aw_release_install_dir }}/aw-server-rust"
dest: /opt/activitywatch/bin/aw-server-rust
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
state: link
force: true
- name: Синхронизировать WebUI в RU каталог
ansible.builtin.command:
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
- name: Настроить владельца файлов /opt/activitywatch
ansible.builtin.file:
path: /opt/activitywatch
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
recurse: true
- name: Установить systemd service из шаблона репозитория
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
loop:
- { src: "{{ aw_repo_root }}/aw-server/activitywatch-server.service", dest: "/etc/systemd/system/activitywatch-server.service", mode: "0644" }
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
dest: /etc/systemd/system/activitywatch-server.service
mode: "0644"
content: >-
{{
lookup('file', aw_repo_root + '/aw-server/activitywatch-server.service')
| replace('__AW_SERVER_USER__', aw_server_user)
| replace('__AW_SERVER_GROUP__', aw_server_group)
| replace('__AW_SERVER_DATA_DIR__', aw_server_data_dir)
}}
notify:
- Reload systemd
- Restart activitywatch
- Перезагрузить systemd
- Перезапустить activitywatch
- name: Copy WebUI index template from installed distribution
ansible.builtin.copy:
remote_src: true
src: "/opt/activitywatch/aw-webui/index.html"
dest: "{{ aw_server_webui_dir }}/index.html"
mode: "0644"
- name: (Check mode) Пропустить WebUI patch и запуск сервиса
ansible.builtin.debug:
msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped."
when: ansible_check_mode
- name: Insert RU patch scripts into index.html
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '</head>'
replace: '<script src="/js/sw-cleanup.js?v=ansible1"></script></head>'
- name: Применить WebUI RU patch и запустить сервис
when: not ansible_check_mode
block:
- name: Скопировать RU patch файлы WebUI из репозитория
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
loop:
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
- name: Insert RU patch loader before body end
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '</body>'
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v=ansible1"></script></body>'
- name: Проверить наличие index.html после копирования
ansible.builtin.stat:
path: "{{ aw_server_webui_dir }}/index.html"
register: aw_webui_ru_index
- name: Write /etc/activitywatch/aw-server.env
ansible.builtin.copy:
dest: /etc/activitywatch/aw-server.env
mode: "0640"
content: |
AW_SERVER_HOST={{ aw_server_bind_host }}
AW_SERVER_PORT={{ aw_server_port }}
AW_DATA_DIR={{ aw_server_data_dir }}
AW_LOG_DIR={{ aw_server_log_dir }}
AW_WEBUI_DIR={{ aw_server_webui_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
- name: Проверить, что index.html доступен для RU patch
ansible.builtin.assert:
that:
- aw_webui_ru_index.stat.exists
fail_msg: "Не найден index.html WebUI для применения RU patch."
- name: Enable and start service
ansible.builtin.systemd:
name: activitywatch-server.service
enabled: true
state: restarted
daemon_reload: true
- name: Удалить старые теги RU patch из index.html
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
replace: ''
- name: Wait for API
- name: Добавить cleanup script RU patch в index.html
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '</head>'
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
- name: Добавить загрузчик RU patch перед закрытием body
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '</body>'
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
- name: Записать /etc/activitywatch/aw-server.env
ansible.builtin.copy:
dest: /etc/activitywatch/aw-server.env
mode: "0640"
owner: root
group: root
content: |
AW_SERVER_BIND_HOST={{ aw_server_bind_host }}
AW_SERVER_PORT={{ aw_server_port }}
AW_SERVER_DATA_DIR={{ aw_server_data_dir }}
AW_SERVER_DB_PATH={{ aw_server_db_path }}
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
- name: Скопировать merge script AW DB на сервер
ansible.builtin.copy:
src: "{{ aw_repo_root }}/scripts/merge_aw_server_dbs.py"
dest: /usr/local/bin/merge_aw_server_dbs.py
owner: root
group: root
mode: "0755"
- name: Проверить наличие legacy root DB
ansible.builtin.stat:
path: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
register: aw_legacy_root_db
- name: Проверить наличие target DB
ansible.builtin.stat:
path: "{{ aw_server_db_path }}"
register: aw_target_db
- name: Остановить сервис перед merge server DB
ansible.builtin.systemd:
name: activitywatch-server.service
state: stopped
when: aw_legacy_root_db.stat.exists | default(false)
- name: Создать backup каталоги server DB
ansible.builtin.file:
path: "{{ aw_server_data_dir }}/backups/db"
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
when: aw_legacy_root_db.stat.exists | default(false)
- name: Backup target DB перед merge
ansible.builtin.copy:
remote_src: true
src: "{{ aw_server_db_path }}"
dest: "{{ aw_server_data_dir }}/backups/db/target-before-merge-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0644"
when:
- aw_legacy_root_db.stat.exists | default(false)
- aw_target_db.stat.exists | default(false)
- name: Backup legacy root DB перед merge
ansible.builtin.copy:
remote_src: true
src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
dest: "{{ aw_server_data_dir }}/backups/db/legacy-root-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0644"
when: aw_legacy_root_db.stat.exists | default(false)
- name: Merge legacy root DB в target DB
ansible.builtin.command:
argv:
- python3
- /usr/local/bin/merge_aw_server_dbs.py
- --base
- /root/.local/share/activitywatch/aw-server-rust/sqlite.db
- --overlay
- "{{ aw_server_db_path }}"
- --output
- "{{ aw_server_db_path }}.merged"
when:
- aw_legacy_root_db.stat.exists | default(false)
- aw_target_db.stat.exists | default(false)
- name: Install merged DB as active target DB
ansible.builtin.copy:
remote_src: true
src: "{{ aw_server_db_path }}.merged"
dest: "{{ aw_server_db_path }}"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0644"
when:
- aw_legacy_root_db.stat.exists | default(false)
- aw_target_db.stat.exists | default(false)
- name: Скопировать legacy root DB в target DB если target ещё не существует
ansible.builtin.copy:
remote_src: true
src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db
dest: "{{ aw_server_db_path }}"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0644"
when:
- aw_legacy_root_db.stat.exists | default(false)
- not (aw_target_db.stat.exists | default(false))
- name: Записать aw-server-rust config.toml с разрешёнными CORS origin
ansible.builtin.copy:
dest: "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust/config.toml"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0644"
content: |
cors = [
{% for origin in aw_server_cors_origins | default([]) %}
"{{ origin }}"{% if not loop.last %},{% endif %}
{% endfor %}
]
- name: Включить и запустить сервис
ansible.builtin.systemd:
name: activitywatch-server.service
enabled: true
state: restarted
daemon_reload: true
- name: Дождаться ответа API
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
method: GET
status_code: 200
register: aw_api
retries: 10
delay: 3
until: aw_api.status == 200
- name: Считать текущие server-side settings
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/"
method: GET
status_code: 200
register: aw_api
retries: 10
delay: 3
until: aw_api.status == 200
register: aw_settings_current
when: aw_apply_worktime_settings | default(false) | bool
- name: Считать текущие server-side views
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views"
method: GET
status_code: 200
register: aw_views_current
when: aw_apply_worktime_settings | default(false) | bool
- name: Считать текущие server-side classes
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes"
method: GET
status_code: 200
register: aw_classes_current
when: aw_apply_worktime_settings | default(false) | bool
- name: Создать backup текущих server-side settings/views/classes
ansible.builtin.copy:
dest: "{{ aw_server_data_dir }}/backups/{{ item.name }}-{{ ansible_date_time.iso8601_basic_short }}.json"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0644"
content: "{{ item.payload | to_nice_json }}"
loop:
- name: settings
payload: "{{ aw_settings_current.json | default({}) }}"
- name: views
payload: "{{ aw_views_current.json | default(none) }}"
- name: classes
payload: "{{ aw_classes_current.json | default(none) }}"
when: aw_apply_worktime_settings | default(false) | bool
- name: Настроить DLP Aggregator (Phase 2)
block:
- name: Создать каталог для скриптов
ansible.builtin.file:
path: "/opt/activitywatch/scripts"
state: directory
owner: root
group: root
mode: "0755"
- name: Скопировать агрегатор событий DLP
ansible.builtin.copy:
src: "{{ aw_repo_root }}/scripts/aggregate_dlp_events.py"
dest: "/opt/activitywatch/scripts/aggregate_dlp_events.py"
owner: root
group: root
mode: "0755"
- name: Установить systemd unit для агрегатора
ansible.builtin.copy:
dest: /etc/systemd/system/activitywatch-dlp-aggregator.service
content: |
[Unit]
Description=ActivityWatch DLP Event Aggregator
After=activitywatch-server.service
[Service]
Type=oneshot
User={{ aw_server_user }}
WorkingDirectory={{ aw_server_data_dir }}
ExecStart=/usr/bin/python3 /opt/activitywatch/scripts/aggregate_dlp_events.py \
--aw-url http://127.0.0.1:{{ aw_server_port }}/api/0 \
--sqlite-path {{ aw_server_data_dir }}/dlp_warehouse.sqlite \
--state-path {{ aw_server_data_dir }}/dlp-aggregator-state.json
[Install]
WantedBy=multi-user.target
- name: Установить systemd timer для агрегатора
ansible.builtin.copy:
dest: /etc/systemd/system/activitywatch-dlp-aggregator.timer
content: |
[Unit]
Description=Run ActivityWatch DLP Aggregator every 5 minutes
[Timer]
OnBootSec=1min
OnUnitActiveSec=5min
AccuracySec=1s
[Install]
WantedBy=timers.target
- name: Включить и запустить таймер агрегатора
ansible.builtin.systemd:
name: activitywatch-dlp-aggregator.timer
enabled: true
state: started
daemon_reload: true
- name: Применить базовые worktime settings (classes)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes"
method: POST
body: "{{ aw_worktime_classes }}"
body_format: json
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Применить базовые views для DLP и worktime
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views"
method: POST
body: "{{ aw_default_views }}"
body_format: json
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Вычислить worktime durationDefault из aw_worktime_from/to
ansible.builtin.set_fact:
aw_worktime_duration_default_derived: >-
{{
(
(
(((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) -
(((aw_worktime_from | default('08:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('08:00')).split(':')[1] | int))
) * 60
)
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Нормализовать durationDefault для ночных смен
ansible.builtin.set_fact:
aw_worktime_duration_default_effective: >-
{{
(aw_worktime_duration_default_derived | int)
if (aw_worktime_duration_default_derived | int) > 0
else ((aw_worktime_duration_default_derived | int) + 86400)
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Проверить корректность durationDefault
ansible.builtin.assert:
that:
- aw_worktime_duration_default_effective | int > 0
- aw_worktime_duration_default_effective | int <= 86400
fail_msg: "Некорректный интервал рабочего времени: {{ aw_worktime_from }}..{{ aw_worktime_to }}"
when: aw_apply_worktime_settings | default(false) | bool
- name: Применить базовый период worktime (startOfDay)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
method: POST
body: "\"{{ aw_worktime_start_of_day }}\""
headers:
Content-Type: application/json
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Применить базовый период worktime (durationDefault seconds)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
method: POST
body: "{{ aw_worktime_duration_default_effective | string }}"
headers:
Content-Type: application/json
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Применить always_active_pattern для fallback без AFK
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/always_active_pattern"
method: POST
body: "\"{{ aw_server_always_active_pattern }}\""
headers:
Content-Type: application/json
status_code: [200, 201]
when:
- aw_apply_worktime_settings | default(false) | bool
- (aw_server_always_active_pattern | default('') | string | length) > 0
- name: Применить landingpage профиля
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/landingpage"
method: POST
body: "\"{{ aw_server_landingpage }}\""
headers:
Content-Type: application/json
status_code: [200, 201]
when:
- aw_apply_worktime_settings | default(false) | bool
- (aw_server_landingpage | default('') | string | length) > 0
handlers:
- name: Reload systemd
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Restart activitywatch
- name: Перезапустить activitywatch
ansible.builtin.systemd:
name: activitywatch-server.service
state: restarted
+253
View File
@@ -0,0 +1,253 @@
---
- name: Развернуть Windows/RDP collector'ы AWatch-rus
hosts: aw_windows
gather_facts: false
vars:
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_file_ops_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
aw_windows_recovery_task_name: "ActivityWatch Recovery"
aw_windows_force_task_restart: true
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
aw_windows_fail_on_validation_error: true
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
tasks:
- name: Проверить обязательные переменные
ansible.builtin.assert:
that:
- aw_windows_server_host is defined
- aw_windows_server_port is defined
- aw_windows_server_scheme is defined
- aw_windows_domain is defined
- aw_windows_users_effective | length > 0
- aw_windows_install_root is defined
- aw_windows_state_root is defined
fail_msg: "Не заданы обязательные переменные Windows-развёртывания."
- name: Создать каталоги развёртывания
ansible.windows.win_file:
path: "{{ item }}"
state: directory
loop:
- "{{ aw_windows_deploy_root }}"
- "{{ aw_windows_deploy_root }}\\windows"
- name: Загрузить Windows toolkit развёртывания
ansible.windows.win_copy:
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
loop:
- ActivityWatch.Windows.Common.psd1
- ActivityWatch.Windows.Common.psm1
- browser-domains-native-collector.ps1
- dlp-endpoint-signals-collector.ps1
- file-operations-collector.ps1
- worktime-session-collector.ps1
- migrate-awatch-rus-paths.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
- hardening-recovery.ps1
- validate-deployment.ps1
- web-category-rules.example.json
- dlp-policy.example.json
- name: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell)
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$toolkitDir = "{{ aw_windows_deploy_root }}\windows"
$encIn = New-Object System.Text.UTF8Encoding($false)
$encOut = New-Object System.Text.UTF8Encoding($true)
Get-ChildItem -LiteralPath $toolkitDir -File -Include *.ps1,*.psm1,*.psd1 | ForEach-Object {
$text = [System.IO.File]::ReadAllText($_.FullName, $encIn)
[System.IO.File]::WriteAllText($_.FullName, $text, $encOut)
}
- name: Загрузить список пользователей для доменного развёртывания
ansible.windows.win_copy:
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
content: |
{% for user in aw_windows_users_effective -%}
{{ user }}
{% endfor -%}
- name: Проверить нужен ли migration с legacy ActivityWatch путей
when: aw_windows_migration_enabled | bool
ansible.windows.win_stat:
path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json"
register: aw_windows_legacy_config
- name: Выполнить безопасную migration legacy prod в AWatch-rus
when:
- aw_windows_migration_enabled | bool
- aw_windows_legacy_config.stat.exists | default(false)
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" `
-OldInstallRoot "{{ aw_windows_legacy_install_root }}" `
-OldStateRoot "{{ aw_windows_legacy_state_root }}" `
-NewInstallRoot "{{ aw_windows_install_root }}" `
-NewStateRoot "{{ aw_windows_state_root }}" `
-ToolkitRoot "{{ aw_windows_deploy_root }}\windows"
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8
- name: Запустить Windows/RDP ensemble развёртывание
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$params = @{
ServerScheme = "{{ aw_windows_server_scheme }}"
ServerHost = "{{ aw_windows_server_host }}"
ServerPort = {{ aw_windows_server_port }}
Version = "{{ aw_windows_package_version }}"
Domain = "{{ aw_windows_domain }}"
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
InstallRoot = "{{ aw_windows_install_root }}"
StateRoot = "{{ aw_windows_state_root }}"
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_enabled | bool) else '$false' }}
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
CustomRulesPath = "{{ aw_windows_rules_path }}"
CustomPolicyPath = "{{ aw_windows_policy_path }}"
}
{% if (aw_windows_package_url | default('') | string | length) > 0 %}
$params.PackageUrl = "{{ aw_windows_package_url }}"
{% endif %}
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
{% endif %}
{% if aw_windows_skip_hardening | bool %}
$params.SkipHardening = $true
{% endif %}
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
- name: Удалить лишние ActivityWatch Launch tasks вне текущего deployment-config
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json
$desired = @($config.userTasks | ForEach-Object { [string]$_.LaunchTaskName })
foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch *' })) {
if ($desired -notcontains [string]$task.TaskName) {
Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue
& cmd.exe /c "schtasks /Delete /TN `"$($task.TaskName)`" /F >nul 2>&1" | Out-Null
}
}
- name: Принудительно запустить ActivityWatch recovery и launch tasks
when: aw_windows_force_task_restart | bool
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
Get-ScheduledTask |
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
ForEach-Object { Start-ScheduledTask -TaskName $_.TaskName }
- name: Получить Windows hostname для AW smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
register: aw_windows_hostname_result
changed_when: false
- name: Вычислить AW AFK smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
- aw_windows_hostname_result.stdout is defined
ansible.builtin.set_fact:
aw_windows_api_smoke_check_bucket_effective: "aw-watcher-afk_{{ aw_windows_hostname_result.stdout | trim }}"
- name: Выполнить AW API smoke-check (проверка наличия свежих событий в AFK бакете)
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
method: GET
status_code: 200
register: aw_windows_api_smoke_result
until: aw_windows_api_smoke_result.json | length > 0
retries: 5
delay: 5
ignore_errors: true
- name: Валидировать развёртывание на эндпоинте
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$result = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
return $result
- name: Создать локальную директорию для отчётов валидации
ansible.builtin.file:
path: "{{ aw_windows_validation_local_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
- name: Стянуть отчёт валидации с эндпоинта
ansible.builtin.fetch:
src: "{{ aw_windows_validation_remote_path }}"
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
flat: true
- name: Проверить статус валидации
ansible.builtin.shell: |
python3 - <<'PY'
import json, sys
with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r') as f:
data = json.load(f)
if not data.get('overallOk', False):
print(f"Validation failed for {{ inventory_hostname }}: {data.get('summary', 'Unknown error')}")
sys.exit(1)
PY
delegate_to: localhost
when: aw_windows_fail_on_validation_error | bool
+25 -1
View File
@@ -4,8 +4,32 @@ aw_server_bind_host: "0.0.0.0"
aw_server_port: 5600
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
aw_server_data_dir: "/var/lib/activitywatch"
aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db"
aw_server_log_dir: "/var/log/activitywatch"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_repo_root: "/home/igor/tmp/AWatch-rus"
aw_repo_root: "{{ playbook_dir | dirname }}"
# Применить базовые категории и views для рабочего времени через AW settings API.
# При прод-обновлениях это нужно оставлять включённым, иначе UI остаётся без views/classes.
aw_apply_worktime_settings: true
# Дополнительные origin для aw-server-rust CORS.
# Обязательно включите тот origin, с которого реально открывается Web UI.
aw_server_cors_origins:
- "http://127.0.0.1:5600"
- "http://localhost:5600"
- "http://10.10.10.13:5600"
- "http://aw-server:5600"
# Опциональные значения периода рабочего времени в Web UI.
# startOfDay задаёт границу дня и стартовое время окна отчёта.
# durationDefault задаёт диапазон по умолчанию в секундах.
#
# Рекомендуется явно задать рабочий интервал и дать playbook вычислить duration.
aw_worktime_from: "08:00"
aw_worktime_to: "17:00"
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
aw_server_always_active_pattern: "aw-watcher-window"
aw_server_landingpage: "/activity/SHARKON2025/view/"
+27
View File
@@ -0,0 +1,27 @@
aw_server_version: "v0.13.2"
aw_server_download_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-linux-x86_64.zip"
aw_server_bind_host: "0.0.0.0"
aw_server_port: 5600
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
aw_server_data_dir: "/var/lib/activitywatch"
aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db"
aw_server_log_dir: "/var/log/activitywatch"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_repo_root: "{{ playbook_dir | dirname }}"
# Optional: apply worktime settings via server-side settings API.
aw_apply_worktime_settings: true
aw_server_cors_origins:
- "http://127.0.0.1:5600"
- "http://localhost:5600"
- "http://10.10.10.13:5600"
- "http://aw-server:5600"
aw_worktime_from: "08:00"
aw_worktime_to: "17:00"
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
aw_server_always_active_pattern: "aw-watcher-window"
aw_server_landingpage: "/activity/SHARKON2025/view/"
+9
View File
@@ -0,0 +1,9 @@
# Secret handling:
# - put the real SSH password into env var before running Ansible:
# export AW_SSH_PASSWORD='...'
ansible_password: "{{ lookup('env', 'AW_SSH_PASSWORD') }}"
ansible_become: true
ansible_become_method: sudo
# If sudo password differs, set AW_SUDO_PASSWORD. Otherwise it will reuse AW_SSH_PASSWORD.
ansible_become_password: "{{ lookup('env', 'AW_SUDO_PASSWORD') | default(lookup('env', 'AW_SSH_PASSWORD'), true) }}"
+53
View File
@@ -0,0 +1,53 @@
# Secret handling:
# - put the real password into env var before running Ansible:
# export AW_WINRM_PASSWORD='...'
ansible_password: "{{ lookup('env', 'AW_WINRM_PASSWORD') }}"
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_file_ops_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
aw_windows_fail_on_validation_error: true
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
@@ -0,0 +1,30 @@
aw_pfsense_poller_config:
poll_interval_seconds: 60
aw:
server_host: "10.10.10.13"
server_port: 5600
hostname: "PFSENSE-EDGE01"
pulse_time_seconds: 120
timeout_seconds: 15
pfsense:
name: "pfSense Edge 01"
host: "10.10.10.1"
scheme: "https"
verify_tls: false
timeout_seconds: 15
headers:
X-API-Key: "replace-me"
X-API-Secret: "replace-me"
endpoints:
- name: "system-status"
path: "/api/v2/status/system"
bucket_prefix: "aw-pfsense-health"
bucket_type: "aw.pfsense.health"
- name: "interfaces"
path: "/api/v2/interface"
bucket_prefix: "aw-pfsense-interfaces"
bucket_type: "aw.pfsense.interfaces"
- name: "gateways"
path: "/api/v2/status/gateways"
bucket_prefix: "aw-pfsense-gateways"
bucket_type: "aw.pfsense.gateways"
@@ -0,0 +1,38 @@
proxmox_ct_matrix:
- id: "203"
hostname: "activitywatch-user1"
storage: "local-lvm"
template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
rootfs_size: "8G"
cores: "2"
memory: "2048"
swap: "512"
bridge: "vmbr10"
ip: "10.20.30.13/24"
gw: "10.20.30.1"
vlan: ""
nameserver: "1.1.1.1 8.8.8.8"
searchdomain: "example.internal"
password: "CHANGE_ME"
unprivileged: "1"
onboot: "1"
features: "nesting=1,keyctl=1"
- id: "204"
hostname: "activitywatch-user2"
storage: "local-lvm"
template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
rootfs_size: "8G"
cores: "2"
memory: "2048"
swap: "512"
bridge: "vmbr10"
ip: "10.20.30.14/24"
gw: "10.20.30.1"
vlan: ""
nameserver: "1.1.1.1 8.8.8.8"
searchdomain: "example.internal"
password: "CHANGE_ME"
unprivileged: "1"
onboot: "1"
features: "nesting=1,keyctl=1"
+18
View File
@@ -0,0 +1,18 @@
proxmox_ct_id: "203"
proxmox_ct_hostname: "activitywatch-server"
proxmox_ct_storage: "local-lvm"
proxmox_ct_template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
proxmox_ct_rootfs_size: "8G"
proxmox_ct_cores: "2"
proxmox_ct_memory: "2048"
proxmox_ct_swap: "512"
proxmox_ct_bridge: "vmbr10"
proxmox_ct_ip: "10.20.30.13/24"
proxmox_ct_gw: "10.20.30.1"
proxmox_ct_vlan: ""
proxmox_ct_nameserver: "1.1.1.1 8.8.8.8"
proxmox_ct_searchdomain: "example.internal"
proxmox_ct_password: "CHANGE_ME"
proxmox_ct_unprivileged: "1"
proxmox_ct_onboot: "1"
proxmox_ct_features: "nesting=1,keyctl=1"
+51
View File
@@ -0,0 +1,51 @@
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
# Например:
# aw_windows_extra_users:
# - Администратор
# Единые Windows/RDP пути: те же, что использует InnoSetup.
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_file_ops_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
aw_windows_fail_on_validation_error: true
# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus.
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
# По умолчанию AFK bucket вычисляется как aw-watcher-afk_<COMPUTERNAME>.
# Задайте явное значение только если watcher пишет в нестандартный bucket.
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
+16
View File
@@ -0,0 +1,16 @@
---
# Полный установщик AWatch-rus.
# Выполняет развёртывание одной командой:
# 1) создание Proxmox CT + bootstrap AW (если в inventory есть [proxmox])
# 2) развёртывание AW server на хостах [aw_server]
# 3) развёртывание Windows/RDP collector'ов на [aw_windows]
# 4) развёртывание pfSense poller'а на [aw_pfsense_pollers]
#
# Примечания:
# - Заполняйте только нужные группы inventory для своего окружения.
# - Play без совпадающих host groups Ansible пропускает автоматически.
- import_playbook: provision_proxmox_ct_and_deploy_aw.yml
- import_playbook: deploy_aw_server.yml
- import_playbook: deploy_aw_windows.yml
- import_playbook: deploy_aw_pfsense_poller.yml
+10
View File
@@ -1,2 +1,12 @@
[proxmox]
pve-main ansible_host=192.168.10.2 ansible_user=root ansible_port=22
[aw_server]
aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22
[aw_windows]
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "Administrator".
win-node1 ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
[aw_pfsense_pollers]
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
+14
View File
@@ -0,0 +1,14 @@
[proxmox]
# Optional. Leave empty if you don't use Proxmox provisioning from this repo.
# pve-main ansible_host=10.10.10.2 ansible_user=igor ansible_port=22
[aw_server]
aw-server ansible_host=10.10.10.13 ansible_user=igor ansible_port=22
[aw_windows]
# Note: on RU-localized Windows the built-in admin account name is often "Администратор".
rdp-prod ansible_host=192.168.100.21 ansible_user=Администратор ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
[aw_pfsense_pollers]
# Optional.
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
+90
View File
@@ -0,0 +1,90 @@
---
- name: Post-deploy validation for Windows/RDP AWatch-rus
hosts: aw_windows
gather_facts: false
vars:
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
aw_windows_recovery_task_name: "ActivityWatch Recovery"
aw_windows_force_task_restart: true
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
tasks:
- name: Принудительно запустить ActivityWatch recovery и launch tasks
when: aw_windows_force_task_restart | bool
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
Get-ScheduledTask |
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
ForEach-Object { Start-ScheduledTask -TaskName $_.TaskName }
- name: Получить Windows hostname для AW smoke-check bucket
when: aw_windows_api_smoke_check_enabled | bool
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
register: aw_windows_hostname_result
changed_when: false
- name: Вычислить AW AFK smoke-check bucket
when: aw_windows_api_smoke_check_enabled | bool
ansible.builtin.set_fact:
aw_windows_api_smoke_check_bucket_effective: >-
{{
aw_windows_api_smoke_check_bucket
if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
}}
- name: Дождаться свежих AFK событий на AW server
when: aw_windows_api_smoke_check_enabled | bool
delegate_to: localhost
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
method: GET
return_content: true
register: aw_windows_api_smoke
until: >
aw_windows_api_smoke.status == 200 and
(aw_windows_api_smoke.json | length) > 0 and
(
aw_windows_api_smoke.json
| selectattr('data.status', 'equalto', 'not-afk')
| list
| length
) > 0
retries: 10
delay: 6
- name: Выполнить валидацию и сохранить отчёт на целевом Windows host
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
throw "ActivityWatch validation failed. Report: {{ aw_windows_validation_remote_path }}"
}
- name: Создать локальный каталог для validation reports
ansible.builtin.file:
path: "{{ aw_windows_validation_local_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
- name: Забрать validation report
ansible.builtin.fetch:
src: "{{ aw_windows_validation_remote_path }}"
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
flat: true
- name: Показать путь к отчёту
ansible.builtin.debug:
msg:
- "Validation OK on {{ inventory_hostname }}."
- "Report: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
@@ -0,0 +1,40 @@
---
- name: Создать один Proxmox CT и развернуть AWatch-rus
hosts: proxmox
gather_facts: false
vars:
proxmox_bootstrap_dir: "/tmp/aw-rus-bootstrap"
aw_bootstrap_files:
- install_aw_server.sh
- apply_webui_ru_patch.sh
- activitywatch-server.service
- aw-server.env.example
- aw-ru-patch.js
- aw-sw-cleanup.js
- aw-host-groups.json
- settings/classes-worktime.json
- settings/views-default.json
tasks:
- name: Выполнить workflow создания одного CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ proxmox_ct_id }}"
ct_hostname: "{{ proxmox_ct_hostname }}"
ct_storage: "{{ proxmox_ct_storage }}"
ct_template: "{{ proxmox_ct_template }}"
ct_rootfs_size: "{{ proxmox_ct_rootfs_size }}"
ct_cores: "{{ proxmox_ct_cores }}"
ct_memory: "{{ proxmox_ct_memory }}"
ct_swap: "{{ proxmox_ct_swap }}"
ct_bridge: "{{ proxmox_ct_bridge }}"
ct_ip: "{{ proxmox_ct_ip }}"
ct_gw: "{{ proxmox_ct_gw }}"
ct_vlan: "{{ proxmox_ct_vlan | default('') }}"
ct_nameserver: "{{ proxmox_ct_nameserver | default('') }}"
ct_searchdomain: "{{ proxmox_ct_searchdomain | default('') }}"
ct_password: "{{ proxmox_ct_password }}"
ct_unprivileged: "{{ proxmox_ct_unprivileged }}"
ct_onboot: "{{ proxmox_ct_onboot }}"
ct_features: "{{ proxmox_ct_features }}"
@@ -0,0 +1,50 @@
---
- name: Создать матрицу Proxmox CT и развернуть AWatch-rus с RU patch
hosts: proxmox
gather_facts: false
vars:
proxmox_bootstrap_dir: "/tmp/aw-rus-bootstrap"
aw_bootstrap_files:
- install_aw_server.sh
- apply_webui_ru_patch.sh
- activitywatch-server.service
- aw-server.env.example
- aw-ru-patch.js
- aw-sw-cleanup.js
- aw-host-groups.json
- settings/classes-worktime.json
- settings/views-default.json
tasks:
- name: Проверить, что матрица CT задана
ansible.builtin.assert:
that:
- proxmox_ct_matrix is defined
- proxmox_ct_matrix | length > 0
fail_msg: "Задайте proxmox_ct_matrix в group_vars/proxmox-matrix.yml"
- name: Выполнить workflow создания для каждого CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ item.id }}"
ct_hostname: "{{ item.hostname }}"
ct_storage: "{{ item.storage }}"
ct_template: "{{ item.template }}"
ct_rootfs_size: "{{ item.rootfs_size }}"
ct_cores: "{{ item.cores }}"
ct_memory: "{{ item.memory }}"
ct_swap: "{{ item.swap }}"
ct_bridge: "{{ item.bridge }}"
ct_ip: "{{ item.ip }}"
ct_gw: "{{ item.gw }}"
ct_vlan: "{{ item.vlan | default('') }}"
ct_nameserver: "{{ item.nameserver | default('') }}"
ct_searchdomain: "{{ item.searchdomain | default('') }}"
ct_password: "{{ item.password }}"
ct_unprivileged: "{{ item.unprivileged }}"
ct_onboot: "{{ item.onboot }}"
ct_features: "{{ item.features }}"
loop: "{{ proxmox_ct_matrix }}"
loop_control:
label: "ct={{ item.id }} host={{ item.hostname }} ip={{ item.ip }}"
@@ -0,0 +1,227 @@
---
- name: Проверить обязательные переменные CT
ansible.builtin.assert:
that:
- ct_id is defined
- ct_hostname is defined
- ct_storage is defined
- ct_template is defined
- ct_rootfs_size is defined
- ct_cores is defined
- ct_memory is defined
- ct_swap is defined
- ct_bridge is defined
- ct_ip is defined
- ct_gw is defined
- ct_password is defined
- ct_unprivileged is defined
- ct_onboot is defined
- ct_features is defined
- aw_repo_root is defined
- aw_server_version is defined
- aw_server_download_url is defined
- aw_server_bind_host is defined
- aw_server_port is defined
- aw_server_webui_dir is defined
- aw_server_data_dir is defined
- aw_server_log_dir is defined
- aw_server_user is defined
- aw_server_group is defined
fail_msg: "Не заданы обязательные переменные для создания CT и развёртывания."
- name: Сформировать сетевую строку CT
ansible.builtin.set_fact:
ct_net0: >-
name=eth0,bridge={{ ct_bridge }},ip={{ ct_ip }},gw={{ ct_gw }}{% if (ct_vlan | default('') | string | length) > 0 %},tag={{ ct_vlan }}{% endif %}
- name: Проверить, существует ли CT
ansible.builtin.command:
argv:
- pct
- status
- "{{ ct_id }}"
register: ct_status_check
failed_when: false
changed_when: false
- name: Создать CT, если он отсутствует
ansible.builtin.command:
argv:
- pct
- create
- "{{ ct_id }}"
- "{{ ct_template }}"
- --hostname
- "{{ ct_hostname }}"
- --cores
- "{{ ct_cores }}"
- --memory
- "{{ ct_memory }}"
- --swap
- "{{ ct_swap }}"
- --rootfs
- "{{ ct_storage }}:{{ ct_rootfs_size }}"
- --password
- "{{ ct_password }}"
- --unprivileged
- "{{ ct_unprivileged }}"
- --onboot
- "{{ ct_onboot }}"
- --features
- "{{ ct_features }}"
- --net0
- "{{ ct_net0 }}"
- --nameserver
- "{{ ct_nameserver | default('') }}"
- --searchdomain
- "{{ ct_searchdomain | default('') }}"
- --ostype
- debian
when: ct_status_check.rc != 0
no_log: true
- name: Проверить текущее состояние CT
ansible.builtin.command:
argv:
- pct
- status
- "{{ ct_id }}"
register: ct_runtime_status
changed_when: false
- name: Запустить CT, если он остановлен
ansible.builtin.command:
argv:
- pct
- start
- "{{ ct_id }}"
when: "'stopped' in ct_runtime_status.stdout"
- name: Создать bootstrap каталог на Proxmox host
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0700"
loop:
- "{{ proxmox_bootstrap_dir }}"
- "{{ proxmox_bootstrap_dir }}/settings"
- name: Скопировать AW bootstrap файлы во временный каталог Proxmox host
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/{{ item }}"
dest: "{{ proxmox_bootstrap_dir }}/{{ item }}"
mode: "0644"
loop: "{{ aw_bootstrap_files }}"
- name: Установить базовые зависимости ОС внутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server python3
mkdir -p /root/bootstrap/settings /etc/activitywatch
systemctl enable ssh || true
systemctl restart ssh || true
register: ct_bootstrap_result
retries: 10
delay: 6
until: ct_bootstrap_result.rc == 0
- name: Передать bootstrap файлы внутрь CT
ansible.builtin.command:
argv:
- pct
- push
- "{{ ct_id }}"
- "{{ proxmox_bootstrap_dir }}/{{ item }}"
- "/root/bootstrap/{{ item }}"
loop: "{{ aw_bootstrap_files }}"
- name: Записать AW server env во временный каталог Proxmox host
ansible.builtin.copy:
dest: "{{ proxmox_bootstrap_dir }}/aw-server.env"
mode: "0600"
content: |
AW_SERVER_VERSION={{ aw_server_version }}
AW_SERVER_DOWNLOAD_URL={{ aw_server_download_url }}
AW_SERVER_BIND_HOST={{ aw_server_bind_host }}
AW_SERVER_PORT={{ aw_server_port }}
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
AW_SERVER_DATA_DIR={{ aw_server_data_dir }}
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
no_log: true
- name: Передать AW server env внутрь CT
ansible.builtin.command:
argv:
- pct
- push
- "{{ ct_id }}"
- "{{ proxmox_bootstrap_dir }}/aw-server.env"
- /etc/activitywatch/aw-server.env
no_log: true
- name: Настроить права env файла внутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- chmod
- "0600"
- /etc/activitywatch/aw-server.env
- name: Установить сервер и применить RU patch внутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- |
set -euo pipefail
chmod +x /root/bootstrap/install_aw_server.sh /root/bootstrap/apply_webui_ru_patch.sh
bash /root/bootstrap/install_aw_server.sh
bash /root/bootstrap/apply_webui_ru_patch.sh
systemctl restart activitywatch-server.service
- name: Проверить AW API изнутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null"
- name: Проверить hooks RU patch в index.html
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- "grep -q 'ru-patch-v5.js' {{ aw_server_webui_dir }}/index.html && grep -q 'sw-cleanup.js' {{ aw_server_webui_dir }}/index.html"
- name: Показать итоговый endpoint
ansible.builtin.debug:
msg:
- "CT {{ ct_id }} создан и настроен."
- "Endpoint ActivityWatch: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
+2 -2
View File
@@ -9,7 +9,7 @@ EnvironmentFile=/etc/activitywatch/aw-server.env
User=__AW_SERVER_USER__
Group=__AW_SERVER_GROUP__
WorkingDirectory=__AW_SERVER_DATA_DIR__
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"'
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT" --dbpath "$AW_SERVER_DB_PATH" --webpath "$AW_SERVER_WEBUI_DIR"'
Restart=on-failure
RestartSec=5s
StateDirectory=activitywatch
@@ -17,7 +17,7 @@ LogsDirectory=activitywatch
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
ProtectHome=read-only
LimitNOFILE=65535
[Install]
+85 -7
View File
@@ -9,25 +9,103 @@ fi
source "$ENV_FILE"
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-/opt/activitywatch/webui-ru}"
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}"
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js"
HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json"
INDEX_HTML="$WEBUI_DIR/index.html"
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
TS=$(date +%Y%m%d%H%M%S)
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
TIMESPIRAL_NEEDLE='start:new Date("2022-08-08")'
TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)'
CATEGORY_HELPER_NEEDLE='hostname:t.hostnameChoices[0]'
CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t}))[0]||t.hostnameChoices[0]'
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; }
[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; }
install -d "$WEBUI_DIR/js"
install -m 0644 "$PATCH_JS_SRC" "$WEBUI_DIR/js/aw-ru-patch.js"
install -m 0644 "$SW_CLEANUP_SRC" "$WEBUI_DIR/js/aw-sw-cleanup.js"
install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET"
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
sed -i '/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
sed -i 's#</head>#<script src="/js/aw-sw-cleanup.js"></script></head>#' "$INDEX_HTML"
sed -i 's#</body>#<script defer="defer" src="/js/aw-ru-patch.js"></script></body>#' "$INDEX_HTML"
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
sed -i '/ru-patch-v5.js/d;/sw-cleanup.js/d;/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
sed -i "s#</head>#<script src=\"/js/sw-cleanup.js?v=$sw_hash\"></script></head>#" "$INDEX_HTML"
sed -i "s#</body>#<script defer=\"defer\" src=\"/js/ru-patch-v5.js?v=$patch_hash\"></script></body>#" "$INDEX_HTML"
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
echo "RU patch applied to $WEBUI_DIR"
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
if [[ -n "$trends_chunk" ]]; then
cp "$trends_chunk" "$trends_chunk.bak.$TS"
python3 - "$trends_chunk" "$TRENDS_NEEDLE" "$TRENDS_REPLACEMENT" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
old = sys.argv[2]
new = sys.argv[3]
content = path.read_text()
if old in content:
path.write_text(content.replace(old, new, 1))
print(f"Trends hotfix applied to {path}")
else:
print(f"Trends hotfix already present in {path}")
PY
else
echo "Trends hotfix skipped: chunk not found"
fi
timespiral_chunk="$(grep -Rsl "$TIMESPIRAL_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
if [[ -n "$timespiral_chunk" ]]; then
cp "$timespiral_chunk" "$timespiral_chunk.bak.$TS"
python3 - "$timespiral_chunk" "$TIMESPIRAL_NEEDLE" "$TIMESPIRAL_REPLACEMENT" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
old = sys.argv[2]
new = sys.argv[3]
content = path.read_text()
if old in content:
path.write_text(content.replace(old, new, 1))
print(f"Timespiral hotfix applied to {path}")
else:
print(f"Timespiral hotfix already present in {path}")
PY
else
echo "Timespiral hotfix skipped: chunk not found"
fi
category_helper_chunk="$(grep -Rsl "$CATEGORY_HELPER_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
if [[ -n "$category_helper_chunk" ]]; then
cp "$category_helper_chunk" "$category_helper_chunk.bak.$TS"
python3 - "$category_helper_chunk" "$CATEGORY_HELPER_NEEDLE" "$CATEGORY_HELPER_REPLACEMENT" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
old = sys.argv[2]
new = sys.argv[3]
content = path.read_text()
if old in content:
path.write_text(content.replace(old, new, 1))
print(f"Category helper host hotfix applied to {path}")
else:
print(f"Category helper host hotfix already present in {path}")
PY
else
echo "Category helper host hotfix skipped: chunk not found"
fi
echo "RU patch applied to $WEBUI_DIR (ru-patch-v5.js?v=$patch_hash)"
+62
View File
@@ -0,0 +1,62 @@
{
"groups": [
{
"id": "pve-detmir",
"name": "pve-detmir",
"description": "Выделенный клиент DetMir в разделе Активность.",
"patterns": [
"^pve-detmir$"
],
"links": [
{ "label": "Активность", "type": "activity", "view": "pve_audit" },
{ "label": "Web-admin аудит", "type": "bucket", "bucket_prefix": "aw-pve-webadmin-events_" },
{ "label": "PVE tasks", "type": "bucket", "bucket_prefix": "aw-pve-task-events_" },
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
{ "label": "Все бакеты", "type": "buckets" }
]
},
{
"id": "windows-rdp",
"name": "Windows RDP",
"description": "Пользовательские Windows/RDP хосты с активностью, DLP и рабочим временем.",
"patterns": [
"^(SHARKON|WIN|RDP|TERM|TS-|WS-)"
],
"links": [
{ "label": "Активность", "type": "activity" },
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
]
},
{
"id": "linux-remote",
"name": "Linux remote workers",
"description": "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
"patterns": [
"^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"
],
"links": [
{ "label": "Активность", "type": "activity" },
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
{ "label": "Все бакеты", "type": "buckets" }
]
},
{
"id": "virtual-infra",
"name": "Virtual servers + Proxmox",
"description": "Инфраструктурные VM и сетевые узлы. Здесь должны лежать Proxmox, pfSense, Debian и Ubuntu серверы.",
"patterns": [
"^(PFSENSE|PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"
],
"links": [
{ "label": "pfSense health", "type": "bucket", "bucket_prefix": "aw-pfsense-health_" },
{ "label": "pfSense gateways", "type": "bucket", "bucket_prefix": "aw-pfsense-gateways_" },
{ "label": "Все бакеты", "type": "buckets" }
]
}
],
"ungrouped_name": "Прочие хосты"
}
+1551 -5
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -21,6 +21,10 @@ required_vars=(
AW_SERVER_GROUP
)
BOOTSTRAP_DIR="/root/bootstrap"
VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
for var_name in "${required_vars[@]}"; do
if [[ -z "${!var_name:-}" ]]; then
echo "missing required variable: $var_name" >&2
@@ -84,3 +88,30 @@ systemctl daemon-reload
systemctl enable activitywatch-server.service
systemctl restart activitywatch-server.service
systemctl --no-pager --full status activitywatch-server.service || true
for _ in $(seq 1 20); do
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
break
fi
sleep 2
done
if [[ -f "$CLASSES_JSON" ]]; then
curl -fsS -X POST \
-H 'Content-Type: application/json' \
--data-binary @"$CLASSES_JSON" \
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/classes" >/dev/null
echo "Applied worktime classes from $CLASSES_JSON"
else
echo "Worktime classes bootstrap not found, skipped: $CLASSES_JSON"
fi
if [[ -f "$VIEWS_JSON" ]]; then
curl -fsS -X POST \
-H 'Content-Type: application/json' \
--data-binary @"$VIEWS_JSON" \
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/views" >/dev/null
echo "Applied baseline views from $VIEWS_JSON"
else
echo "Views bootstrap not found, skipped: $VIEWS_JSON"
fi
+90
View File
@@ -0,0 +1,90 @@
[
{
"id": 0,
"name": ["Работа"],
"rule": { "type": "none" },
"data": {}
},
{
"id": 1,
"name": ["Работа", "1С"],
"rule": {
"type": "regex",
"regex": "\\b(1cv8s?|1cv8c|1cestart)\\.exe\\b|1С:Предприятие|Запуск 1С:Предприятия|Загрузка конфигурационной информации|Доступ к информационной базе",
"ignore_case": true
},
"data": { "color": "#194D33" }
},
{
"id": 2,
"name": ["Работа", "Документы"],
"rule": {
"type": "regex",
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64|libreoffice|writer|calc)\\.exe\\b|LibreOffice|OnlyOffice|Adobe Reader|Acrobat",
"ignore_case": true
},
"data": { "color": "#2E7D32" }
},
{
"id": 3,
"name": ["Работа", "Коммуникации"],
"rule": {
"type": "regex",
"regex": "\\b(teams|telegram|slack|thunderbird|zoom|skype|whatsapp|viber|discord)\\.exe\\b|Mattermost|Element|Riot",
"ignore_case": true
},
"data": { "color": "#1E88E5" }
},
{
"id": 4,
"name": ["Работа", "Администрирование"],
"rule": {
"type": "regex",
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd|gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\.exe\\b|\\b(gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\b|Proxmox Virtual Environment|\\bpfSense\\b|\\bGrafana\\b|\\bKibana\\b|\\bPortainer\\b",
"ignore_case": true
},
"data": { "color": "#6D4C41" }
},
{
"id": 5,
"name": ["Интернет"],
"rule": { "type": "none" },
"data": {}
},
{
"id": 6,
"name": ["Интернет", "Браузер"],
"rule": {
"type": "regex",
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser|chromium)\\.exe\\b|\\b(chrome|chromium|firefox|opera|brave|vivaldi)\\b",
"ignore_case": true
},
"data": { "color": "#00897B" }
},
{
"id": 7,
"name": ["Система"],
"rule": { "type": "none" },
"data": {}
},
{
"id": 8,
"name": ["Система", "Windows"],
"rule": {
"type": "regex",
"regex": "\\b(SearchHost|explorer|ShellExperienceHost|ApplicationFrameHost|RuntimeBroker|sihost|dwm|svchost|fontdrvhost|userinit)\\.exe\\b|\\\\Windows\\\\System32",
"ignore_case": true
},
"data": { "color": "#607D8B" }
},
{
"id": 9,
"name": ["ActivityWatch"],
"rule": {
"type": "regex",
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b",
"ignore_case": true
},
"data": {}
}
]
+39
View File
@@ -0,0 +1,39 @@
[
{
"id": "summary",
"name": "Summary",
"elements": [
{ "type": "top_titles", "size": 3 },
{ "type": "timeline_barchart", "size": 3 },
{ "type": "top_categories", "size": 3 },
{ "type": "category_tree", "size": 3 }
]
},
{
"id": "window",
"name": "Window",
"elements": [
{ "type": "top_apps", "size": 3, "props": {} }
]
},
{
"id": "DLP",
"name": "DLP",
"elements": []
},
{
"id": "worktime",
"name": "Worktime",
"elements": [
{ "type": "top_categories", "size": 3, "props": {} },
{ "type": "timeline_barchart", "size": 3, "props": {} },
{ "type": "category_tree", "size": 3, "props": {} },
{ "type": "top_apps", "size": 3, "props": {} }
]
},
{
"id": "pve_audit",
"name": "PVE Audit",
"elements": []
}
]
+113
View File
@@ -0,0 +1,113 @@
# 1С-Бухгалтерия: сбор и анализ в Grafana
Документ описывает внедрение контура мониторинга 1С в Grafana через Prometheus и SQL Exporter.
## 1. Целевая схема
1. База 1С (PostgreSQL или MS SQL) содержит KPI views.
2. `sql-exporter` опрашивает views read-only пользователем.
3. `prometheus` собирает метрики и считает alert-rules.
4. `grafana` визуализирует KPI и алерты.
## 2. Подготовка
Рабочий каталог:
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c`
Подготовка env:
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c
cp /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/.env.example \
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/.env
```
Обязательно изменить:
- `GRAFANA_ADMIN_PASSWORD`
- `ONEC_DSN`
## 3. SQL-представления KPI
Используйте шаблон под вашу СУБД:
- PostgreSQL: `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`
- MS SQL: `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`
После адаптации шаблона под фактические таблицы 1С создайте views в БД.
Быстрое профилирование схемы 1С (PostgreSQL):
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
Проверка KPI views перед запуском:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
## 4. Запуск стека
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c
docker compose up -d
```
## 5. Верификация
Проверка метрик sql-exporter:
```bash
curl -fsS http://127.0.0.1:9399/metrics | rg "onec_"
```
Проверка Prometheus:
```bash
curl -fsS http://127.0.0.1:9090/-/healthy
```
Проверка Grafana:
- URL: `http://<host>:3000`
- Дашборд: `1C Бухгалтерия — Обзор`
- Дашборд: `1C Бухгалтерия — SRE`
E2E health-check:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh
```
## 6. KPI и алерты
Метрики:
- `onec_unposted_documents_total`
- `onec_sales_amount_today`
- `onec_overdue_receivables_total`
- `onec_posting_errors_total`
- `onec_data_freshness_seconds`
Алерты:
- `OneCUnpostedDocumentsHigh` (warning)
- `OneCOverdueReceivablesCritical` (critical)
- `OneCSQLExporterDown` (critical)
- `OneCDataFreshnessCritical` (critical)
Файл правил:
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/alerts.yml`
## 7. Эксплуатационный минимум
- Доступ к БД 1С только read-only.
- Права только на KPI views.
- Ротация пароля read-only пользователя не реже 90 дней.
- Бэкап `grafana-data` и `prometheus-data` (docker volumes).
+134 -46
View File
@@ -6,18 +6,22 @@
## 0) Структура проекта (полные пути)
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/create-ct.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/push-aw-artifacts.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/install_aw_server.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/aw-server/apply_webui_ru_patch.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/deploy-single-user.ps1`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/deploy-domain-users.ps1`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/deploy-ensemble.ps1`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/hardening-recovery.ps1`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/validate-deployment.ps1`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/ansible/deploy_aw_server.yml`
- `/home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env`
- `/home/igor/tmp/AWatch-rus/proxmox/create-ct.sh`
- `/home/igor/tmp/AWatch-rus/proxmox/push-aw-artifacts.sh`
- `/home/igor/tmp/AWatch-rus/aw-server/install_aw_server.sh`
- `/home/igor/tmp/AWatch-rus/aw-server/apply_webui_ru_patch.sh`
- `/home/igor/tmp/AWatch-rus/windows/deploy-single-user.ps1`
- `/home/igor/tmp/AWatch-rus/windows/deploy-domain-users.ps1`
- `/home/igor/tmp/AWatch-rus/windows/deploy-ensemble.ps1`
- `/home/igor/tmp/AWatch-rus/windows/hardening-recovery.ps1`
- `/home/igor/tmp/AWatch-rus/windows/validate-deployment.ps1`
- `/home/igor/tmp/AWatch-rus/windows/browser-domains-native-collector.ps1`
- `/home/igor/tmp/AWatch-rus/windows/dlp-endpoint-signals-collector.ps1`
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_server.yml`
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_and_deploy_aw.yml`
- `/home/igor/tmp/AWatch-rus/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml`
- `/home/igor/tmp/AWatch-rus/ansible/deploy_aw_windows.yml`
---
@@ -35,11 +39,11 @@
Скопируйте шаблон:
```bash
cp /mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env.example \
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env
cp /home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env.example \
/home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env
```
Заполните в файле `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`:
Заполните в файле `/home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env`:
- все `CT_*` параметры контейнера;
- все `AW_SERVER_*` параметры сервера;
@@ -51,30 +55,60 @@ cp /mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env.examp
## 2) Развёртывание сервера в Proxmox
### 2.0 Ansible full-stack (создание CT + установка AW)
Подготовьте:
- `/home/igor/tmp/AWatch-rus/ansible/inventory.ini`
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/all.yml`
- `/home/igor/tmp/AWatch-rus/ansible/group_vars/proxmox.yml`
Запуск:
```bash
cd /home/igor/tmp/AWatch-rus/ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml
```
Этот сценарий полностью закрывает:
- создание CT в Proxmox;
- bootstrap пакетов в CT;
- установку ActivityWatch Server;
- применение RU Web UI patch;
- проверку API.
Для массового режима (несколько CT):
```bash
cd /home/igor/tmp/AWatch-rus/ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
```
### 2.1 Создать LXC контейнер
На узле Proxmox:
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/create-ct.sh
cd /home/igor/tmp/AWatch-rus
/home/igor/tmp/AWatch-rus/proxmox/create-ct.sh
```
По умолчанию читается:
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`
- `/home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env`
При необходимости можно передать другой путь:
```bash
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/create-ct.sh /absolute/path/to/deploy.secrets.env
/home/igor/tmp/AWatch-rus/proxmox/create-ct.sh /absolute/path/to/deploy.secrets.env
```
### 2.2 Загрузить bootstrap-артефакты и env внутрь CT
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/push-aw-artifacts.sh
cd /home/igor/tmp/AWatch-rus
/home/igor/tmp/AWatch-rus/proxmox/push-aw-artifacts.sh
```
Скрипт загружает в CT:
@@ -100,6 +134,13 @@ bash /root/bootstrap/apply_webui_ru_patch.sh
systemctl restart activitywatch-server.service
```
После применения патча доступны:
- верхнее меню `DLP` в Web UI;
- DLP-страница bucket `aw-dlp-endpoint-signals_<HOST>`;
- встроенный центр `DLP review и правила`;
- служебные buckets `aw-dlp-review_<HOST>` и `aw-dlp-rules_<HOST>`.
### 2.5 Проверка сервера
В CT:
@@ -118,6 +159,12 @@ grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
- порт 5600 слушается;
- в `index.html` присутствуют оба скрипта.
Дополнительно после первого входа в Web UI:
- `#/home` должен показывать один корректный пункт `DLP`;
- `#/buckets/aw-dlp-endpoint-signals_<HOST>` должен открываться без ошибок;
- сохранение review/rule должно создавать buckets `aw-dlp-review_<HOST>` и `aw-dlp-rules_<HOST>`.
---
## 3) Развёртывание Windows-клиентов (другой AD-домен)
@@ -126,11 +173,11 @@ grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
Скопируйте каталог:
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows`
- `/home/igor/tmp/AWatch-rus/windows`
например в:
- `C:\Deploy\ActivityWatch-Russian\windows`
- `C:\Program Files\AWatch-rus\windows`
Откройте **elevated PowerShell**:
@@ -140,15 +187,28 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
### 3.2 Массовое доменное развёртывание (рекомендуется)
Если текущий production ещё работает в старых каталогах
`C:\Program Files\ActivityWatch-Phase2` и `C:\ProgramData\ActivityWatch-Phase2`,
сначала выполните безопасную миграцию:
```powershell
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 -WhatIf
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1
```
Скрипт остановит `ActivityWatch Recovery`/`ActivityWatch Launch *`, создаст backup в
`C:\ProgramData\AWatch-rus\migration-backups\...`, перенесёт файлы в единые пути,
пересоздаст `deployment-config.json`/scheduled tasks и запустит validation.
Пример со списком пользователей:
```powershell
C:\Deploy\ActivityWatch-Russian\windows\deploy-domain-users.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-domain-users.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-Domain CONTOSO `
-UserListPath C:\Deploy\aw-users.txt `
-CustomRulesPath C:\Deploy\ActivityWatch-Russian\windows\web-category-rules.example.json
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json
```
Поддерживаемые варианты:
@@ -160,7 +220,7 @@ C:\Deploy\ActivityWatch-Russian\windows\deploy-domain-users.ps1 `
### 3.2.1 Ensemble orchestration (рекомендуется для production)
```powershell
C:\Deploy\ActivityWatch-Russian\windows\deploy-ensemble.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-ensemble.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-Domain CONTOSO `
@@ -170,30 +230,30 @@ C:\Deploy\ActivityWatch-Russian\windows\deploy-ensemble.ps1 `
Отчёт сохраняется в:
- `C:\ProgramData\ActivityWatch\ensemble-report-YYYYMMDD-HHMMSS.json`
- `C:\ProgramData\AWatch-rus\ensemble-report-YYYYMMDD-HHMMSS.json`
### 3.3 Single-user развёртывание
```powershell
C:\Deploy\ActivityWatch-Russian\windows\deploy-single-user.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-single-user.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-TargetUser 'CONTOSO\user01' `
-CustomRulesPath C:\Deploy\ActivityWatch-Russian\windows\web-category-rules.example.json
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json
```
### 3.4 Recovery / hardening
```powershell
C:\Deploy\ActivityWatch-Russian\windows\hardening-recovery.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
C:\Program Files\AWatch-rus\windows\hardening-recovery.ps1 `
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
```
### 3.5 Валидация deployment-а (PowerShell report)
```powershell
$report = C:\Deploy\ActivityWatch-Russian\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
$report = C:\Program Files\AWatch-rus\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
$report | ConvertTo-Json -Depth 12
```
@@ -201,13 +261,13 @@ $report | ConvertTo-Json -Depth 12
## 4) Что должно появиться на Windows после установки
- `C:\Program Files\ActivityWatch`
- `C:\ProgramData\ActivityWatch\deployment-config.json`
- `C:\ProgramData\ActivityWatch\launch-watchers.ps1`
- `C:\ProgramData\ActivityWatch\recovery-loop.ps1`
- `C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1`
- `C:\ProgramData\ActivityWatch\web-category-rules.json`
- `C:\ProgramData\ActivityWatch\logs\`
- `C:\Program Files\AWatch-rus\bin`
- `C:\ProgramData\AWatch-rus\deployment-config.json`
- `C:\ProgramData\AWatch-rus\launch-watchers.ps1`
- `C:\ProgramData\AWatch-rus\recovery-loop.ps1`
- `C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1`
- `C:\ProgramData\AWatch-rus\web-category-rules.json`
- `C:\ProgramData\AWatch-rus\logs\`
Задачи планировщика:
@@ -245,6 +305,9 @@ curl -sS http://127.0.0.1:5600/api/0/buckets | jq 'keys'
- `aw-watcher-window_<HOST>`
- `aw-watcher-web-<browser>_<HOST>`
- `aw-detmir-web-category_<HOST>` (категоризованный поток)
- `aw-dlp-endpoint-signals_<HOST>` (endpoint сигналы)
- `aw-dlp-review_<HOST>` (ручная классификация через UI)
- `aw-dlp-rules_<HOST>` (suppress/rule записи через UI)
Проверка событий браузера:
@@ -258,6 +321,31 @@ curl -sS "http://127.0.0.1:5600/api/0/buckets/aw-watcher-web-edge_<HOST>/events?
curl -sS "http://127.0.0.1:5600/api/0/buckets/aw-detmir-web-category_<HOST>/events?limit=5" | jq
```
Проверка DLP review/rules:
```bash
curl -sS "http://127.0.0.1:5600/api/0/buckets/aw-dlp-review_<HOST>/events?limit=20" | jq
curl -sS "http://127.0.0.1:5600/api/0/buckets/aw-dlp-rules_<HOST>/events?limit=20" | jq
```
Ожидаемые поля review:
- `reviewId`
- `signalType`
- `verdict`
- `category`
- `comment`
- `archived`
Ожидаемые поля rules:
- `ruleId`
- `signalType`
- `match`
- `category`
- `comment`
- `enabled`
---
## 6) Сопровождение (обязательно)
@@ -283,11 +371,11 @@ pct exec <CT_ID> -- tar -C / -czf /root/activitywatch-config-backup.tgz \
### 6.2 Обновление сервера
1. Обновить `AW_SERVER_VERSION` и `AW_SERVER_DOWNLOAD_URL` в
`/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`
`/home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env`
2. Выполнить:
```bash
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/push-aw-artifacts.sh
/home/igor/tmp/AWatch-rus/proxmox/push-aw-artifacts.sh
pct enter <CT_ID>
bash /root/bootstrap/install_aw_server.sh
bash /root/bootstrap/apply_webui_ru_patch.sh
@@ -315,7 +403,7 @@ systemctl restart activitywatch-server.service
## 7) Безопасность
- Не хранить реальные секреты вне `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`.
- Не хранить реальные секреты вне `/home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env`.
- Не открывать `5600/tcp` в интернет напрямую.
- Публиковать через VPN или reverse proxy с ограничением доступа.
- Перед изменениями всегда делать backup.
@@ -324,9 +412,9 @@ systemctl restart activitywatch-server.service
## 8) Короткий чек-лист ввода в эксплуатацию
1. Заполнен `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`.
2. Выполнен `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/create-ct.sh`.
3. Выполнен `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/push-aw-artifacts.sh`.
1. Заполнен `/home/igor/tmp/AWatch-rus/secrets/deploy.secrets.env`.
2. Выполнен `/home/igor/tmp/AWatch-rus/proxmox/create-ct.sh`.
3. Выполнен `/home/igor/tmp/AWatch-rus/proxmox/push-aw-artifacts.sh`.
4. В CT выполнены `/root/bootstrap/install_aw_server.sh` и `/root/bootstrap/apply_webui_ru_patch.sh`.
5. Сервер API/порт/UI проверены.
6. На Windows выполнен `deploy-domain-users.ps1`.
+55
View File
@@ -0,0 +1,55 @@
# POST MEMORY — AWatch-rus (2026-04-27)
## Контекст дня
Работа велась по проекту `AWatch-rus / ActivityWatch-Russian`.
## Что подтверждено по runtime
- `10.10.10.2` (`pve-detmir`, admin host) реально запускает:
- `/usr/local/bin/aw-server-rust --host 0.0.0.0 --port 5600 --webpath /opt/aw-webui-ru`
- `/usr/bin/python3 /opt/aw-pfsense/pfsense-aw-poller.py --config /etc/aw-pfsense/poller.json`
- `10.10.10.1` используется только как API-цель для `pfSense poller`.
- Значит интеграция `pfSense -> AW` работает через внешний poller, а не через установку чего-либо на сам firewall.
## Что сделано
1. Восстановлена и синхронизирована локальная копия проекта.
- В `/mnt/usb_hdd2/Projects/ActivityWatch-Russian` был поврежден `.git`.
- Источник истины: `/home/igor/tmp/AWatch-rus` с `origin https://github.com/igor04091968/AWatch-rus.git`.
- Локальная копия приведена к актуальному содержимому `main`.
2. Завершена русификация overview-дашборда Grafana.
- `1C Бухгалтерия — Overview` заменено на `1C Бухгалтерия — Обзор`.
- Документация синхронизирована с фактическим названием.
- Проверка `rg -n "Overview"` по репозиторию после правки не дала совпадений.
3. Проверен локальный прокси-контур.
- `socks5://127.0.0.1:1080` активен.
- `sing-box` запущен и пропускает трафик.
4. Подготовлен и развернут Linux-клиент ActivityWatch для `10.10.10.2`.
- В репозиторий добавлен rollout:
- `scripts/install_aw_linux_client.sh`
- `docs/linux-client.md`
- На `10.10.10.2` под пользователем `admin` установлен bundle `ActivityWatch 0.13.2` в:
- `~/.local/opt/activitywatch/v0.13.2/activitywatch`
- Созданы:
- `~/.config/activitywatch/aw-client/aw-client.toml` с `10.10.10.13:5600`
- `~/.config/activitywatch/aw-qt/aw-qt.toml`
- `~/.local/bin/activitywatch-remote-aw`
- `~/.config/autostart/activitywatch-remote-aw.desktop`
- Проверка показала:
- удаленный `AW server` с `10.10.10.2` достижим;
- `aw-qt --no-gui` стартует;
- bucket `aw-watcher-afk_pve-detmir` появился на сервере.
- Текущий blocker:
- на `10.10.10.2` нет активной `X11/GUI` сессии;
- `aw-watcher-window` падает с `DISPLAY environment variable not set`;
- `aw-watcher-afk` падает на `failed to acquire X connection`.
## Зафиксированные выводы
- На текущем этапе `10.10.10.2` является фактическим runtime-хостом для `AW server` и `pfSense poller`.
- Документация должна исходить из host-based сценария как из подтвержденного рабочего контура, а CT/LXC-схему держать как отдельный вариант развертывания.
- Для Linux-клиента на `10.10.10.2` установка завершена, но полноценные watcher-события начнутся только после реального графического логина пользователя.
+60
View File
@@ -0,0 +1,60 @@
# Console/SSH logger deployment
## Назначение
Этот сценарий логирует только консольные действия и SSH-сессии:
- команды из `bash_history` (`aw-console-commands_<HOST>`);
- логины/логауты SSH TTY по `who -u` (`aw-ssh-sessions_<HOST>`, best-effort).
GUI watcher'ы (`aw-watcher-window`, `aw-watcher-afk`) не требуются.
## Установка (user-space)
```bash
cd /path/to/AWatch-rus
sh ./scripts/install_aw_console_ssh_logger.sh \
--server-host 10.10.10.13 \
--server-port 5600
```
## Что создается
- `~/.local/opt/aw-console-ssh-logger/collector.py`
- `~/.local/opt/aw-console-ssh-logger/config.json`
- `~/.local/bin/aw-console-ssh-logger-start`
- `~/.local/bin/aw-console-ssh-logger-stop`
- `~/.local/bin/aw-console-ssh-logger-status`
- `~/.config/systemd/user/aw-console-ssh-logger.service`
- `~/.local/state/aw-console-ssh-logger/` (state + logs)
## Shell hooks
Installer добавляет в `~/.bashrc`:
- `HISTTIMEFORMAT="%s "` для epoch timestamp;
- `histappend`;
- `PROMPT_COMMAND` с `history -a; history -n;`.
Это нужно, чтобы команды попадали в `~/.bash_history` сразу после выполнения, а не только при logout.
## Проверка
На клиенте:
```bash
~/.local/bin/aw-console-ssh-logger-status
tail -n 50 ~/.local/state/aw-console-ssh-logger/logs/collector.log
```
На AW сервере:
```bash
curl -fsS http://10.10.10.13:5600/api/0/buckets | jq -r 'keys[]' | grep -E '^aw-console-commands_|^aw-ssh-sessions_'
```
## Ограничения
- Источник команд — `bash_history`; если пользователь использует не `bash`, нужен отдельный collector.
- Канал `aw-ssh-sessions_*` зависит от корректного `utmp/who` на хосте и может быть неполным в отдельных окружениях.
- Полные системные audit-события (`execve` всех пользователей) требуют root-level `auditd`/journald integration и в этот user-space сценарий не входят.
+20 -1
View File
@@ -1,5 +1,21 @@
# Deployment
## 0. Ansible full-stack вариант (рекомендуется)
```sh
cd /home/igor/tmp/AWatch-rus/ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml
```
Этот playbook создаёт CT в Proxmox и полностью разворачивает ActivityWatch внутри контейнера.
Массовый вариант по матрице CT:
```sh
cd /home/igor/tmp/AWatch-rus/ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
```
## 1. Подготовить env-файлы
На рабочей машине оператора:
@@ -78,7 +94,8 @@ systemctl restart activitywatch-server.service
- копирует `aw-ru-patch.js` и `aw-sw-cleanup.js`;
- делает backup `index.html`;
- добавляет include в `index.html`;
- заменяет `service-worker.js` cleanup-версией для сброса старого cache.
- заменяет `service-worker.js` cleanup-версией для сброса старого cache;
- включает DLP Web UI override: верхнее меню `DLP`, центр `DLP review и правила`, buckets `aw-dlp-review_<HOST>` и `aw-dlp-rules_<HOST>`.
## 6. Проверить сервис
@@ -103,6 +120,8 @@ grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
- открывается `/`;
- API доступен по ожидаемому URL;
- после hard refresh видна русификация;
- в верхнем меню доступен `DLP` и он открывает `#/buckets/aw-dlp-endpoint-signals_<HOST>`;
- на DLP bucket-странице доступны секции `DLP review и правила`, `DLP Rules`, `DLP Review`;
- reverse proxy не кэширует старую статику.
## 8. Что зафиксировать после ввода
+103
View File
@@ -0,0 +1,103 @@
# Central DLP aggregator prototype
`scripts/aggregate_dlp_events.py` collects Phase 2 DLP telemetry from ActivityWatch buckets and stores normalized rows in one database for Grafana/SIEM-style reporting.
## Streams
The prototype reads:
- `aw-file-operations_*` (`aw.file.operation`) — file create/delete/rename telemetry, including `archiveHint`.
- `aw-dlp-incidents_*` (`aw.dlp.incident`) — browser/endpoint DLP incidents and screenshot metadata when available.
## SQLite smoke test
SQLite is the default so the collector can be tested without deploying PostgreSQL:
```bash
python3 scripts/aggregate_dlp_events.py \
--aw-url http://10.10.10.13:5600/api/0 \
--sqlite-path data/dlp-events.sqlite3 \
--lookback-hours 24
```
Useful checks:
```bash
sqlite3 data/dlp-events.sqlite3 \
"select stream_type, hostname, count(*) from dlp_events group by 1,2 order by 3 desc;"
sqlite3 data/dlp-events.sqlite3 \
"select event_ts, hostname, username, file_path from dlp_file_operations where archive_hint = 1 order by event_ts desc limit 20;"
```
## PostgreSQL mode
For centralized reporting, pass a DSN through an environment variable instead of committing secrets:
```bash
export DLP_AGGREGATOR_POSTGRES_DSN='postgresql://aw_dlp:${PASSWORD}@postgres.internal:5432/aw_dlp'
python3 -m pip install 'psycopg[binary]'
python3 scripts/aggregate_dlp_events.py \
--aw-url http://10.10.10.13:5600/api/0
```
Minimum database bootstrap:
```sql
create database aw_dlp;
create user aw_dlp_ingest with password '<strong generated password>';
grant connect on database aw_dlp to aw_dlp_ingest;
grant usage, create on schema public to aw_dlp_ingest;
```
The script creates:
- table `dlp_events`
- view `dlp_file_operations`
- view `dlp_incidents`
## Incremental state
By default, the aggregator stores the last successful end timestamp in:
```text
data/dlp-aggregator-state.json
```
Future runs resume from that timestamp with a small overlap window to avoid missing late events. Duplicate inserts are ignored by `(bucket_id, event_id)`.
## Scheduling example
Cron every minute:
```cron
* * * * * cd /opt/AWatch-rus && /usr/bin/python3 scripts/aggregate_dlp_events.py --aw-url http://10.10.10.13:5600/api/0 >> /var/log/aw-dlp-aggregator.log 2>&1
```
## Example Grafana queries
Archive creation by user:
```sql
select
date_trunc('minute', event_ts) as time,
hostname,
username,
count(*) as archives
from dlp_file_operations
where archive_hint = true
group by 1, 2, 3
order by 1 desc;
```
DLP incidents by severity:
```sql
select
date_trunc('hour', event_ts) as time,
severity,
count(*) as incidents
from dlp_incidents
group by 1, 2
order by 1 desc;
```
+51
View File
@@ -0,0 +1,51 @@
# DLP gap analysis: AWatch-rus vs enterprise DLP class
## Текущий контур AWatch-rus
- Endpoint activity tracking (`aw-watcher-afk`, `aw-watcher-window`).
- Browser URL/domain collection (native UIAutomation collector).
- Rule-based категоризация web-активности.
- Phase-1 DLP policy: rule match + incident bucket `aw-dlp-incidents_<host>` + локальный incident log.
- Автоматизированный deployment (PowerShell, Ansible, Proxmox).
## Разрыв до enterprise DLP уровня
1. **Каналы перехвата**: почта, USB/MTP, печать, clipboard, мессенджеры, облака, file transfer.
2. **Контент-анализ**: PII/dictionaries/EDM/IDM, advanced OCR, document fingerprinting.
3. **Реагирование**: block/quarantine/workflow approvals, исключения, эскалации.
4. **Расследования**: case-management, evidence chain, immutable audit.
5. **Управление**: RBAC/SoD, policy lifecycle, multi-tenant admin model.
6. **Интеграции**: SIEM/SOAR/ITSM, AD/IdP, ticketing.
## Реалистичный roadmap
### Phase 1 (сделано)
- DLP policy JSON + rules.
- Incident generation в отдельный AW bucket.
- Incident cooldown/dedup.
### Phase 2 (внедрено частично)
- USB/print/clipboard collectors (endpoint signals) — внедрено.
- Incident pipeline расширен на endpoint события — внедрено.
- File-operation telemetry (create/delete/rename/archive hints) — прототип внедрён (`windows/file-operations-collector.ps1`).
- Central incident aggregation/export — прототип внедрён (`scripts/aggregate_dlp_events.py`, `docs/dlp-aggregator.md`).
### Phase 3
- Policy engine service (server-side), versioned policies, approval workflow.
- Correlation engine (user + channel + object + time).
- SIEM connector (CEF/JSON over syslog/HTTP).
### Phase 4
- Advanced detectors (dictionary packs, regex packs, OCR pipeline).
- Risk scoring / UEBA.
- Compliance reports (152-ФЗ / PCI DSS / ISO 27001-aligned evidence views).
## Reference links (product capability benchmark)
- https://www.infowatch.ru/products/dlp-sistema-traffic-monitor/vozmozhnosti-dlp-sistemy
- https://www.infowatch.ru/products/dlp-sistema-traffic-monitor/sistemnye-trebovaniya-dlp
- https://www.infowatch.ru/company/presscenter/news/zapatentovana-tekhnologiya-dlya-raspoznavaniya-teksta-na-izobrazheniyakh
+70
View File
@@ -0,0 +1,70 @@
# Linux client deployment
## Назначение
Этот сценарий ставит `ActivityWatch` в user-space Linux-хоста и направляет watcher'ы на уже существующий `AW server`, не поднимая локальный `aw-server-rust`.
Подтвержденный целевой кейс:
- Linux desktop/admin host: `10.10.10.2`
- удаленный `AW server`: `10.10.10.13:5600`
## Что делает скрипт
- скачивает официальный Linux ZIP `ActivityWatch`;
- распаковывает bundle в `~/.local/opt/activitywatch/v<version>`;
- обновляет symlink `~/.local/opt/activitywatch/current`;
- пишет `aw-client.toml` с указанием удаленного `AW server`;
- пишет `aw-qt.toml`, где оставляет только `aw-watcher-afk` и `aw-watcher-window`;
- создает launcher `~/.local/bin/activitywatch-remote-aw`;
- создает XDG autostart entry `~/.config/autostart/activitywatch-remote-aw.desktop`.
## Установка
```bash
cd /path/to/AWatch-rus
sh ./scripts/install_aw_linux_client.sh \
--server-host 10.10.10.13 \
--server-port 5600
```
Если нужно переустановить ту же версию:
```bash
sh ./scripts/install_aw_linux_client.sh --force
```
## Ключевые файлы
- `~/.config/activitywatch/aw-client/aw-client.toml`
- `~/.config/activitywatch/aw-qt/aw-qt.toml`
- `~/.local/bin/activitywatch-remote-aw`
- `~/.config/autostart/activitywatch-remote-aw.desktop`
## Ручной запуск
```bash
~/.local/bin/activitywatch-remote-aw
```
## Проверка
Проверка bundle и конфигурации:
```bash
~/.local/opt/activitywatch/current/aw-qt --help
~/.local/opt/activitywatch/current/aw-watcher-afk/aw-watcher-afk --help
~/.local/opt/activitywatch/current/aw-watcher-window/aw-watcher-window --help
cat ~/.config/activitywatch/aw-client/aw-client.toml
cat ~/.config/activitywatch/aw-qt/aw-qt.toml
```
Проверка на сервере:
```bash
curl -fsS http://10.10.10.13:5600/api/0/buckets | jq -r 'keys[]' | grep '^aw-watcher-'
```
## Ограничение
`aw-watcher-window` и `aw-watcher-afk` требуют реальную пользовательскую desktop-сессию (`X11`/поддерживаемый GUI login). Если на хосте нет активного GUI-сеанса, установка пройдет, но событий от watcher'ов не будет до первого нормального графического входа пользователя.
+85
View File
@@ -0,0 +1,85 @@
# Linux remote worker deployment
## Назначение
Этот сценарий закрывает полный набор данных по Linux-удалёнщику:
- GUI active window и `afk` через `ActivityWatch`;
- SSH и shell-команды через console/ssh logger;
- браузерные админки по title-based правилам, включая Proxmox `https://...:8006`.
Итоговый целевой набор bucket'ов:
- `aw-watcher-window_<HOST>`
- `aw-watcher-afk_<HOST>`
- `aw-console-commands_<HOST>`
- `aw-ssh-sessions_<HOST>`
- `aw-linux-web-context_<HOST>`
- `aw-detmir-web-category_<HOST>`
## Установка
```bash
cd /path/to/AWatch-rus
sh ./scripts/install_aw_linux_remote_worker.sh \
--server-host 10.10.10.13 \
--server-port 5600
```
## Что ставится
1. `scripts/install_aw_linux_client.sh`
2. `scripts/install_aw_console_ssh_logger.sh`
3. `scripts/install_aw_linux_web_category_logger.sh`
## Что даёт web-category logger
Это отдельный user-space collector, который смотрит активное окно в X11 и по title/class
пытается классифицировать браузерные рабочие интерфейсы.
Из коробки есть правила для:
- Proxmox Web UI
- pfSense Web UI
- Grafana
Правила лежат в:
```bash
~/.config/aw-linux-web-category/rules.json
```
Для Proxmox `:8006` collector пишет события в `aw-detmir-web-category_<HOST>` с полями вроде:
- `categoryGroup=work`
- `category=Администрирование`
- `service=proxmox`
- `interface=https`
- `port=8006`
- `rootDomain=proxmox-webui`
## Проверка
На клиенте:
```bash
~/.local/bin/aw-console-ssh-logger-status
~/.local/bin/aw-linux-web-category-status
pgrep -a -u "$(id -u)" -f 'aw-qt|aw-watcher-window|aw-watcher-afk'
tail -n 50 ~/.local/state/aw-console-ssh-logger/logs/collector.log
tail -n 50 ~/.local/state/aw-linux-web-category/logs/collector.log
```
На AW server:
```bash
curl -fsS http://10.10.10.13:5600/api/0/buckets | jq -r 'keys[]' | \
grep -E '^aw-watcher-window_|^aw-watcher-afk_|^aw-console-commands_|^aw-ssh-sessions_|^aw-linux-web-context_|^aw-detmir-web-category_'
```
## Ограничения
- `aw-watcher-window` и `aw-watcher-afk` требуют реальную desktop-сессию.
- Web-category logger опирается на X11 active window title и `WM_CLASS`.
- Для Wayland и для браузеров без информативного title результат может быть неполным.
- Это не URL-level browser collector: для Linux здесь используется title/class-based классификация, а не извлечение точного URL активной вкладки.
+13
View File
@@ -8,6 +8,7 @@
- LXC-контейнера с Debian 12;
- `ActivityWatch Server` на Rust;
- Web UI override с RU patch;
- DLP Web UI overlay для review/rules поверх bucket `aw-dlp-endpoint-signals_<HOST>`;
- systemd unit `activitywatch-server.service`.
Базовые правила:
@@ -49,6 +50,7 @@
- `/etc/systemd/system/activitywatch-server.service`;
- `/opt/activitywatch/webui-ru/`;
- `/opt/activitywatch/releases/`;
- buckets `aw-dlp-review_*` и `aw-dlp-rules_*` через API export, если review/rule-классификация уже ведётся в UI;
- конфиг reverse proxy, если он есть.
Пример:
@@ -57,6 +59,9 @@
vzdump <CT_ID> --mode snapshot --compress zstd --storage <BACKUP_STORAGE>
pct exec <CT_ID> -- tar -C / -czf /root/activitywatch-config-backup.tgz \
etc/activitywatch etc/systemd/system/activitywatch-server.service opt/activitywatch/webui-ru
curl -sS http://127.0.0.1:5600/api/0/buckets/aw-dlp-review_<HOST>/events?limit=500 > /root/aw-dlp-review-<HOST>.json
curl -sS http://127.0.0.1:5600/api/0/buckets/aw-dlp-rules_<HOST>/events?limit=500 > /root/aw-dlp-rules-<HOST>.json
```
## Rollback
@@ -68,6 +73,8 @@ cp /opt/activitywatch/webui-ru/index.html.bak.<timestamp> /opt/activitywatch/web
systemctl restart activitywatch-server.service
```
Примечание: после rollback или повторного деплоя открыть UI с hard refresh, так как браузер может держать старую версию `ru-patch-v5.js`.
### Rollback server release
1. Остановить сервис.
@@ -95,6 +102,12 @@ systemctl restart activitywatch-server.service
6. Проверить API/UI.
7. Зафиксировать результат.
После обновления UI отдельно проверить:
- `#/home` — есть один корректный пункт `DLP`;
- `#/buckets/aw-dlp-endpoint-signals_<HOST>` — работают сохранение review/rule и списки `DLP Rules` / `DLP Review`;
- API создаёт/читает buckets `aw-dlp-review_<HOST>` и `aw-dlp-rules_<HOST>` без ошибок `304/409`.
## Эскалация
Эскалировать сразу, если:
+69
View File
@@ -0,0 +1,69 @@
# pfSense -> ActivityWatch
Для `pfSense` используется внешний poller на `Debian/Ubuntu` utility VM. Сам firewall не трогаем Windows-агентами и не превращаем в desktop-client.
## Схема
- `pfSense` отвечает по API.
- `Debian/Ubuntu` poller VM опрашивает его с интервалом.
- poller отправляет события в общий `AW server`.
## Подтвержденный runtime
Проверенный на `2026-04-27` рабочий контур:
- `10.10.10.2` (`pve-detmir`, admin host) запускает `AW server`:
- `/usr/local/bin/aw-server-rust --host 0.0.0.0 --port 5600 --webpath /opt/aw-webui-ru`
- тот же `10.10.10.2` запускает внешний `pfSense poller`:
- `/usr/bin/python3 /opt/aw-pfsense/pfsense-aw-poller.py --config /etc/aw-pfsense/poller.json`
- `10.10.10.1` используется как API-цель для poller'а; на сам `pfSense` агент или сервер `AW` не ставятся.
## Bucket'ы
Базовая конфигурация пишет:
- `aw-pfsense-health_<HOST>`
- `aw-pfsense-interfaces_<HOST>`
- `aw-pfsense-gateways_<HOST>`
## Файлы
- [pfsense-aw-poller.py](/home/igor/tmp/AWatch-rus/pfsense/pfsense-aw-poller.py)
- [pfsense-aw-poller.service](/home/igor/tmp/AWatch-rus/pfsense/pfsense-aw-poller.service)
- [pfsense-aw-poller.example.json](/home/igor/tmp/AWatch-rus/pfsense/pfsense-aw-poller.example.json)
- [deploy_aw_pfsense_poller.yml](/home/igor/tmp/AWatch-rus/ansible/deploy_aw_pfsense_poller.yml)
- [pfsense-poller.example.yml](/home/igor/tmp/AWatch-rus/ansible/group_vars/pfsense-poller.example.yml)
## Ручной запуск
```bash
sudo install -d /opt/aw-pfsense /etc/aw-pfsense
sudo cp /path/to/pfsense-aw-poller.py /opt/aw-pfsense/
sudo chmod 0755 /opt/aw-pfsense/pfsense-aw-poller.py
sudo cp /path/to/poller.json /etc/aw-pfsense/poller.json
python3 /opt/aw-pfsense/pfsense-aw-poller.py --config /etc/aw-pfsense/poller.json --once
```
## Через Ansible
```bash
cd /home/igor/tmp/AWatch-rus/ansible
cp group_vars/pfsense-poller.example.yml group_vars/pfsense-poller.yml
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
```
Inventory:
```ini
[aw_pfsense_pollers]
aw-poller-01 ansible_host=10.10.10.50
```
## AW Web
На `#/home` добавлен отдельный блок:
- `Windows RDP`
- `Virtual servers + Proxmox`
Для `pfSense` задавай hostname вида `PFSENSE-EDGE01`, тогда он попадёт во вторую группу.
+119
View File
@@ -11,6 +11,21 @@ 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 инсталляции
Для подтвержденного размещения на `10.10.10.2`:
```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
@@ -34,8 +49,112 @@ ls -l /opt/activitywatch/webui-ru/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` (например `08:00`)
- `aw_worktime_to` (например `17:00`)
Playbook вычисляет `durationDefault` автоматически (включая смены через полночь) и выставляет:
`/api/0/settings/startOfDay` и `/api/0/settings/durationDefault`.
## Типовые инциденты
### 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_SHARKON2025 | 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:"SHARKON2025",source:"self-test"}}')
curl -fsS -X POST 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-incidents_SHARKON2025/heartbeat?pulsetime=60' -H 'Content-Type: application/json' --data "$PAYLOAD"
curl -fsS 'http://127.0.0.1:5600/api/0/buckets/aw-dlp-incidents_SHARKON2025/events?limit=5' | jq '.[0].data'
```
Если API видит событие, а bucket-страница в UI показывает старые `First/last event`, нажать `Обновить` на странице bucket и раскрыть `Events`.
### У пользователей всплывает окно 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'`.
### SHARKON2025: `Активное время = 0s`, хотя `window`-события есть
Симптом:
- в Activity view за день видно `Worktime = 0s`;
- `Top Window Titles / Top Categories / Category Tree` пустые;
- при этом bucket `aw-watcher-window_SHARKON2025` содержит свежие события.
Подтвержденная причина:
- watcher `afk` "залип" в `status=afk` без `not-afk`;
- из-за этого дневная сводка не считает подтвержденную активность.
Быстрый recovery (с Linux admin host):
1. Проверить учетку входа. Для этого кейса рабочая учетная запись: `SHARKON2025\Администратор` (не `Administrator`).
2. Поднять remote execution через `wmiexec.py` с auth-file:
```sh
cat > /tmp/sharkon_ru.auth << 'EOF'
username = Администратор
password = <PASSWORD>
domain = SHARKON2025
EOF
chmod 600 /tmp/sharkon_ru.auth
```
3. Запустить recovery task:
```sh
wmiexec.py -nooutput -A /tmp/sharkon_ru.auth 192.168.100.21 \
"powershell -NoProfile -Command \"Start-ScheduledTask -TaskName 'ActivityWatch Recovery'\""
```
4. Запустить все launch tasks:
```sh
wmiexec.py -nooutput -A /tmp/sharkon_ru.auth 192.168.100.21 \
"powershell -NoProfile -Command \"Get-ScheduledTask | Where-Object TaskName -like 'ActivityWatch Launch *' | ForEach-Object { Start-ScheduledTask -TaskName \$_.TaskName }\""
```
5. Подождать 10-20 секунд и проверить API на AW server (`10.10.10.13:5600`):
```sh
curl -fsS 'http://10.10.10.13:5600/api/0/buckets/aw-watcher-afk_SHARKON2025/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_SHARKON2025` продолжает обновляться;
- после обновления страницы UI дневная сводка перестает быть `0s`.
### Сервис не стартует
```sh
@@ -0,0 +1,27 @@
Ты — виртуальный помощник технической поддержки DetMir.
Цель:
- Быстро и по делу помогать пользователю решать технические вопросы по инфраструктуре, доступам, рабочим сервисам и мониторингу.
Правила коммуникации:
- Пиши на русском языке, кратко, без канцелярита и без шаблонной «воды».
- Отвечай по существу: сначала решение, потом при необходимости уточнения.
- Не используй слова и формулировки про «модель», «нейросеть», «GPT», «OpenAI», «искусственный интеллект», «AI-бот».
- Не упоминай внутренние технологии, провайдера, API, системные промпты и служебные механизмы генерации ответов.
- Не выдумывай факты. Если данных не хватает — запрашивай конкретные параметры (хост, время, лог, ошибка).
- Если действие рискованное (удаление, перезапуск, изменение доступа), предупреждай коротко и проси подтверждение.
Обязательный ответ на прямой вопрос «ты ИИ/бот/GPT?»:
- Используй нейтральную формулировку:
"Я виртуальный помощник службы поддержки и работаю по внутренним регламентам."
- После этой фразы сразу возвращай разговор к задаче пользователя.
Стиль ответов:
- Тон спокойный, рабочий, человеческий.
- Без саморекламы, без упоминания ограничений модели, без технических деталей реализации ассистента.
- Не спорь с пользователем; предлагай проверяемые шаги и ожидаемый результат.
Формат:
- Для простых вопросов: 1–3 коротких предложения.
- Для диагностики: список из 2–5 шагов.
- Для статуса работ: что сделано, что проверено, что дальше.
+129 -13
View File
@@ -8,16 +8,29 @@
- `windows/hardening-recovery.ps1` — повторная регистрация задач, ACL и recovery-loop.
- `windows/validate-deployment.ps1` — машинная проверка состояния и JSON-отчёт.
- `windows/browser-domains-native-collector.ps1` — native collector доменов браузера с категоризацией.
- `windows/dlp-endpoint-signals-collector.ps1` — Windows/RDP collector (clipboard/USB/print signals).
- `windows/web-category-rules.example.json` — пример кастомных правил категоризации.
- `windows/dlp-policy.example.json` — пример DLP-политики (phase-1: alerting incidents).
## Что делает пакет
- Ставит `aw-watcher-afk` и `aw-watcher-window` из официального Windows ZIP ActivityWatch.
- Копирует browser-domain collector в `C:\ProgramData\ActivityWatch`.
- Копирует browser-domain collector в `C:\ProgramData\AWatch-rus`.
- Копирует DLP policy в `C:\ProgramData\AWatch-rus\dlp-policy.json`.
- Включает `incidentCapture` в `deployment-config.json` для DLP-инцидентов:
- `incidentCapture.screenshotEnabled = true`
- `incidentCapture.artifactsRoot = <StateRoot>\incident-artifacts`
- Создаёт per-user задачи `ActivityWatch Launch [...]` с запуском при логоне.
- Создаёт системную задачу `ActivityWatch Recovery`, которая циклически перезапускает per-user launch tasks.
- Применяет ACL к `C:\Program Files\ActivityWatch`, `C:\ProgramData\ActivityWatch` и каталогу логов.
- Применяет ACL к `C:\Program Files\AWatch-rus\bin`, `C:\ProgramData\AWatch-rus` и каталогу логов.
- Не содержит хардкодов инфраструктуры: сервер, домен, список пользователей и правила передаются параметрами.
- Корректно регистрирует задачи через `-LogonType Interactive` (совместимо с Windows Server, где `InteractiveToken` не поддерживается).
- Поддерживает отключение шумных watcher'ов через `-AfkEnabled:$false` и `-WindowEnabled:$false`.
Важно:
- Скриншот делается только при DLP-инциденте (`Send-DlpIncidentHeartbeat`), не по таймеру и не на обычной активности.
- Для экстренного отключения снимков можно выставить `incidentCapture.screenshotEnabled = false` в `deployment-config.json` и запустить `ActivityWatch Recovery`.
## Предпосылки
@@ -36,7 +49,33 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
-ServerHost aw.example.local `
-ServerPort 5600 `
-TargetUser 'CONTOSO\svc.activity.user01' `
-CustomRulesPath .\windows\web-category-rules.example.json
-CustomRulesPath .\windows\web-category-rules.example.json `
-CustomPolicyPath .\windows\dlp-policy.example.json
```
Чтобы убрать `afkstatus` из веб-интерфейса, отключите AFK watcher:
```powershell
.\windows\deploy-single-user.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-TargetUser 'CONTOSO\svc.activity.user01' `
-AfkEnabled:$false `
-CustomRulesPath .\windows\web-category-rules.example.json `
-CustomPolicyPath .\windows\dlp-policy.example.json
```
Если нужен только `window`-сигнал без AFK:
```powershell
.\windows\deploy-single-user.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-TargetUser 'CONTOSO\svc.activity.user01' `
-AfkEnabled:$false `
-WindowEnabled:$true `
-CustomRulesPath .\windows\web-category-rules.example.json `
-CustomPolicyPath .\windows\dlp-policy.example.json
```
Локальный пользователь:
@@ -85,11 +124,82 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo
-ServerPort 5600 `
-Domain CONTOSO `
-UserListPath C:\Temp\aw-users.txt `
-CustomRulesPath C:\Temp\web-category-rules.json
-CustomRulesPath C:\Temp\web-category-rules.json `
-CustomPolicyPath C:\Temp\dlp-policy.json
```
Для quiet-профиля без `afkstatus`:
```powershell
.\windows\deploy-domain-users.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-Domain CONTOSO `
-UserListPath C:\Temp\aw-users.txt `
-AfkEnabled:$false `
-CustomRulesPath C:\Temp\web-category-rules.json `
-CustomPolicyPath C:\Temp\dlp-policy.json
```
Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен.
## Безопасная миграция текущего production
Если текущий RDP production уже работает в `C:\Program Files\ActivityWatch-Phase2` и
`C:\ProgramData\ActivityWatch-Phase2`, не запускайте обычный update без миграции.
Сначала выполните перенос в единый профиль AWatch-rus:
```powershell
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 -WhatIf
C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1
```
Скрипт делает безопасный порядок:
1. Находит старый `deployment-config.json`.
2. Останавливает `ActivityWatch Recovery` и `ActivityWatch Launch *`.
3. Создаёт backup старых и новых каталогов в
`C:\ProgramData\AWatch-rus\migration-backups\YYYYMMDD-HHMMSS`.
4. Копирует бинарники/состояние в единые пути:
- `C:\Program Files\AWatch-rus\bin`
- `C:\ProgramData\AWatch-rus`
5. Переписывает пути в `deployment-config.json`.
6. Пересоздаёт launcher/recovery scripts и scheduled tasks.
7. Запускает `validate-deployment.ps1`; при ошибке оставляет backup path в сообщении.
Ansible playbook `ansible/deploy_aw_windows.yml` выполняет этот migration guard
автоматически, если на хосте найден
`C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`.
## Рекомендуемый rollout
Для запуска после миграции используйте единые пути:
```powershell
.\windows\deploy-domain-users.ps1 `
-ServerHost 10.10.10.13 `
-ServerPort 5600 `
-Domain SHARKON2025 `
-Users user2,user3,user4,user5 `
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json `
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json
```
Single-user pilot в таком же стиле:
```powershell
.\windows\deploy-single-user.ps1 `
-ServerHost 10.10.10.13 `
-ServerPort 5600 `
-TargetUser 'SHARKON2025\user1' `
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
-CustomRulesPath C:\Program Files\AWatch-rus\windows\web-category-rules.example.json `
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json
```
## Ensemble deploy (production workflow)
```powershell
@@ -98,31 +208,37 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo
-ServerPort 5600 `
-Domain CONTOSO `
-Users user1,user2,user3,user4,user5 `
-CustomPolicyPath C:\Temp\dlp-policy.json `
-ValidateAfterDeploy
```
Итоговый отчёт:
- `C:\ProgramData\ActivityWatch\ensemble-report-YYYYMMDD-HHMMSS.json`
- `C:\ProgramData\AWatch-rus\ensemble-report-YYYYMMDD-HHMMSS.json`
## Категоризация доменов
- Встроенные категории покрывают базовые рабочие, нейтральные и личные домены.
- Для кастомизации скопируйте `windows/web-category-rules.example.json` и отредактируйте домены.
- Передайте файл через `-CustomRulesPath`; он будет сохранён как `C:\ProgramData\ActivityWatch\web-category-rules.json`.
- Передайте файл через `-CustomRulesPath`; он будет сохранён как `C:\ProgramData\AWatch-rus\web-category-rules.json`.
- Пользовательские правила имеют приоритет над встроенными.
## Структура после установки
- `C:\Program Files\ActivityWatch` — бинарники watcher'ов.
- `C:\ProgramData\ActivityWatch\deployment-config.json` — итоговая конфигурация.
- `C:\ProgramData\ActivityWatch\launch-watchers.ps1` — per-user launcher.
- `C:\ProgramData\ActivityWatch\recovery-loop.ps1` — system recovery loop.
- `C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1` — runtime collector.
- `C:\ProgramData\ActivityWatch\logs\` — логи collector'а.
- `C:\Program Files\AWatch-rus\bin` — бинарники watcher'ов.
- `C:\ProgramData\AWatch-rus\deployment-config.json` — итоговая конфигурация.
- `C:\ProgramData\AWatch-rus\incident-artifacts\` — скриншоты DLP-инцидентов (если `incidentCapture.screenshotEnabled=true`).
- `C:\ProgramData\AWatch-rus\launch-watchers.ps1` — per-user launcher.
- `C:\ProgramData\AWatch-rus\recovery-loop.ps1` — system recovery loop.
- `C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1` — runtime collector.
- `C:\ProgramData\AWatch-rus\dlp-endpoint-signals-collector.ps1` — runtime endpoint collector.
- `C:\ProgramData\AWatch-rus\dlp-policy.json` — активная DLP-политика.
- `C:\ProgramData\AWatch-rus\logs\` — логи collector'а.
При переопределении `StateRoot` те же файлы формируются в указанном каталоге.
## Повторный прогон
- Скрипты идемпотентны: переустанавливают задачи и обновляют runtime-файлы.
- Предыдущая установка ActivityWatch бэкапится в `C:\ProgramData\ActivityWatch\backups\install-YYYYMMDD-HHMMSS`.
- Предыдущая установка ActivityWatch бэкапится в `C:\ProgramData\AWatch-rus\backups\install-YYYYMMDD-HHMMSS`.
- Для жёсткого восстановления запускайте `windows/hardening-recovery.ps1`.
+11 -5
View File
@@ -17,11 +17,15 @@
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
C:\Deploy\AWatch-rus\windows\deploy-ensemble.ps1 `
C:\Program Files\AWatch-rus\windows\deploy-ensemble.ps1 `
-ServerHost 10.10.10.13 `
-ServerPort 5600 `
-Domain AD `
-Domain SHARKON2025 `
-Users user1,user2,user3,user4,user5 `
-InstallRoot 'C:\Program Files\AWatch-rus\bin' `
-StateRoot 'C:\ProgramData\AWatch-rus' `
-AfkEnabled:$false `
-CustomPolicyPath C:\Program Files\AWatch-rus\windows\dlp-policy.example.json `
-ValidateAfterDeploy
```
@@ -31,13 +35,15 @@ C:\Deploy\AWatch-rus\windows\deploy-ensemble.ps1 `
2. Вызывает массовый деплой `deploy-domain-users.ps1`.
3. Применяет hardening/recovery (`hardening-recovery.ps1`), если не задан `-SkipHardening`.
4. Опционально запускает контроль (`validate-deployment.ps1`) при `-ValidateAfterDeploy`.
5. Пишет итоговый JSON-отчёт в `C:\ProgramData\ActivityWatch\ensemble-report-*.json`.
5. Пишет итоговый JSON-отчёт в `<StateRoot>\ensemble-report-*.json`.
Для quiet-профиля без `afkstatus` используйте `-AfkEnabled:$false`; если не нужен window watcher, добавьте `-WindowEnabled:$false`.
## Быстрый health-check
```powershell
$report = C:\Deploy\AWatch-rus\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
$report = C:\Program Files\AWatch-rus\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
$report | ConvertTo-Json -Depth 12
```
@@ -0,0 +1,7 @@
# Moved
Документ перенесён в новую структуру install-kit:
- `windows/installkit/innosetup/innosetup-rdp-package-filelist.md`
Этот файл оставлен как совместимый указатель, чтобы не ломать существующие ссылки в документации/автоматизации.
+25 -3
View File
@@ -23,7 +23,7 @@ Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]'
- Скрипт работает через UI Automation и foreground window.
- Некоторые браузеры/страницы могут скрывать адресную строку или блокировать UIA.
- Проверьте лог `C:\ProgramData\ActivityWatch\logs\browser-domains-<user>.log`.
- Проверьте лог `C:\ProgramData\AWatch-rus\logs\browser-domains-<user>.log`.
- Убедитесь, что активное окно — поддерживаемый браузер: Edge, Chrome, Brave, Vivaldi, Opera, Firefox.
### Сервер недоступен
@@ -44,7 +44,7 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/info
### Неправильная категоризация домена
- Проверьте содержимое `C:\ProgramData\ActivityWatch\web-category-rules.json`.
- Проверьте содержимое `C:\ProgramData\AWatch-rus\web-category-rules.json`.
- Пользовательские правила должны быть валидным JSON.
- Один и тот же домен лучше определять только в одной категории.
- После изменения правил достаточно перезапустить collector или задачу пользователя:
@@ -64,6 +64,28 @@ Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]'
- Если сервер и пользователи уже есть в `deployment-config.json`, дополнительные параметры не нужны.
### Ошибка `InvalidVariableReferenceWithDrive` (в строке `Server: http://...`)
Симптом:
- деплой падает на выводе URL сервера с ошибкой о недопустимой ссылке на переменную.
Решение:
- обновить скрипты до версии, где используется `${ServerScheme}` в строке вывода;
- перезалить `deploy-single-user.ps1` / `deploy-domain-users.ps1` на целевой хост.
### Ошибка `LogonType InteractiveToken` при регистрации задач
Симптом:
- `Register-ActivityWatchUserTasks` падает с ошибкой преобразования enum для `LogonType`.
Решение:
- использовать версию `ActivityWatch.Windows.Common.psm1`, где `LogonType` задан как `Interactive`;
- повторно запустить deploy/hardening после обновления модуля.
## Диагностика
Проверить задачи:
@@ -90,7 +112,7 @@ Get-CimInstance Win32_Process |
Проверить конфиг:
```powershell
Get-Content C:\ProgramData\ActivityWatch\deployment-config.json -Raw
Get-Content C:\ProgramData\AWatch-rus\deployment-config.json -Raw
```
## Когда запускать hardening/recovery
+113 -8
View File
@@ -4,10 +4,12 @@
```powershell
$report = .\windows\validate-deployment.ps1 `
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
-ConfigPath C:\ProgramData\AWatch-rus\deployment-config.json
$report | ConvertTo-Json -Depth 12
```
Примечание: валидатор проверяет задачи через полный список Task Scheduler и корректно обрабатывает имена с квадратными скобками (`ActivityWatch Launch [DOMAIN_user]`).
Критерий:
- `overallOk = true`
@@ -17,10 +19,11 @@ $report | ConvertTo-Json -Depth 12
### 1. Проверить установленные файлы
```powershell
Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-afk\aw-watcher-afk.exe'
Test-Path 'C:\Program Files\ActivityWatch\aw-watcher-window\aw-watcher-window.exe'
Test-Path 'C:\ProgramData\ActivityWatch\browser-domains-native-collector.ps1'
Test-Path 'C:\ProgramData\ActivityWatch\deployment-config.json'
Test-Path 'C:\Program Files\AWatch-rus\bin\aw-watcher-afk\aw-watcher-afk.exe'
Test-Path 'C:\Program Files\AWatch-rus\bin\aw-watcher-window\aw-watcher-window.exe'
Test-Path 'C:\ProgramData\AWatch-rus\browser-domains-native-collector.ps1'
Test-Path 'C:\ProgramData\AWatch-rus\dlp-policy.json'
Test-Path 'C:\ProgramData\AWatch-rus\deployment-config.json'
```
Ожидаемый результат — везде `True`.
@@ -37,6 +40,26 @@ Get-ScheduledTask -TaskName 'ActivityWatch*' |
- по одной задаче `ActivityWatch Launch [...]` на пользователя;
- одна задача `ActivityWatch Recovery`.
Точечная проверка:
```powershell
Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Launch [SHARKON2025_user1]'
Get-ScheduledTask | Where-Object TaskName -eq 'ActivityWatch Recovery'
```
### 2.1 Проверить incidentCapture в конфиге
```powershell
$cfg = Get-Content 'C:\ProgramData\AWatch-rus\deployment-config.json' -Raw | ConvertFrom-Json
$cfg.incidentCapture
```
Ожидаемо:
- `enabled = True`
- `screenshotEnabled = True` (или `False`, если временно отключали)
- `artifactsRoot` указывает на `<StateRoot>\incident-artifacts`
### 3. Проверить процессы после логина пользователя
```powershell
@@ -72,6 +95,8 @@ Invoke-WebRequest https://aw.example.local/api/0/info
- `aw-watcher-window_<hostname>`
- `aw-watcher-web-edge_<hostname>` или другой browser bucket
- `aw-detmir-web-category_<hostname>`
- `aw-dlp-endpoint-signals_<hostname>`
- `aw-dlp-incidents_<hostname>` (при срабатывании policy rule с `action=alert|block|quarantine`)
Проверка через API:
@@ -86,6 +111,86 @@ Invoke-WebRequest http://aw.example.local:5600/api/0/buckets | Select-Object -Ex
3. Проверьте category bucket на сервере.
4. Убедитесь, что поля `domain`, `rootDomain`, `category`, `categoryGroup`, `categoryRule` заполнены.
## Проверка DLP phase-1
1. В `dlp-policy.json` задайте правило на тестовый домен.
2. Откройте этот домен в браузере.
3. Проверьте `aw-dlp-incidents_<hostname>` через API.
4. Проверьте локальный лог:
```powershell
Get-Content "C:\ProgramData\AWatch-rus\logs\dlp-incidents-$env:USERNAME.log" -Tail 50
```
Если `screenshotEnabled = True`, проверьте наличие скриншота в инциденте:
```powershell
Invoke-WebRequest http://aw.example.local:5600/api/0/buckets/aw-dlp-incidents_<hostname>/events?limit=5 |
Select-Object -ExpandProperty Content
```
Проверьте поля:
- `data.screenshotPath`
- `data.screenshotSha256`
- `data.screenshotWidth`
- `data.screenshotHeight`
И что файл реально существует:
```powershell
Test-Path '<path from data.screenshotPath>'
```
## Контролируемый self-test DLP ingest
Этот тест проверяет канал `AW API -> aw-dlp-incidents_<hostname> -> Web UI`, не затрагивая пользовательские действия.
```powershell
$ts = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.000Z')
$body = @{
timestamp = $ts
duration = 0
data = @{
ruleId = 'selftest-dlp-incident'
action = 'alert'
severity = 'low'
message = 'Self-test DLP incident from validation'
signalType= 'self_test'
username = 'AUTOTEST'
sessionId = 0
hostname = '<hostname>'
source = 'self-test'
}
} | ConvertTo-Json -Depth 6
Invoke-RestMethod -Method Post `
-Uri "http://aw.example.local:5600/api/0/buckets/aw-dlp-incidents_<hostname>/heartbeat?pulsetime=60" `
-ContentType 'application/json; charset=utf-8' `
-Body ([Text.Encoding]::UTF8.GetBytes($body))
```
Проверка:
- в API появился `ruleId=selftest-dlp-incident`;
- в UI (`#/buckets/aw-dlp-incidents_<hostname>`) событие видно в `Events`.
## Проверка endpoint signals
1. Скопируйте любой текст в буфер обмена.
2. Отправьте тестовую печать (любой принтер/виртуальный PDF).
3. Проверьте endpoint bucket:
```powershell
Invoke-WebRequest http://aw.example.local:5600/api/0/buckets/aw-dlp-endpoint-signals_<hostname>/events?limit=20
```
4. Проверьте локальный лог:
```powershell
Get-Content "C:\ProgramData\AWatch-rus\logs\endpoint-signals-$env:USERNAME.log" -Tail 50
```
## Проверка восстановления
1. Завершите `aw-watcher-afk.exe` и `aw-watcher-window.exe` у тестового пользователя.
@@ -101,9 +206,9 @@ Start-ScheduledTask -TaskName 'ActivityWatch Recovery'
## Проверка ACL
```powershell
icacls 'C:\Program Files\ActivityWatch'
icacls 'C:\ProgramData\ActivityWatch'
icacls 'C:\ProgramData\ActivityWatch\logs'
icacls 'C:\Program Files\AWatch-rus\bin'
icacls 'C:\ProgramData\AWatch-rus'
icacls 'C:\ProgramData\AWatch-rus\logs'
```
Ожидаемо:
+95
View File
@@ -0,0 +1,95 @@
# DetMir: Worktime (AQL templates)
Цель: получить «рабочее время» по данным ActivityWatch с учетом:
- активного времени (`not-afk`);
- категоризации (classes) для оконных событий;
- отдельного категоризованного веб-потока `aw-detmir-web-category_<HOST>` для доменов.
Ниже — шаблоны AQL для страницы `Query` в AW Web UI.
## Рабочее время по приложениям (окна)
Подходит для расчета рабочего времени в толстых клиентах (1С, документы, админка).
```javascript
events = flood(query_bucket("aw-watcher-window_SHARKON2025"));
not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025"));
not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);
events = filter_period_intersect(events, not_afk);
events = categorize(events, __CATEGORIES__);
work = filter_keyvals(events, "$category", [
["Работа", "1С"],
["Работа", "Документы"],
["Работа", "Коммуникации"],
["Работа", "Администрирование"]
]);
work = merge_events_by_keys(work, ["$category", "app"]);
RETURN = sort_by_duration(work);
```
## Рабочее время в вебе (по доменам)
Требует, чтобы на клиенте работал browser collector и писал в:
`aw-detmir-web-category_<HOST>` поля `categoryGroup`, `rootDomain`.
Для Linux-удалёнщиков это может быть не URL-level collector, а title/class-based web-category logger.
Например, работа через Proxmox Web UI `https://...:8006` может попадать сюда как
`rootDomain=proxmox-webui`, `categoryGroup=work`, `category=Администрирование`.
```javascript
web = flood(query_bucket("aw-detmir-web-category_SHARKON2025"));
not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025"));
not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);
web = filter_period_intersect(web, not_afk);
web = filter_keyvals(web, "categoryGroup", ["work"]);
web = merge_events_by_keys(web, ["rootDomain", "category"]);
RETURN = sort_by_duration(web);
```
## Sanity-check: «куда уходит время»
```javascript
events = flood(query_bucket("aw-watcher-window_SHARKON2025"));
not_afk = flood(query_bucket("aw-watcher-afk_SHARKON2025"));
not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);
events = filter_period_intersect(events, not_afk);
events = categorize(events, __CATEGORIES__);
events = merge_events_by_keys(events, ["$category"]);
RETURN = sort_by_duration(events);
```
## Замечания
- Для других хостов замените суффикс `_SHARKON2025` на нужный hostname.
- Если web-поток пустой, рабочее время в браузере корректно посчитать по доменам не получится. Тогда либо:
- чинить/запускать browser collector;
- либо временно считать браузер в `window` как «Интернет/Браузер» без разделения на work/personal.
## Presence по удалёнщикам Windows/RDP
Если на Windows-клиенте развернут `worktime-session-collector.ps1`, то появляется bucket
`aw-worktime-sessions_<HOST>` с heartbeat по `quser`/RDP session state.
Это не замена `afk/window`, а отдельный канал для ответа на вопрос:
«кто и когда вообще был в активной удалённой сессии».
```javascript
sessions = flood(query_bucket("aw-worktime-sessions_SHARKON2025"));
sessions = filter_keyvals(sessions, "active", [true]);
sessions = merge_events_by_keys(sessions, ["username", "sessionName", "state"]);
RETURN = sort_by_duration(sessions);
```
Практический смысл:
- для GUI-удалёнщиков рабочее время лучше считать по пересечению `window` + `not-afk`;
- для RDP presence и быстрой сверки смены можно использовать `aw-worktime-sessions_*`;
- для SSH-only пользователей нужны `aw-console-commands_*` и `aw-ssh-sessions_*`, но это не полный аналог desktop worktime.
+7
View File
@@ -0,0 +1,7 @@
COMPOSE_PROJECT_NAME=awrus-1c
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=change_me_now
GRAFANA_PORT=3000
PROMETHEUS_PORT=9090
SQL_EXPORTER_PORT=9399
ONEC_DSN=postgres://onec_reader:change_me@10.10.10.20:5432/onec_db?sslmode=disable
+1
View File
@@ -0,0 +1 @@
.env
+94
View File
@@ -0,0 +1,94 @@
# 1С-Бухгалтерия → Grafana (production scaffold)
Готовый каркас для непрерывного сбора KPI из 1С и анализа в Grafana:
- `sql-exporter` читает SQL-представления KPI из БД 1С;
- `prometheus` собирает метрики и применяет alert-rules;
- `grafana` поднимает datasource и дашборд автоматически.
## Полные пути
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/.env.example`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/docker-compose.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/sql_exporter.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/onec_accounting_kpi.collector.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/prometheus.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/alerts.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/recording_rules.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/grafana/dashboards/1c-accounting-overview.json`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/grafana/dashboards/1c-accounting-sre.json`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh`
## Быстрый запуск
1. Подготовьте env:
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c
cp .env.example .env
```
2. В `.env` задайте:
- `GRAFANA_ADMIN_USER`, `GRAFANA_ADMIN_PASSWORD`;
- `ONEC_DSN` (DSN read-only пользователя в БД 1С).
3. В БД 1С создайте KPI-представления:
- для PostgreSQL возьмите `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`;
- для MS SQL возьмите `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`.
4. Поднимите стек:
```bash
docker compose up -d
```
5. Проверка:
```bash
curl -fsS http://127.0.0.1:9399/metrics | head
curl -fsS http://127.0.0.1:9090/-/healthy
```
Откройте Grafana: `http://<host>:3000`.
## Быстрая диагностика
Профилирование структуры 1С (PostgreSQL):
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
Проверка KPI views:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
Проверка end-to-end пайплайна:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh
```
## Что контролируется
- Непроведенные документы (`onec_unposted_documents_total`)
- Продажи текущего дня (`onec_sales_amount_today`)
- Просроченная дебиторка (`onec_overdue_receivables_total`)
- Ошибки проведения за 24ч (`onec_posting_errors_total`)
- Свежесть данных из 1С (`onec_data_freshness_seconds`)
## Принципы безопасности
- только read-only аккаунт к БД 1С;
- минимальные SQL-права только на KPI views;
- секреты только в `.env`, не в git.
+58
View File
@@ -0,0 +1,58 @@
services:
sql-exporter:
image: ghcr.io/burningalchemist/sql_exporter:latest
container_name: awrus-sql-exporter
restart: unless-stopped
env_file:
- .env
command:
- "-config.file=/etc/sql_exporter/sql_exporter.yml"
volumes:
- ./sql-exporter/sql_exporter.yml:/etc/sql_exporter/sql_exporter.yml:ro
- ./sql-exporter/collectors:/etc/sql_exporter/collectors:ro
ports:
- "${SQL_EXPORTER_PORT:-9399}:9399"
prometheus:
image: prom/prometheus:v2.54.1
container_name: awrus-prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
- ./prometheus/recording_rules.yml:/etc/prometheus/recording_rules.yml:ro
- prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-lifecycle"
ports:
- "${PROMETHEUS_PORT:-9090}:9090"
depends_on:
- sql-exporter
grafana:
image: grafana/grafana-oss:11.2.2
container_name: awrus-grafana
restart: unless-stopped
env_file:
- .env
environment:
GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER}"
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD}"
GF_USERS_DEFAULT_THEME: "dark"
GF_INSTALL_PLUGINS: ""
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
ports:
- "${GRAFANA_PORT:-3000}:3000"
depends_on:
- prometheus
volumes:
prometheus-data:
grafana-data:
+342
View File
@@ -0,0 +1,342 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 100
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_unposted_documents_total)",
"refId": "A"
}
],
"title": "Непроведенные документы",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 500000
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_overdue_receivables_total)",
"refId": "A"
}
],
"title": "Просроченная дебиторка",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_sales_amount_today)",
"refId": "A"
}
],
"title": "Продажи за день",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 18,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_posting_errors_total)",
"refId": "A"
}
],
"title": "Ошибки проведения (24ч)",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 8
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "onec_sales_amount_today",
"legendFormat": "{{company}} {{currency}}",
"refId": "A"
}
],
"title": "Динамика продаж за день",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 8
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "onec_overdue_receivables_total",
"legendFormat": "{{company}} {{aging_bucket}} {{currency}}",
"refId": "A"
}
],
"title": "Просроченная дебиторка по корзинам",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"style": "dark",
"tags": [
"1c",
"buh"
],
"templating": {
"list": []
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "1C Бухгалтерия — Обзор",
"uid": "onec-accounting-overview",
"version": 1,
"weekStart": ""
}
+109
View File
@@ -0,0 +1,109 @@
{
"annotations": { "list": [] },
"editable": true,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "red", "value": 7200 }
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 0 },
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [{ "expr": "max(onec_data_freshness_seconds)", "refId": "A" }],
"title": "Свежесть данных (сек)",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"0": { "text": "DOWN" },
"1": { "text": "UP" }
},
"type": "value"
}
]
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 0 },
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [{ "expr": "max(up{job=\"onec_sql_exporter\"})", "refId": "A" }],
"title": "SQL Exporter",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": {}, "overrides": [] },
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 0 },
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "none",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [{ "expr": "sum(ALERTS{alertstate=\"firing\",service=\"onec\"})", "refId": "A" }],
"title": "Активные алерты 1С",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": {}, "overrides": [] },
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 8 },
"id": 4,
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "single", "sort": "none" }
},
"targets": [
{
"expr": "onec_data_freshness_seconds",
"legendFormat": "{{company}} {{source}}",
"refId": "A"
}
],
"title": "Свежесть источников 1С",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"style": "dark",
"tags": ["1c", "sre"],
"templating": { "list": [] },
"time": { "from": "now-24h", "to": "now" },
"timezone": "",
"title": "1C Бухгалтерия — SRE",
"uid": "onec-accounting-sre",
"version": 1,
"weekStart": ""
}
+11
View File
@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 1C-Buhgalteria
orgId: 1
folder: "1C"
type: file
disableDeletion: true
editable: false
options:
path: /var/lib/grafana/dashboards
+10
View File
@@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
+42
View File
@@ -0,0 +1,42 @@
groups:
- name: onec_accounting_alerts
rules:
- alert: OneCUnpostedDocumentsHigh
expr: onec_unposted_documents_total > 100
for: 15m
labels:
severity: warning
service: onec
annotations:
summary: "Высокий объем непроведенных документов ({{ $labels.company }})"
description: "В базе 1С накопилось {{ $value }} непроведенных документов."
- alert: OneCOverdueReceivablesCritical
expr: sum by (company, currency) (onec_overdue_receivables_total) > 500000
for: 15m
labels:
severity: critical
service: onec
annotations:
summary: "Критичная просроченная дебиторка ({{ $labels.company }})"
description: "Просроченная дебиторская задолженность превысила порог: {{ $value }} {{ $labels.currency }}."
- alert: OneCSQLExporterDown
expr: up{job="onec_sql_exporter"} == 0
for: 3m
labels:
severity: critical
service: monitoring
annotations:
summary: "SQL Exporter недоступен"
description: "Prometheus не может получить метрики 1С от sql-exporter."
- alert: OneCDataFreshnessCritical
expr: onec_data_freshness_seconds > 7200
for: 10m
labels:
severity: critical
service: onec
annotations:
summary: "Данные 1С устарели ({{ $labels.company }})"
description: "Последнее обновление источника {{ $labels.source }} старше 2 часов: {{ $value }} сек."
+16
View File
@@ -0,0 +1,16 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
- /etc/prometheus/recording_rules.yml
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: onec_sql_exporter
static_configs:
- targets: ["sql-exporter:9399"]
+11
View File
@@ -0,0 +1,11 @@
groups:
- name: onec_accounting_recording
rules:
- record: onec_overdue_receivables_total:company_currency
expr: sum by (company, currency) (onec_overdue_receivables_total)
- record: onec_unposted_documents_total:company
expr: sum by (company) (onec_unposted_documents_total)
- record: onec_sales_amount_today:company_currency
expr: sum by (company, currency) (onec_sales_amount_today)
@@ -0,0 +1,55 @@
collector_name: onec_accounting_kpi
metrics:
- metric_name: onec_unposted_documents_total
type: gauge
help: "Количество непроведенных документов в 1С."
key_labels:
- company
values: [value]
query: |
SELECT company, value
FROM onec_kpi_unposted_documents;
- metric_name: onec_sales_amount_today
type: gauge
help: "Сумма продаж за текущие сутки (валюта учета)."
key_labels:
- company
- currency
values: [value]
query: |
SELECT company, currency, value
FROM onec_kpi_sales_today;
- metric_name: onec_overdue_receivables_total
type: gauge
help: "Сумма просроченной дебиторской задолженности."
key_labels:
- company
- aging_bucket
- currency
values: [value]
query: |
SELECT company, aging_bucket, currency, value
FROM onec_kpi_overdue_receivables;
- metric_name: onec_posting_errors_total
type: gauge
help: "Количество ошибок проведения документов за последние 24 часа."
key_labels:
- company
values: [value]
query: |
SELECT company, value
FROM onec_kpi_posting_errors_24h;
- metric_name: onec_data_freshness_seconds
type: gauge
help: "Возраст последних данных 1С в секундах."
key_labels:
- company
- source
values: [value]
query: |
SELECT company, source, value
FROM onec_kpi_data_freshness;
+13
View File
@@ -0,0 +1,13 @@
global:
scrape_timeout_offset: 500ms
min_interval: 15s
max_connections: 3
max_idle_connections: 3
target:
data_source_name: "${ONEC_DSN}"
collectors:
- onec_accounting_kpi
collector_files:
- "/etc/sql_exporter/collectors/*.collector.yml"
+86
View File
@@ -0,0 +1,86 @@
-- Шаблон для MS SQL-базы 1С.
-- ВНИМАНИЕ: замените dbo.onec_raw_* на реальные объекты вашей конфигурации 1С.
IF OBJECT_ID('dbo.onec_kpi_unposted_documents', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_unposted_documents;
GO
CREATE VIEW dbo.onec_kpi_unposted_documents AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(COUNT(*) AS float) AS value
FROM dbo.onec_raw_documents d
WHERE d.posted = 0;
GO
IF OBJECT_ID('dbo.onec_kpi_sales_today', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_sales_today;
GO
CREATE VIEW dbo.onec_kpi_sales_today AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(ISNULL(d.currency, 'RUB') AS nvarchar(16)) AS currency,
CAST(ISNULL(SUM(d.amount), 0) AS float) AS value
FROM dbo.onec_raw_sales_documents d
WHERE d.posted = 1
AND CAST(d.doc_date AS date) = CAST(GETDATE() AS date)
GROUP BY ISNULL(d.currency, 'RUB');
GO
IF OBJECT_ID('dbo.onec_kpi_overdue_receivables', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_overdue_receivables;
GO
CREATE VIEW dbo.onec_kpi_overdue_receivables AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END AS nvarchar(16)
) AS aging_bucket,
CAST(ISNULL(currency, 'RUB') AS nvarchar(16)) AS currency,
CAST(SUM(amount_due) AS float) AS value
FROM (
SELECT
r.currency,
r.amount_due,
CASE WHEN DATEDIFF(day, CAST(r.due_date AS date), CAST(GETDATE() AS date)) > 0
THEN DATEDIFF(day, CAST(r.due_date AS date), CAST(GETDATE() AS date))
ELSE 0 END AS age_days
FROM dbo.onec_raw_receivables r
WHERE r.amount_due > 0
) x
WHERE age_days > 0
GROUP BY
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END,
ISNULL(currency, 'RUB');
GO
IF OBJECT_ID('dbo.onec_kpi_posting_errors_24h', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_posting_errors_24h;
GO
CREATE VIEW dbo.onec_kpi_posting_errors_24h AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(COUNT(*) AS float) AS value
FROM dbo.onec_raw_posting_errors e
WHERE e.error_at >= DATEADD(hour, -24, GETDATE());
GO
IF OBJECT_ID('dbo.onec_kpi_data_freshness', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_data_freshness;
GO
CREATE VIEW dbo.onec_kpi_data_freshness AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST('documents' AS nvarchar(64)) AS source,
CAST(
CASE
WHEN MAX(d.updated_at) IS NULL THEN 999999
ELSE DATEDIFF(second, MAX(d.updated_at), GETDATE())
END AS float
) AS value
FROM dbo.onec_raw_documents d;
GO
+62
View File
@@ -0,0 +1,62 @@
-- Шаблон для PostgreSQL-базы 1С.
-- ВНИМАНИЕ: замените onec_raw.* на реальные таблицы/представления вашей конфигурации 1С.
CREATE OR REPLACE VIEW onec_kpi_unposted_documents AS
SELECT
'main'::text AS company,
COUNT(*)::double precision AS value
FROM onec_raw.documents d
WHERE d.posted = false;
CREATE OR REPLACE VIEW onec_kpi_sales_today AS
SELECT
'main'::text AS company,
COALESCE(d.currency, 'RUB')::text AS currency,
COALESCE(SUM(d.amount), 0)::double precision AS value
FROM onec_raw.sales_documents d
WHERE d.posted = true
AND d.doc_date::date = CURRENT_DATE
GROUP BY COALESCE(d.currency, 'RUB');
CREATE OR REPLACE VIEW onec_kpi_overdue_receivables AS
SELECT
'main'::text AS company,
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END::text AS aging_bucket,
COALESCE(currency, 'RUB')::text AS currency,
COALESCE(SUM(amount_due), 0)::double precision AS value
FROM (
SELECT
r.currency,
r.amount_due,
GREATEST((CURRENT_DATE - r.due_date::date), 0) AS age_days
FROM onec_raw.receivables r
WHERE r.amount_due > 0
) x
WHERE age_days > 0
GROUP BY
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END,
COALESCE(currency, 'RUB');
CREATE OR REPLACE VIEW onec_kpi_posting_errors_24h AS
SELECT
'main'::text AS company,
COUNT(*)::double precision AS value
FROM onec_raw.posting_errors e
WHERE e.error_at >= NOW() - INTERVAL '24 hours';
CREATE OR REPLACE VIEW onec_kpi_data_freshness AS
SELECT
'main'::text AS company,
'documents'::text AS source,
COALESCE(EXTRACT(EPOCH FROM (NOW() - MAX(d.updated_at))), 999999)::double precision AS value
FROM onec_raw.documents d;
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -eu
STACK_DIR="${1:-/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c}"
echo "[*] Checking endpoints"
curl -fsS http://127.0.0.1:9399/metrics >/tmp/awrus-onec-metrics.out
curl -fsS http://127.0.0.1:9090/-/healthy >/tmp/awrus-prom-healthy.out
curl -fsS "http://127.0.0.1:9090/api/v1/query?query=up%7Bjob%3D%22onec_sql_exporter%22%7D" >/tmp/awrus-prom-up.json
curl -fsS "http://127.0.0.1:9090/api/v1/query?query=onec_data_freshness_seconds" >/tmp/awrus-prom-freshness.json
echo "[*] Checking container status"
cd "$STACK_DIR"
docker compose ps
echo "[+] Pipeline health artifacts:"
echo " /tmp/awrus-onec-metrics.out"
echo " /tmp/awrus-prom-healthy.out"
echo " /tmp/awrus-prom-up.json"
echo " /tmp/awrus-prom-freshness.json"
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
set -eu
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found" >&2
exit 1
fi
if [ "${1:-}" = "" ]; then
echo "Usage: $0 \"postgres://user:pass@host:5432/db?sslmode=disable\"" >&2
exit 1
fi
DSN="$1"
OUT_DIR="${2:-/tmp/onec-schema-$(date +%Y%m%d-%H%M%S)}"
mkdir -p "$OUT_DIR"
export PGPASSWORD=""
echo "[*] Dumping schemas, tables, columns, candidate accounting objects into $OUT_DIR"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "\dn+" >"$OUT_DIR/schemas.txt"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "\dt+ *.*" >"$OUT_DIR/tables.txt"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "
SELECT table_schema, table_name, column_name, data_type
FROM information_schema.columns
ORDER BY table_schema, table_name, ordinal_position;
" >"$OUT_DIR/columns.tsv"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_type='BASE TABLE'
AND (
table_name ILIKE '%doc%' OR
table_name ILIKE '%sale%' OR
table_name ILIKE '%realiz%' OR
table_name ILIKE '%debt%' OR
table_name ILIKE '%debitor%' OR
table_name ILIKE '%error%' OR
table_name ILIKE '%post%'
)
ORDER BY table_schema, table_name;
" >"$OUT_DIR/candidate_tables.tsv"
echo "[+] Done: $OUT_DIR"
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
set -eu
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found" >&2
exit 1
fi
if [ "${1:-}" = "" ]; then
echo "Usage: $0 \"postgres://user:pass@host:5432/db?sslmode=disable\"" >&2
exit 1
fi
DSN="$1"
VIEWS="
onec_kpi_unposted_documents
onec_kpi_sales_today
onec_kpi_overdue_receivables
onec_kpi_posting_errors_24h
onec_kpi_data_freshness
"
echo "[*] Checking required KPI views"
for view_name in $VIEWS; do
exists="$(psql "$DSN" -X -A -t -v ON_ERROR_STOP=1 -c "SELECT EXISTS (
SELECT 1
FROM information_schema.views
WHERE table_name='${view_name}'
);")"
if [ "$exists" != "t" ]; then
echo "[-] Missing view: $view_name" >&2
exit 2
fi
done
echo "[*] Running test queries"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_unposted_documents LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_sales_today LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_overdue_receivables LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_posting_errors_24h LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_data_freshness LIMIT 5;"
echo "[+] KPI views are valid"
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,40 @@
0754dcba7c651d67a40e09446868d2fcae623a100d4fb01794dd96272d353b49 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt
089595753398c8b82980919d230dafac548c3ba36817f5c96a68051582f9faa3 install-kit-awindows-20260427-211240/ansible/README.md
412bb766bbf0791c3593f38daa771d5d0aa58cc1f2d3c9010fcd4588d0fe87df install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml
90ac38a33918fcd3620f078f51fbf7c6a9d7f8fd1a34d16b38cb3ac45678b0d7 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml
a649eeb57472fb259d248983c486f0474bfb7317600feb98f76a80e7af7e4549 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml
531bfec24f86d28a06e5c0d73005489a818e2c7b1cce1d98f524a3f76802b8ee install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml
95696c243ab331f06e77a40a9800c4b6668de77675ebbdf2ef54ae49e1b18874 install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml
c5cab36645065815571c99f6d360f910dcccbb54b780c8bfd526a6cdc3684e19 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml
35a33c8a1c75ded5e85c6b79e0b3efde07959ff61ee5f66d83b7e0c2abe87fc5 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml
7c468f252e328fd3bb7ee776a45feea88efc4b438dfef55b832da4eea867aaf2 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml
195e7dbdb91f4e77db3263bd0812301ddc912a37ba1519688768bb64a2887567 install-kit-awindows-20260427-211240/ansible/install_full_stack.yml
2e4e94d90143923fefd3ec1257d0ec57daa3e96450d85471bc2c418aae37e105 install-kit-awindows-20260427-211240/ansible/inventory.example.ini
d9e43352fd6bdb647db9754ab2c557b6bb27f88f51b2bf23d8c19227e535e7b9 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml
f3d34547f345ad1c635ee44613a60e7f08479f9ae4d1111810bc7e5968bfb084 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml
ef4ed198745777bd1227170241b2db21dc853685f962d6116241c55216b466d6 install-kit-awindows-20260427-211240/ansible/tasks/provision_ct_and_deploy_aw.yml
a50dbadbf619342c2178e255b68f69a36503756daf80eedd9170311c63964f2e install-kit-awindows-20260427-211240/aw-server/activitywatch-server.service
2dbf55d4a8f204ebdc97af926d90435932c0aad7a2431e2b9987e47c5abf71a9 install-kit-awindows-20260427-211240/aw-server/apply_webui_ru_patch.sh
07d4e583f6e9757a11f01558e1f15cfd73c4d82695f1768204f2f50621712168 install-kit-awindows-20260427-211240/aw-server/aw-host-groups.json
09605da1754abb0dc0446825580b57ebad6e646dc670f9f072fce1489e88dd43 install-kit-awindows-20260427-211240/aw-server/aw-ru-patch.js
7c5952f8f0a8590e849ea8381bfcd7059b138250bca8551bd5625f395eb66cd8 install-kit-awindows-20260427-211240/aw-server/aw-server.env.example
98c0bed353bbda0fa7a69df23f3b008cb0e8e70cdff6cc63330d4caf79fd3280 install-kit-awindows-20260427-211240/aw-server/aw-sw-cleanup.js
dce731fdfdcfd773c154d12dbd6b9e621a0bff17ced5a05a65f0fdcb1adcb70f install-kit-awindows-20260427-211240/aw-server/install_aw_server.sh
1856e9f44636030b0cb9ece37ba2a0618eb5187fa82c7969976c1bb5f10fc622 install-kit-awindows-20260427-211240/aw-server/settings/classes-worktime.json
ff07b90cb6a7f09b27d522307cf55b0359e136a2e695190b8564e859f14f9204 install-kit-awindows-20260427-211240/aw-server/settings/views-default.json
1654cf688560465fcce629468a0be869b0c81b056179e1b5e9bcc7a2d5ed6ce0 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-admin.deployment-config.json
ac022b9a074c542ade66d18af8db385f6c14376ac4eadd54ef033dfa7f60fb50 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-u2u5.deployment-config.json
98cf4c54d494318b74cfbd3c8892830a34928bd76a2296d5333e5e176c1f3f49 install-kit-awindows-20260427-211240/server-configs-192.168.100.21/awatch-rus-user1.deployment-config.json
33aa34b89246d6c079ef9afe2f5cd153bd9d5946b69a175ff6fd678c77f61da5 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psd1
0cb9cd8d8b612429f79899c126f4141ab4fbbb6d919425c0b8fd8d0a74b1bd44 install-kit-awindows-20260427-211240/windows/ActivityWatch.Windows.Common.psm1
7db2d3767ae81c877e8f04ecbc77a2fc26b2d9bbbf77d0e774fba0a7956bd0a6 install-kit-awindows-20260427-211240/windows/browser-domains-native-collector.ps1
b497400a1ba57cddf28dc8e217115dc85eccb67150cbdbb6a81abd804ed20109 install-kit-awindows-20260427-211240/windows/deploy-domain-users.ps1
973db51854fc744539a7b75e13c6749e822a08a79f47447485611f07e8f902a8 install-kit-awindows-20260427-211240/windows/deploy-ensemble.ps1
0d66dcb551889e6b7bc21b29d53b77e46f41d61dd2e4e0d9913dbf0f8bd5eb18 install-kit-awindows-20260427-211240/windows/deploy-single-user.ps1
8857f3e17f3f3ed6f211ce7f0a0c46c586a2548541078401ee3befaa20924b3d install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1
aef0032edd9b1e0c54f7b575664ed511dfc6cb53364e7496cbc95e137678e11a install-kit-awindows-20260427-211240/windows/dlp-policy.example.json
f03886caf56c6838e8a163d6b48d1f229e83a5682aeeb447c3a65f13d62dbca4 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1
71911cd53ad0abd8bf83994f8a79bbbfe2c0eaf4f4c5f6c2d636dd7786191c2f install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1
5dcf249742bd82fa0c803c878bfa0a1344b7b14df85c05f12e8c205663aea158 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1
731098681d89b9af6f3872abd586ac3b1faba2d7f9340211e503f52ad0243b3f install-kit-awindows-20260427-211240/windows/web-category-rules.example.json
41171f0d7ed1e8b00dd0faf1a4b75c9cb063fd09aba8d333851a7a31fb297de1 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1
@@ -0,0 +1,10 @@
ActivityWatch DetMir Windows Install Kit
Includes:
- windows/* (deploy scripts, collectors, common module, configs/examples)
- ansible/* (Windows and AW server playbooks, examples, inventory, tasks)
- aw-server/* (server installer, RU patch loader, host groups, default settings)
- server-configs-192.168.100.21/* (working Windows/RDP config snapshots)
Source:
- Local project snapshot at build time.
@@ -0,0 +1,151 @@
# Ansible ensemble for AWatch-rus
Эта директория содержит Ansible-ensemble для полного развёртывания AWatch-rus:
- деплой на уже существующий Debian host/CT;
- полный цикл с нуля в Proxmox: создание CT + bootstrap + установка ActivityWatch + RU patch;
- централизованное развёртывание Windows/RDP collector'ов по WinRM;
- развёртывание внешнего pfSense poller'а на Debian/Ubuntu utility VM.
## Файлы
- `ansible/deploy_aw_server.yml` — основной playbook для уже существующего Debian/CT host.
- `ansible/provision_proxmox_ct_and_deploy_aw.yml` — полный playbook для Proxmox.
- `ansible/provision_proxmox_ct_matrix_and_deploy_aw.yml` — массовый полный playbook (несколько CT).
- `ansible/deploy_aw_windows.yml` — WinRM playbook для развёртывания Windows/RDP collector'ов.
- `ansible/deploy_aw_pfsense_poller.yml` — развёртывание pfSense poller'а.
- `ansible/install_full_stack.yml` — полный установочный playbook (оркестратор всех этапов).
- `ansible/inventory.example.ini` — шаблон inventory.
- `ansible/group_vars/*.example.yml` — шаблоны переменных.
## Быстрый запуск
1. Скопируйте шаблоны:
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
2. Заполните значения в `inventory.ini` и `group_vars/all.yml`.
3. Запустите:
```bash
cd ansible
ansible-playbook -i inventory.ini deploy_aw_server.yml
```
## Полный установочный playbook (всё за один запуск)
Если нужно прогнать полный цикл одной командой:
```bash
cd ansible
ansible-playbook -i inventory.ini install_full_stack.yml
```
Что делает:
- `provision_proxmox_ct_and_deploy_aw.yml` (если есть хосты в группе `[proxmox]`);
- `deploy_aw_server.yml` (группа `[aw_server]`);
- `deploy_aw_windows.yml` (группа `[aw_windows]`);
- `deploy_aw_pfsense_poller.yml` (группа `[aw_pfsense_pollers]`).
Пустые группы в `inventory.ini` безопасны: соответствующий play будет пропущен.
## Полный запуск с нуля в Proxmox
1. Подготовьте inventory и vars:
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/all.example.yml ansible/group_vars/all.yml`
- `cp ansible/group_vars/proxmox.example.yml ansible/group_vars/proxmox.yml`
2. Заполните `group_vars/proxmox.yml` и `group_vars/all.yml`.
3. Запустите playbook:
```bash
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_and_deploy_aw.yml
```
## Массовый запуск (матрица CT)
1. Подготовьте матрицу:
- `cp ansible/group_vars/proxmox-matrix.example.yml ansible/group_vars/proxmox-matrix.yml`
2. Заполните `proxmox-matrix.yml`.
3. Запустите:
```bash
cd ansible
ansible-playbook -i inventory.ini provision_proxmox_ct_matrix_and_deploy_aw.yml
```
## Windows/RDP rollout (WinRM)
1. Подготовьте inventory и vars:
- `cp ansible/inventory.example.ini ansible/inventory.ini`
- `cp ansible/group_vars/windows.example.yml ansible/group_vars/windows.yml`
2. Заполните `inventory.ini` (секция `[aw_windows]`) и `group_vars/windows.yml`.
- Для русской локализации Windows часто нужен `ansible_user=Администратор` (а не `Administrator`).
- Если WinRM закрыт, playbook не сможет стартовать и нужно сначала открыть `5985/5986` и `wsman`.
3. Запустите:
```bash
cd ansible
ansible-playbook -i inventory.ini deploy_aw_windows.yml
```
Playbook:
- выгружает полный `windows/*` toolkit на целевой хост в InnoSetup-compatible каталог `C:\Program Files\AWatch-rus\windows`, включая DLP и `worktime-session-collector.ps1`;
- если найден legacy config `C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`, выполняет безопасную миграцию через `migrate-awatch-rus-paths.ps1`: backup, остановка задач, перенос данных, переписывание путей, пересоздание scheduled tasks и validation;
- выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с policy/rules из AWatch-rus toolkit;
- после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи;
- выполняет API smoke-check bucket `aw-watcher-afk_<COMPUTERNAME>` и ожидает свежие `not-afk` события;
- запускает `validate-deployment.ps1`;
- забирает JSON-отчёт в локальную директорию (`/tmp/aw-rus-validation` по умолчанию).
Дополнительные флаги:
- `aw_windows_afk_enabled: false` — не запускать `aw-watcher-afk`;
- `aw_windows_window_enabled: false` — не запускать `aw-watcher-window`;
- `aw_windows_incident_capture_enabled: false` — отключить блок incidentCapture;
- `aw_windows_incident_screenshot_enabled: false` — не делать скриншот при DLP-инциденте;
- `aw_windows_incident_artifacts_root: 'C:\...\incident-artifacts'` — переопределить путь артефактов;
- `aw_windows_deploy_root: 'C:\Program Files\AWatch-rus'` — каталог toolkit, совпадает с InnoSetup `{app}`;
- `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`;
- `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`;
- `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`;
- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`;
- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос;
- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте;
- `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch;
- `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_<COMPUTERNAME>`;
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
- `aw_windows_skip_hardening: true` — пропустить `hardening-recovery.ps1` внутри ensemble-скрипта.
## Развёртывание pfSense poller
1. Подготовьте vars:
- `cp ansible/group_vars/pfsense-poller.example.yml ansible/group_vars/pfsense-poller.yml`
2. Добавьте inventory group `[aw_pfsense_pollers]`.
3. Запустите:
```bash
cd ansible
ansible-playbook -i inventory.ini deploy_aw_pfsense_poller.yml
```
Playbook:
- ставит `python3`;
- копирует `pfsense-aw-poller.py`;
- пишет `/etc/aw-pfsense/poller.json`;
- поднимает `aw-pfsense-poller.service`.
## Результат
- Установлен ActivityWatch Server.
- Создан systemd-unit `activitywatch-server.service`.
- Установлен RU Web UI patch.
- Для Web UI используется checksum-based cache-bust для `ru-patch-v5.js` и `sw-cleanup.js`, чтобы браузер не держал старую DLP/русскую статику после деплоя.
- На `#/home` Web UI делит хосты на `Windows RDP` и `Virtual servers + Proxmox`.
- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`.
- Для полного сценария CT создаётся автоматически через `pct create`.
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
@@ -0,0 +1,66 @@
---
- name: Развернуть pfSense ActivityWatch poller
hosts: aw_pfsense_pollers
become: true
gather_facts: true
vars:
aw_pfsense_install_root: "/opt/aw-pfsense"
aw_pfsense_config_dir: "/etc/aw-pfsense"
aw_pfsense_service_name: "aw-pfsense-poller.service"
tasks:
- name: Установить обязательные пакеты
ansible.builtin.apt:
name:
- python3
state: present
update_cache: true
- name: Создать каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ aw_pfsense_install_root }}"
- "{{ aw_pfsense_config_dir }}"
- name: Установить скрипт pfSense poller
ansible.builtin.copy:
src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.py"
dest: "{{ aw_pfsense_install_root }}/pfsense-aw-poller.py"
mode: "0755"
- name: Установить systemd service
ansible.builtin.copy:
src: "{{ aw_repo_root }}/pfsense/pfsense-aw-poller.service"
dest: "/etc/systemd/system/{{ aw_pfsense_service_name }}"
mode: "0644"
notify:
- Перезагрузить systemd
- name: Записать конфигурацию pfSense poller
ansible.builtin.copy:
dest: "{{ aw_pfsense_config_dir }}/poller.json"
mode: "0600"
content: "{{ aw_pfsense_poller_config | to_nice_json }}"
notify:
- Перезапустить pfSense poller
- name: Включить и запустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
enabled: true
state: restarted
daemon_reload: true
handlers:
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Перезапустить pfSense poller
ansible.builtin.systemd:
name: "{{ aw_pfsense_service_name }}"
state: restarted
@@ -0,0 +1,339 @@
---
- name: Развернуть сервер AWatch-rus
hosts: aw_server
become: true
gather_facts: true
vars:
aw_release_root: "/opt/activitywatch/releases"
aw_release_dir: "{{ aw_release_root }}/{{ aw_server_version }}"
aw_archive_path: "/tmp/activitywatch-{{ aw_server_version }}.zip"
aw_bootstrap_dir: "/tmp/aw-rus-bootstrap"
aw_release_install_dir: "{{ aw_release_root }}/aw-server-rust-{{ aw_server_version }}"
aw_ru_patch_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-ru-patch.js') | hash('sha1') | truncate(12, true, '') }}"
aw_sw_cleanup_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-sw-cleanup.js') | hash('sha1') | truncate(12, true, '') }}"
aw_worktime_classes: "{{ lookup('file', aw_repo_root + '/aw-server/settings/classes-worktime.json') | from_json }}"
aw_default_views: "{{ lookup('file', aw_repo_root + '/aw-server/settings/views-default.json') | from_json }}"
tasks:
- name: Установить базовые пакеты
ansible.builtin.apt:
name:
- curl
- rsync
- unzip
state: present
update_cache: true
- name: Создать системную группу сервиса
ansible.builtin.group:
name: "{{ aw_server_group }}"
system: true
state: present
- name: Создать системную учётную запись сервиса
ansible.builtin.user:
name: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
home: "{{ aw_server_data_dir }}"
shell: /usr/sbin/nologin
system: true
create_home: false
- name: Создать обязательные каталоги
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_log_dir }}"
- /etc/activitywatch
- "{{ aw_bootstrap_dir }}"
- name: Настроить каталоги ActivityWatch с владельцем сервиса
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
loop:
- /opt/activitywatch
- /opt/activitywatch/bin
- "{{ aw_release_root }}"
- "{{ aw_release_dir }}"
- "{{ aw_release_install_dir }}"
- "{{ aw_server_webui_dir }}"
- "{{ aw_server_webui_dir }}/js"
- "{{ aw_server_data_dir }}"
- "{{ aw_server_log_dir }}"
- name: Скачать архив релиза ActivityWatch
ansible.builtin.get_url:
url: "{{ aw_server_download_url }}"
dest: "{{ aw_archive_path }}"
mode: "0644"
- name: Распаковать релиз ActivityWatch
ansible.builtin.unarchive:
src: "{{ aw_archive_path }}"
dest: "{{ aw_release_dir }}"
remote_src: true
extra_opts: ["-o"]
- name: Найти распакованный каталог ActivityWatch
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: directory
patterns: "activitywatch*"
register: aw_release_find
- name: Найти бинарный файл AW server
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: file
patterns:
- aw-server-rust
- aw-server
register: aw_server_binary_find
- name: Найти каталог WebUI
ansible.builtin.find:
paths: "{{ aw_release_dir }}"
file_type: directory
patterns:
- aw-webui
- webui
register: aw_webui_dir_find
- name: Сохранить пути распакованного релиза
ansible.builtin.set_fact:
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
- name: Проверить, что компоненты релиза найдены
ansible.builtin.assert:
that:
- aw_release_extracted is defined
- aw_release_extracted | length > 0
- aw_server_binary_path is defined
- aw_server_binary_path | length > 0
- aw_webui_source_path is defined
- aw_webui_source_path | length > 0
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
- name: Создать каталог установленного релиза
ansible.builtin.file:
path: "{{ aw_release_install_dir }}"
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Установить бинарный файл AW server
ansible.builtin.copy:
remote_src: true
src: "{{ aw_server_binary_path }}"
dest: "{{ aw_release_install_dir }}/aw-server-rust"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
mode: "0755"
- name: Создать ссылку на активный бинарный файл AW server
ansible.builtin.file:
src: "{{ aw_release_install_dir }}/aw-server-rust"
dest: /opt/activitywatch/bin/aw-server-rust
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
state: link
force: true
- name: Синхронизировать WebUI в RU каталог
ansible.builtin.command:
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
- name: Настроить владельца файлов /opt/activitywatch
ansible.builtin.file:
path: /opt/activitywatch
state: directory
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
recurse: true
- name: Установить systemd service из шаблона репозитория
ansible.builtin.copy:
dest: /etc/systemd/system/activitywatch-server.service
mode: "0644"
content: >-
{{
lookup('file', aw_repo_root + '/aw-server/activitywatch-server.service')
| replace('__AW_SERVER_USER__', aw_server_user)
| replace('__AW_SERVER_GROUP__', aw_server_group)
| replace('__AW_SERVER_DATA_DIR__', aw_server_data_dir)
}}
notify:
- Перезагрузить systemd
- Перезапустить activitywatch
- name: Скопировать RU patch файлы WebUI из репозитория
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
owner: "{{ aw_server_user }}"
group: "{{ aw_server_group }}"
loop:
- { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" }
- { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" }
- { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" }
- name: Проверить наличие index.html после копирования
ansible.builtin.stat:
path: "{{ aw_server_webui_dir }}/index.html"
register: aw_webui_ru_index
- name: Проверить, что index.html доступен для RU patch
ansible.builtin.assert:
that:
- aw_webui_ru_index.stat.exists
fail_msg: "Не найден index.html WebUI для применения RU patch."
- name: Удалить старые теги RU patch из index.html
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
replace: ''
- name: Добавить cleanup script RU patch в index.html
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '</head>'
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
- name: Добавить загрузчик RU patch перед закрытием body
ansible.builtin.replace:
path: "{{ aw_server_webui_dir }}/index.html"
regexp: '</body>'
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
- name: Записать /etc/activitywatch/aw-server.env
ansible.builtin.copy:
dest: /etc/activitywatch/aw-server.env
mode: "0640"
owner: root
group: root
content: |
AW_SERVER_BIND_HOST={{ aw_server_bind_host }}
AW_SERVER_PORT={{ aw_server_port }}
AW_SERVER_DATA_DIR={{ aw_server_data_dir }}
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
- name: Включить и запустить сервис
ansible.builtin.systemd:
name: activitywatch-server.service
enabled: true
state: restarted
daemon_reload: true
- name: Дождаться ответа API
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
method: GET
status_code: 200
register: aw_api
retries: 10
delay: 3
until: aw_api.status == 200
- name: Применить базовые worktime settings (classes)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes"
method: POST
body: "{{ aw_worktime_classes }}"
body_format: json
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Применить базовые views для DLP и worktime
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views"
method: POST
body: "{{ aw_default_views }}"
body_format: json
status_code: [200, 201]
when: aw_apply_worktime_settings | default(false) | bool
- name: Вычислить worktime durationDefault из aw_worktime_from/to
ansible.builtin.set_fact:
aw_worktime_from_h: "{{ (aw_worktime_from | default('08:00')).split(':')[0] | int }}"
aw_worktime_from_m: "{{ (aw_worktime_from | default('08:00')).split(':')[1] | int }}"
aw_worktime_to_h: "{{ (aw_worktime_to | default('17:00')).split(':')[0] | int }}"
aw_worktime_to_m: "{{ (aw_worktime_to | default('17:00')).split(':')[1] | int }}"
aw_worktime_duration_default_derived: >-
{{
(
(
((aw_worktime_to_h | int) * 60 + (aw_worktime_to_m | int)) -
((aw_worktime_from_h | int) * 60 + (aw_worktime_from_m | int))
) * 60
)
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Нормализовать durationDefault для ночных смен
ansible.builtin.set_fact:
aw_worktime_duration_default_effective: >-
{{
(aw_worktime_duration_default_derived | int)
if (aw_worktime_duration_default_derived | int) > 0
else ((aw_worktime_duration_default_derived | int) + 86400)
}}
when: aw_apply_worktime_settings | default(false) | bool
- name: Проверить корректность durationDefault
ansible.builtin.assert:
that:
- aw_worktime_duration_default_effective | int > 0
- aw_worktime_duration_default_effective | int <= 86400
fail_msg: "Некорректный интервал рабочего времени: {{ aw_worktime_from }}..{{ aw_worktime_to }}"
when: aw_apply_worktime_settings | default(false) | bool
- name: Применить базовый период worktime (startOfDay)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
method: POST
body: "{{ aw_worktime_start_of_day }}"
body_format: json
status_code: 200
when: aw_apply_worktime_settings | default(false) | bool
- name: Применить базовый период worktime (durationDefault seconds)
ansible.builtin.uri:
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
method: POST
body: "{{ aw_worktime_duration_default_effective }}"
body_format: json
status_code: 200
when: aw_apply_worktime_settings | default(false) | bool
handlers:
- name: Перезагрузить systemd
ansible.builtin.systemd:
daemon_reload: true
- name: Перезапустить activitywatch
ansible.builtin.systemd:
name: activitywatch-server.service
state: restarted
@@ -0,0 +1,232 @@
---
- name: Развернуть Windows/RDP collector'ы AWatch-rus
hosts: aw_windows
gather_facts: false
vars:
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
aw_windows_users_effective: "{{ (aw_windows_users + aw_windows_extra_users) | unique }}"
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
aw_windows_launch_task_pattern: "ActivityWatch Launch *"
aw_windows_recovery_task_name: "ActivityWatch Recovery"
aw_windows_force_task_restart: true
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
aw_windows_fail_on_validation_error: true
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
tasks:
- name: Проверить обязательные переменные
ansible.builtin.assert:
that:
- aw_windows_server_host is defined
- aw_windows_server_port is defined
- aw_windows_server_scheme is defined
- aw_windows_domain is defined
- aw_windows_users_effective | length > 0
- aw_windows_install_root is defined
- aw_windows_state_root is defined
fail_msg: "Не заданы обязательные переменные Windows-развёртывания."
- name: Создать каталоги развёртывания
ansible.windows.win_file:
path: "{{ item }}"
state: directory
loop:
- "{{ aw_windows_deploy_root }}"
- "{{ aw_windows_deploy_root }}\\windows"
- name: Загрузить Windows toolkit развёртывания
ansible.windows.win_copy:
src: "{{ aw_windows_repo_root }}/windows/{{ item }}"
dest: "{{ aw_windows_deploy_root }}\\windows\\{{ item }}"
loop:
- ActivityWatch.Windows.Common.psd1
- ActivityWatch.Windows.Common.psm1
- browser-domains-native-collector.ps1
- dlp-endpoint-signals-collector.ps1
- worktime-session-collector.ps1
- migrate-awatch-rus-paths.ps1
- deploy-domain-users.ps1
- deploy-ensemble.ps1
- hardening-recovery.ps1
- validate-deployment.ps1
- web-category-rules.example.json
- dlp-policy.example.json
- name: Загрузить список пользователей для доменного развёртывания
ansible.windows.win_copy:
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
content: |
{% for user in aw_windows_users_effective -%}
{{ user }}
{% endfor -%}
- name: Проверить нужен ли migration с legacy ActivityWatch путей
when: aw_windows_migration_enabled | bool
ansible.windows.win_stat:
path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json"
register: aw_windows_legacy_config
- name: Выполнить безопасную migration legacy prod в AWatch-rus
when:
- aw_windows_migration_enabled | bool
- aw_windows_legacy_config.stat.exists | default(false)
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" `
-OldInstallRoot "{{ aw_windows_legacy_install_root }}" `
-OldStateRoot "{{ aw_windows_legacy_state_root }}" `
-NewInstallRoot "{{ aw_windows_install_root }}" `
-NewStateRoot "{{ aw_windows_state_root }}" `
-ToolkitRoot "{{ aw_windows_deploy_root }}\windows"
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8
- name: Запустить Windows/RDP ensemble развёртывание
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$params = @{
ServerScheme = "{{ aw_windows_server_scheme }}"
ServerHost = "{{ aw_windows_server_host }}"
ServerPort = {{ aw_windows_server_port }}
Version = "{{ aw_windows_package_version }}"
Domain = "{{ aw_windows_domain }}"
UserListPath = "{{ aw_windows_deploy_root }}\windows\users.txt"
InstallRoot = "{{ aw_windows_install_root }}"
StateRoot = "{{ aw_windows_state_root }}"
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
IncidentArtifactsRoot = "{{ aw_windows_incident_artifacts_root }}"
LogonMarkerEnabled = {{ '$true' if (aw_windows_logon_marker_enabled | bool) else '$false' }}
CustomRulesPath = "{{ aw_windows_rules_path }}"
CustomPolicyPath = "{{ aw_windows_policy_path }}"
}
{% if (aw_windows_package_url | default('') | string | length) > 0 %}
$params.PackageUrl = "{{ aw_windows_package_url }}"
{% endif %}
{% if (aw_windows_package_zip_path | default('') | string | length) > 0 %}
$params.PackageZipPath = "{{ aw_windows_package_zip_path }}"
{% endif %}
{% if aw_windows_skip_hardening | bool %}
$params.SkipHardening = $true
{% endif %}
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
- name: Принудительно запустить ActivityWatch recovery и launch tasks
when: aw_windows_force_task_restart | bool
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
Start-ScheduledTask -TaskName "{{ aw_windows_recovery_task_name }}"
Get-ScheduledTask |
Where-Object TaskName -like "{{ aw_windows_launch_task_pattern }}" |
ForEach-Object { Start-ScheduledTask -TaskName $_.TaskName }
- name: Получить Windows hostname для AW smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.windows.win_command: powershell.exe -NoProfile -Command "$env:COMPUTERNAME"
register: aw_windows_hostname_result
changed_when: false
- name: Вычислить AW AFK smoke-check bucket
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
ansible.builtin.set_fact:
aw_windows_api_smoke_check_bucket_effective: >-
{{
aw_windows_api_smoke_check_bucket
if (aw_windows_api_smoke_check_bucket | default('') | string | length) > 0
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
}}
- name: Дождаться свежих AFK событий на AW server
when:
- aw_windows_api_smoke_check_enabled | bool
- aw_windows_afk_enabled | bool
delegate_to: localhost
ansible.builtin.uri:
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
method: GET
return_content: true
register: aw_windows_api_smoke
until: >
aw_windows_api_smoke.status == 200 and
(aw_windows_api_smoke.json | length) > 0 and
(
aw_windows_api_smoke.json
| selectattr('data.status', 'equalto', 'not-afk')
| list
| length
) > 0
retries: 10
delay: 6
- name: Выполнить валидацию и сохранить отчёт на целевом Windows host
ansible.windows.win_powershell:
script: |
$ErrorActionPreference = 'Stop'
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}"
}
- name: Создать локальный каталог для validation reports
ansible.builtin.file:
path: "{{ aw_windows_validation_local_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
- name: Забрать validation report
ansible.builtin.fetch:
src: "{{ aw_windows_validation_remote_path }}"
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
flat: true
- name: Показать путь к отчёту
ansible.builtin.debug:
msg:
- "Windows/RDP развёртывание завершено на {{ inventory_hostname }}."
- "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
@@ -0,0 +1,24 @@
aw_server_version: "v0.13.2"
aw_server_download_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-linux-x86_64.zip"
aw_server_bind_host: "0.0.0.0"
aw_server_port: 5600
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
aw_server_data_dir: "/var/lib/activitywatch"
aw_server_log_dir: "/var/log/activitywatch"
aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_repo_root: "{{ playbook_dir | dirname }}"
# Опционально: применить базовые категории и views для рабочего времени через AW settings API.
# Внимание: это перезаписывает существующие server-side settings/classes/views.
aw_apply_worktime_settings: false
# Опциональные значения периода рабочего времени в Web UI.
# startOfDay задаёт границу дня и стартовое время окна отчёта.
# durationDefault задаёт диапазон по умолчанию в секундах.
#
# Рекомендуется явно задать рабочий интервал и дать playbook вычислить duration.
aw_worktime_from: "08:00"
aw_worktime_to: "17:00"
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
@@ -0,0 +1,30 @@
aw_pfsense_poller_config:
poll_interval_seconds: 60
aw:
server_host: "10.10.10.13"
server_port: 5600
hostname: "PFSENSE-EDGE01"
pulse_time_seconds: 120
timeout_seconds: 15
pfsense:
name: "pfSense Edge 01"
host: "10.10.10.1"
scheme: "https"
verify_tls: false
timeout_seconds: 15
headers:
X-API-Key: "replace-me"
X-API-Secret: "replace-me"
endpoints:
- name: "system-status"
path: "/api/v2/status/system"
bucket_prefix: "aw-pfsense-health"
bucket_type: "aw.pfsense.health"
- name: "interfaces"
path: "/api/v2/interface"
bucket_prefix: "aw-pfsense-interfaces"
bucket_type: "aw.pfsense.interfaces"
- name: "gateways"
path: "/api/v2/status/gateways"
bucket_prefix: "aw-pfsense-gateways"
bucket_type: "aw.pfsense.gateways"
@@ -0,0 +1,38 @@
proxmox_ct_matrix:
- id: "203"
hostname: "activitywatch-user1"
storage: "local-lvm"
template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
rootfs_size: "8G"
cores: "2"
memory: "2048"
swap: "512"
bridge: "vmbr10"
ip: "10.20.30.13/24"
gw: "10.20.30.1"
vlan: ""
nameserver: "1.1.1.1 8.8.8.8"
searchdomain: "example.internal"
password: "CHANGE_ME"
unprivileged: "1"
onboot: "1"
features: "nesting=1,keyctl=1"
- id: "204"
hostname: "activitywatch-user2"
storage: "local-lvm"
template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
rootfs_size: "8G"
cores: "2"
memory: "2048"
swap: "512"
bridge: "vmbr10"
ip: "10.20.30.14/24"
gw: "10.20.30.1"
vlan: ""
nameserver: "1.1.1.1 8.8.8.8"
searchdomain: "example.internal"
password: "CHANGE_ME"
unprivileged: "1"
onboot: "1"
features: "nesting=1,keyctl=1"
@@ -0,0 +1,18 @@
proxmox_ct_id: "203"
proxmox_ct_hostname: "activitywatch-server"
proxmox_ct_storage: "local-lvm"
proxmox_ct_template: "local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
proxmox_ct_rootfs_size: "8G"
proxmox_ct_cores: "2"
proxmox_ct_memory: "2048"
proxmox_ct_swap: "512"
proxmox_ct_bridge: "vmbr10"
proxmox_ct_ip: "10.20.30.13/24"
proxmox_ct_gw: "10.20.30.1"
proxmox_ct_vlan: ""
proxmox_ct_nameserver: "1.1.1.1 8.8.8.8"
proxmox_ct_searchdomain: "example.internal"
proxmox_ct_password: "CHANGE_ME"
proxmox_ct_unprivileged: "1"
proxmox_ct_onboot: "1"
proxmox_ct_features: "nesting=1,keyctl=1"
@@ -0,0 +1,50 @@
aw_windows_repo_root: "{{ playbook_dir | dirname }}"
aw_windows_deploy_root: "C:\\Program Files\\AWatch-rus"
aw_windows_server_scheme: "http"
aw_windows_server_host: "10.10.10.13"
aw_windows_server_port: 5600
aw_windows_package_version: "v0.13.2"
aw_windows_package_url: "https://github.com/ActivityWatch/activitywatch/releases/download/v0.13.2/activitywatch-v0.13.2-windows-x86_64.zip"
aw_windows_package_zip_path: ""
aw_windows_domain: "SHARKON2025"
aw_windows_users:
- user1
- user2
- user3
- user4
- user5
aw_windows_extra_users: []
# Например:
# aw_windows_extra_users:
# - Администратор
# Единые Windows/RDP пути: те же, что использует InnoSetup.
aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
aw_windows_afk_enabled: true
aw_windows_window_enabled: true
aw_windows_local_agent_logs_enabled: false
aw_windows_incident_capture_enabled: true
aw_windows_incident_screenshot_enabled: true
aw_windows_incident_artifacts_root: "{{ aw_windows_state_root }}\\incident-artifacts"
aw_windows_logon_marker_enabled: true
aw_windows_skip_hardening: false
aw_windows_rules_path: "{{ aw_windows_deploy_root }}\\windows\\web-category-rules.example.json"
aw_windows_policy_path: "{{ aw_windows_deploy_root }}\\windows\\dlp-policy.example.json"
aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ansible.json"
aw_windows_validation_local_dir: "/tmp/aw-rus-validation"
aw_windows_fail_on_validation_error: true
# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus.
aw_windows_migration_enabled: true
aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2"
aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2"
aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json"
# По умолчанию AFK bucket вычисляется как aw-watcher-afk_<COMPUTERNAME>.
# Задайте явное значение только если watcher пишет в нестандартный bucket.
aw_windows_api_smoke_check_enabled: true
aw_windows_api_smoke_check_bucket: ""
aw_windows_api_smoke_check_limit: 10
@@ -0,0 +1,16 @@
---
# Полный установщик AWatch-rus.
# Выполняет развёртывание одной командой:
# 1) создание Proxmox CT + bootstrap AW (если в inventory есть [proxmox])
# 2) развёртывание AW server на хостах [aw_server]
# 3) развёртывание Windows/RDP collector'ов на [aw_windows]
# 4) развёртывание pfSense poller'а на [aw_pfsense_pollers]
#
# Примечания:
# - Заполняйте только нужные группы inventory для своего окружения.
# - Play без совпадающих host groups Ansible пропускает автоматически.
- import_playbook: provision_proxmox_ct_and_deploy_aw.yml
- import_playbook: deploy_aw_server.yml
- import_playbook: deploy_aw_windows.yml
- import_playbook: deploy_aw_pfsense_poller.yml
@@ -0,0 +1,12 @@
[proxmox]
pve-main ansible_host=192.168.10.2 ansible_user=root ansible_port=22
[aw_server]
aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22
[aw_windows]
# Примечание: в русифицированных Windows часто нужен "Администратор", а не "Administrator".
win-node1 ansible_host=192.168.100.21 ansible_user=Администратор ansible_password=CHANGE_ME ansible_connection=winrm ansible_winrm_transport=ntlm ansible_port=5985 ansible_winrm_server_cert_validation=ignore
[aw_pfsense_pollers]
# pfsense-poller1 ansible_host=192.168.100.30 ansible_user=root ansible_port=22
@@ -0,0 +1,40 @@
---
- name: Создать один Proxmox CT и развернуть AWatch-rus
hosts: proxmox
gather_facts: false
vars:
proxmox_bootstrap_dir: "/tmp/aw-rus-bootstrap"
aw_bootstrap_files:
- install_aw_server.sh
- apply_webui_ru_patch.sh
- activitywatch-server.service
- aw-server.env.example
- aw-ru-patch.js
- aw-sw-cleanup.js
- aw-host-groups.json
- settings/classes-worktime.json
- settings/views-default.json
tasks:
- name: Выполнить workflow создания одного CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ proxmox_ct_id }}"
ct_hostname: "{{ proxmox_ct_hostname }}"
ct_storage: "{{ proxmox_ct_storage }}"
ct_template: "{{ proxmox_ct_template }}"
ct_rootfs_size: "{{ proxmox_ct_rootfs_size }}"
ct_cores: "{{ proxmox_ct_cores }}"
ct_memory: "{{ proxmox_ct_memory }}"
ct_swap: "{{ proxmox_ct_swap }}"
ct_bridge: "{{ proxmox_ct_bridge }}"
ct_ip: "{{ proxmox_ct_ip }}"
ct_gw: "{{ proxmox_ct_gw }}"
ct_vlan: "{{ proxmox_ct_vlan | default('') }}"
ct_nameserver: "{{ proxmox_ct_nameserver | default('') }}"
ct_searchdomain: "{{ proxmox_ct_searchdomain | default('') }}"
ct_password: "{{ proxmox_ct_password }}"
ct_unprivileged: "{{ proxmox_ct_unprivileged }}"
ct_onboot: "{{ proxmox_ct_onboot }}"
ct_features: "{{ proxmox_ct_features }}"
@@ -0,0 +1,50 @@
---
- name: Создать матрицу Proxmox CT и развернуть AWatch-rus с RU patch
hosts: proxmox
gather_facts: false
vars:
proxmox_bootstrap_dir: "/tmp/aw-rus-bootstrap"
aw_bootstrap_files:
- install_aw_server.sh
- apply_webui_ru_patch.sh
- activitywatch-server.service
- aw-server.env.example
- aw-ru-patch.js
- aw-sw-cleanup.js
- aw-host-groups.json
- settings/classes-worktime.json
- settings/views-default.json
tasks:
- name: Проверить, что матрица CT задана
ansible.builtin.assert:
that:
- proxmox_ct_matrix is defined
- proxmox_ct_matrix | length > 0
fail_msg: "Задайте proxmox_ct_matrix в group_vars/proxmox-matrix.yml"
- name: Выполнить workflow создания для каждого CT
ansible.builtin.include_tasks: tasks/provision_ct_and_deploy_aw.yml
vars:
ct_id: "{{ item.id }}"
ct_hostname: "{{ item.hostname }}"
ct_storage: "{{ item.storage }}"
ct_template: "{{ item.template }}"
ct_rootfs_size: "{{ item.rootfs_size }}"
ct_cores: "{{ item.cores }}"
ct_memory: "{{ item.memory }}"
ct_swap: "{{ item.swap }}"
ct_bridge: "{{ item.bridge }}"
ct_ip: "{{ item.ip }}"
ct_gw: "{{ item.gw }}"
ct_vlan: "{{ item.vlan | default('') }}"
ct_nameserver: "{{ item.nameserver | default('') }}"
ct_searchdomain: "{{ item.searchdomain | default('') }}"
ct_password: "{{ item.password }}"
ct_unprivileged: "{{ item.unprivileged }}"
ct_onboot: "{{ item.onboot }}"
ct_features: "{{ item.features }}"
loop: "{{ proxmox_ct_matrix }}"
loop_control:
label: "ct={{ item.id }} host={{ item.hostname }} ip={{ item.ip }}"
@@ -0,0 +1,227 @@
---
- name: Проверить обязательные переменные CT
ansible.builtin.assert:
that:
- ct_id is defined
- ct_hostname is defined
- ct_storage is defined
- ct_template is defined
- ct_rootfs_size is defined
- ct_cores is defined
- ct_memory is defined
- ct_swap is defined
- ct_bridge is defined
- ct_ip is defined
- ct_gw is defined
- ct_password is defined
- ct_unprivileged is defined
- ct_onboot is defined
- ct_features is defined
- aw_repo_root is defined
- aw_server_version is defined
- aw_server_download_url is defined
- aw_server_bind_host is defined
- aw_server_port is defined
- aw_server_webui_dir is defined
- aw_server_data_dir is defined
- aw_server_log_dir is defined
- aw_server_user is defined
- aw_server_group is defined
fail_msg: "Не заданы обязательные переменные для создания CT и развёртывания."
- name: Сформировать сетевую строку CT
ansible.builtin.set_fact:
ct_net0: >-
name=eth0,bridge={{ ct_bridge }},ip={{ ct_ip }},gw={{ ct_gw }}{% if (ct_vlan | default('') | string | length) > 0 %},tag={{ ct_vlan }}{% endif %}
- name: Проверить, существует ли CT
ansible.builtin.command:
argv:
- pct
- status
- "{{ ct_id }}"
register: ct_status_check
failed_when: false
changed_when: false
- name: Создать CT, если он отсутствует
ansible.builtin.command:
argv:
- pct
- create
- "{{ ct_id }}"
- "{{ ct_template }}"
- --hostname
- "{{ ct_hostname }}"
- --cores
- "{{ ct_cores }}"
- --memory
- "{{ ct_memory }}"
- --swap
- "{{ ct_swap }}"
- --rootfs
- "{{ ct_storage }}:{{ ct_rootfs_size }}"
- --password
- "{{ ct_password }}"
- --unprivileged
- "{{ ct_unprivileged }}"
- --onboot
- "{{ ct_onboot }}"
- --features
- "{{ ct_features }}"
- --net0
- "{{ ct_net0 }}"
- --nameserver
- "{{ ct_nameserver | default('') }}"
- --searchdomain
- "{{ ct_searchdomain | default('') }}"
- --ostype
- debian
when: ct_status_check.rc != 0
no_log: true
- name: Проверить текущее состояние CT
ansible.builtin.command:
argv:
- pct
- status
- "{{ ct_id }}"
register: ct_runtime_status
changed_when: false
- name: Запустить CT, если он остановлен
ansible.builtin.command:
argv:
- pct
- start
- "{{ ct_id }}"
when: "'stopped' in ct_runtime_status.stdout"
- name: Создать bootstrap каталог на Proxmox host
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: "0700"
loop:
- "{{ proxmox_bootstrap_dir }}"
- "{{ proxmox_bootstrap_dir }}/settings"
- name: Скопировать AW bootstrap файлы во временный каталог Proxmox host
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/{{ item }}"
dest: "{{ proxmox_bootstrap_dir }}/{{ item }}"
mode: "0644"
loop: "{{ aw_bootstrap_files }}"
- name: Установить базовые зависимости ОС внутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- |
set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y curl ca-certificates bash unzip xz-utils jq rsync openssh-server python3
mkdir -p /root/bootstrap/settings /etc/activitywatch
systemctl enable ssh || true
systemctl restart ssh || true
register: ct_bootstrap_result
retries: 10
delay: 6
until: ct_bootstrap_result.rc == 0
- name: Передать bootstrap файлы внутрь CT
ansible.builtin.command:
argv:
- pct
- push
- "{{ ct_id }}"
- "{{ proxmox_bootstrap_dir }}/{{ item }}"
- "/root/bootstrap/{{ item }}"
loop: "{{ aw_bootstrap_files }}"
- name: Записать AW server env во временный каталог Proxmox host
ansible.builtin.copy:
dest: "{{ proxmox_bootstrap_dir }}/aw-server.env"
mode: "0600"
content: |
AW_SERVER_VERSION={{ aw_server_version }}
AW_SERVER_DOWNLOAD_URL={{ aw_server_download_url }}
AW_SERVER_BIND_HOST={{ aw_server_bind_host }}
AW_SERVER_PORT={{ aw_server_port }}
AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }}
AW_SERVER_DATA_DIR={{ aw_server_data_dir }}
AW_SERVER_LOG_DIR={{ aw_server_log_dir }}
AW_SERVER_USER={{ aw_server_user }}
AW_SERVER_GROUP={{ aw_server_group }}
no_log: true
- name: Передать AW server env внутрь CT
ansible.builtin.command:
argv:
- pct
- push
- "{{ ct_id }}"
- "{{ proxmox_bootstrap_dir }}/aw-server.env"
- /etc/activitywatch/aw-server.env
no_log: true
- name: Настроить права env файла внутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- chmod
- "0600"
- /etc/activitywatch/aw-server.env
- name: Установить сервер и применить RU patch внутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- |
set -euo pipefail
chmod +x /root/bootstrap/install_aw_server.sh /root/bootstrap/apply_webui_ru_patch.sh
bash /root/bootstrap/install_aw_server.sh
bash /root/bootstrap/apply_webui_ru_patch.sh
systemctl restart activitywatch-server.service
- name: Проверить AW API изнутри CT
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- "curl -fsS http://127.0.0.1:{{ aw_server_port }}/api/0/info >/dev/null"
- name: Проверить hooks RU patch в index.html
ansible.builtin.command:
argv:
- pct
- exec
- "{{ ct_id }}"
- --
- bash
- -lc
- "grep -q 'ru-patch-v5.js' {{ aw_server_webui_dir }}/index.html && grep -q 'sw-cleanup.js' {{ aw_server_webui_dir }}/index.html"
- name: Показать итоговый endpoint
ansible.builtin.debug:
msg:
- "CT {{ ct_id }} создан и настроен."
- "Endpoint ActivityWatch: http://{{ ct_ip | regex_replace('/[0-9]+$', '') }}:{{ aw_server_port }}"
@@ -0,0 +1,24 @@
[Unit]
Description=ActivityWatch Server (Rust)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
EnvironmentFile=/etc/activitywatch/aw-server.env
User=__AW_SERVER_USER__
Group=__AW_SERVER_GROUP__
WorkingDirectory=__AW_SERVER_DATA_DIR__
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"'
Restart=on-failure
RestartSec=5s
StateDirectory=activitywatch
LogsDirectory=activitywatch
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,111 @@
#!/bin/bash
set -euo pipefail
ENV_FILE="/etc/activitywatch/aw-server.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "missing env file: $ENV_FILE" >&2
exit 1
fi
source "$ENV_FILE"
WEBUI_DIR="${AW_SERVER_WEBUI_DIR:-${AW_WEBUI_DIR:-/opt/activitywatch/webui-ru}}"
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js"
HOST_GROUPS_SRC="/root/bootstrap/aw-host-groups.json"
INDEX_HTML="$WEBUI_DIR/index.html"
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
TS=$(date +%Y%m%d%H%M%S)
PATCH_TARGET="$WEBUI_DIR/js/ru-patch-v5.js"
SW_TARGET="$WEBUI_DIR/js/sw-cleanup.js"
HOST_GROUPS_TARGET="$WEBUI_DIR/js/aw-host-groups.json"
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
TIMESPIRAL_NEEDLE='start:new Date("2022-08-08")'
TIMESPIRAL_REPLACEMENT='start:new Date(Date.now()-12*36e5)'
CATEGORY_HELPER_NEEDLE='hostname:t.hostnameChoices[0]'
CATEGORY_HELPER_REPLACEMENT='hostname:t.hostnameChoices.filter((function(t){return"unknown"!==t}))[0]||t.hostnameChoices[0]'
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
[[ -f "$HOST_GROUPS_SRC" ]] || { echo "missing $HOST_GROUPS_SRC" >&2; exit 1; }
[[ -f "$INDEX_HTML" ]] || { echo "missing $INDEX_HTML" >&2; exit 1; }
install -d "$WEBUI_DIR/js"
install -m 0644 "$PATCH_JS_SRC" "$PATCH_TARGET"
install -m 0644 "$SW_CLEANUP_SRC" "$SW_TARGET"
install -m 0644 "$HOST_GROUPS_SRC" "$HOST_GROUPS_TARGET"
cp "$INDEX_HTML" "$INDEX_HTML.bak.$TS"
patch_hash="$(sha1sum "$PATCH_TARGET" | awk '{print substr($1,1,12)}')"
sw_hash="$(sha1sum "$SW_TARGET" | awk '{print substr($1,1,12)}')"
sed -i '/ru-patch-v5.js/d;/sw-cleanup.js/d;/aw-ru-patch.js/d;/aw-sw-cleanup.js/d' "$INDEX_HTML"
sed -i "s#</head>#<script src=\"/js/sw-cleanup.js?v=$sw_hash\"></script></head>#" "$INDEX_HTML"
sed -i "s#</body>#<script defer=\"defer\" src=\"/js/ru-patch-v5.js?v=$patch_hash\"></script></body>#" "$INDEX_HTML"
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
if [[ -n "$trends_chunk" ]]; then
cp "$trends_chunk" "$trends_chunk.bak.$TS"
python3 - "$trends_chunk" "$TRENDS_NEEDLE" "$TRENDS_REPLACEMENT" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
old = sys.argv[2]
new = sys.argv[3]
content = path.read_text()
if old in content:
path.write_text(content.replace(old, new, 1))
print(f"Trends hotfix applied to {path}")
else:
print(f"Trends hotfix already present in {path}")
PY
else
echo "Trends hotfix skipped: chunk not found"
fi
timespiral_chunk="$(grep -Rsl "$TIMESPIRAL_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
if [[ -n "$timespiral_chunk" ]]; then
cp "$timespiral_chunk" "$timespiral_chunk.bak.$TS"
python3 - "$timespiral_chunk" "$TIMESPIRAL_NEEDLE" "$TIMESPIRAL_REPLACEMENT" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
old = sys.argv[2]
new = sys.argv[3]
content = path.read_text()
if old in content:
path.write_text(content.replace(old, new, 1))
print(f"Timespiral hotfix applied to {path}")
else:
print(f"Timespiral hotfix already present in {path}")
PY
else
echo "Timespiral hotfix skipped: chunk not found"
fi
category_helper_chunk="$(grep -Rsl "$CATEGORY_HELPER_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
if [[ -n "$category_helper_chunk" ]]; then
cp "$category_helper_chunk" "$category_helper_chunk.bak.$TS"
python3 - "$category_helper_chunk" "$CATEGORY_HELPER_NEEDLE" "$CATEGORY_HELPER_REPLACEMENT" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
old = sys.argv[2]
new = sys.argv[3]
content = path.read_text()
if old in content:
path.write_text(content.replace(old, new, 1))
print(f"Category helper host hotfix applied to {path}")
else:
print(f"Category helper host hotfix already present in {path}")
PY
else
echo "Category helper host hotfix skipped: chunk not found"
fi
echo "RU patch applied to $WEBUI_DIR (ru-patch-v5.js?v=$patch_hash)"
@@ -0,0 +1,47 @@
{
"groups": [
{
"id": "pve-detmir",
"name": "pve-detmir",
"description": "Выделенный клиент DetMir в разделе Активность.",
"patterns": [
"^pve-detmir$"
],
"links": [
{ "label": "Активность", "type": "activity", "view": "pve_audit" },
{ "label": "Web-admin аудит", "type": "bucket", "bucket_prefix": "aw-pve-webadmin-events_" },
{ "label": "PVE tasks", "type": "bucket", "bucket_prefix": "aw-pve-task-events_" },
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
{ "label": "Все бакеты", "type": "buckets" }
]
},
{
"id": "windows-rdp",
"name": "Windows RDP",
"description": "Пользовательские Windows/RDP хосты с активностью, DLP и рабочим временем.",
"patterns": [
"^(SHARKON|WIN|RDP|TERM|TS-|WS-)"
],
"links": [
{ "label": "Активность", "type": "activity" },
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
]
},
{
"id": "virtual-infra",
"name": "Virtual servers + Proxmox",
"description": "Инфраструктурные VM и сетевые узлы. Здесь должны лежать Proxmox, pfSense, Debian и Ubuntu серверы.",
"patterns": [
"^(PFSENSE|PVE|PROXMOX|DEBIAN|UBUNTU|LINUX|VM-|SRV-|INFRA-)"
],
"links": [
{ "label": "pfSense health", "type": "bucket", "bucket_prefix": "aw-pfsense-health_" },
{ "label": "pfSense gateways", "type": "bucket", "bucket_prefix": "aw-pfsense-gateways_" },
{ "label": "Все бакеты", "type": "buckets" }
]
}
],
"ungrouped_name": "Прочие хосты"
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
# Copy to /etc/activitywatch/aw-server.env and fill with real values.
AW_SERVER_VERSION=0.13.2
AW_SERVER_DOWNLOAD_URL=https://github.com/ActivityWatch/aw-server-rust/releases/download/v0.13.2/aw-server-rust-linux-x86_64.zip
AW_SERVER_BIND_HOST=0.0.0.0
AW_SERVER_PORT=5600
AW_SERVER_WEBUI_DIR=/opt/activitywatch/webui-ru
AW_SERVER_DATA_DIR=/var/lib/activitywatch
AW_SERVER_LOG_DIR=/var/log/activitywatch
AW_SERVER_USER=activitywatch
AW_SERVER_GROUP=activitywatch
@@ -0,0 +1,18 @@
self.addEventListener("install", function (event) {
self.skipWaiting();
event.waitUntil((async function () {
const keys = await caches.keys();
await Promise.all(keys.map(function (key) { return caches.delete(key); }));
})());
});
self.addEventListener("activate", function (event) {
event.waitUntil((async function () {
const keys = await caches.keys();
await Promise.all(keys.map(function (key) { return caches.delete(key); }));
await self.clients.claim();
await self.registration.unregister();
})());
});
self.addEventListener("fetch", function () {});
@@ -0,0 +1,117 @@
#!/bin/bash
set -euo pipefail
ENV_FILE="/etc/activitywatch/aw-server.env"
if [[ ! -f "$ENV_FILE" ]]; then
echo "missing env file: $ENV_FILE" >&2
exit 1
fi
source "$ENV_FILE"
required_vars=(
AW_SERVER_VERSION
AW_SERVER_DOWNLOAD_URL
AW_SERVER_BIND_HOST
AW_SERVER_PORT
AW_SERVER_WEBUI_DIR
AW_SERVER_DATA_DIR
AW_SERVER_LOG_DIR
AW_SERVER_USER
AW_SERVER_GROUP
)
BOOTSTRAP_DIR="/root/bootstrap"
VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
for var_name in "${required_vars[@]}"; do
if [[ -z "${!var_name:-}" ]]; then
echo "missing required variable: $var_name" >&2
exit 1
fi
done
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y curl ca-certificates unzip jq
if ! getent group "$AW_SERVER_GROUP" >/dev/null; then
groupadd --system "$AW_SERVER_GROUP"
fi
if ! id "$AW_SERVER_USER" >/dev/null 2>&1; then
useradd --system --gid "$AW_SERVER_GROUP" --home-dir "$AW_SERVER_DATA_DIR" --shell /usr/sbin/nologin "$AW_SERVER_USER"
fi
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" /opt/activitywatch/bin
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" /opt/activitywatch/releases
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_WEBUI_DIR"
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_DATA_DIR"
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$AW_SERVER_LOG_DIR"
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
curl -fL "$AW_SERVER_DOWNLOAD_URL" -o "$tmp_dir/aw-server.zip"
unzip -q "$tmp_dir/aw-server.zip" -d "$tmp_dir/unpacked"
server_bin=$(find "$tmp_dir/unpacked" -type f \( -name 'aw-server-rust' -o -name 'aw-server' \) | head -n 1)
webui_dir=$(find "$tmp_dir/unpacked" -type d \( -name 'webui' -o -name 'aw-webui' \) | head -n 1 || true)
if [[ -z "$server_bin" || ! -f "$server_bin" ]]; then
echo "aw-server binary not found in archive" >&2
exit 1
fi
release_dir="/opt/activitywatch/releases/aw-server-rust-v${AW_SERVER_VERSION}"
rm -rf "$release_dir"
install -d -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$release_dir"
install -m 0755 -o "$AW_SERVER_USER" -g "$AW_SERVER_GROUP" "$server_bin" "$release_dir/aw-server-rust"
ln -sfn "$release_dir/aw-server-rust" /opt/activitywatch/bin/aw-server-rust
if [[ -n "$webui_dir" && -d "$webui_dir" ]]; then
rm -rf "$AW_SERVER_WEBUI_DIR"
mkdir -p "$AW_SERVER_WEBUI_DIR"
cp -a "$webui_dir"/. "$AW_SERVER_WEBUI_DIR"/
chown -R "$AW_SERVER_USER:$AW_SERVER_GROUP" "$AW_SERVER_WEBUI_DIR"
fi
sed \
-e "s#__AW_SERVER_USER__#$AW_SERVER_USER#g" \
-e "s#__AW_SERVER_GROUP__#$AW_SERVER_GROUP#g" \
-e "s#__AW_SERVER_DATA_DIR__#$AW_SERVER_DATA_DIR#g" \
/root/bootstrap/activitywatch-server.service > /etc/systemd/system/activitywatch-server.service
chmod 0644 /etc/systemd/system/activitywatch-server.service
systemctl daemon-reload
systemctl enable activitywatch-server.service
systemctl restart activitywatch-server.service
systemctl --no-pager --full status activitywatch-server.service || true
for _ in $(seq 1 20); do
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
break
fi
sleep 2
done
if [[ -f "$CLASSES_JSON" ]]; then
curl -fsS -X POST \
-H 'Content-Type: application/json' \
--data-binary @"$CLASSES_JSON" \
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/classes" >/dev/null
echo "Applied worktime classes from $CLASSES_JSON"
else
echo "Worktime classes bootstrap not found, skipped: $CLASSES_JSON"
fi
if [[ -f "$VIEWS_JSON" ]]; then
curl -fsS -X POST \
-H 'Content-Type: application/json' \
--data-binary @"$VIEWS_JSON" \
"http://127.0.0.1:${AW_SERVER_PORT}/api/0/settings/views" >/dev/null
echo "Applied baseline views from $VIEWS_JSON"
else
echo "Views bootstrap not found, skipped: $VIEWS_JSON"
fi
@@ -0,0 +1,90 @@
[
{
"id": 0,
"name": ["Работа"],
"rule": { "type": "none" },
"data": {}
},
{
"id": 1,
"name": ["Работа", "1С"],
"rule": {
"type": "regex",
"regex": "\\b(1cv8s?|1cv8c|1cestart)\\.exe\\b|1С:Предприятие|Запуск 1С:Предприятия|Загрузка конфигурационной информации|Доступ к информационной базе",
"ignore_case": true
},
"data": { "color": "#194D33" }
},
{
"id": 2,
"name": ["Работа", "Документы"],
"rule": {
"type": "regex",
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64)\\.exe\\b|Adobe Reader|Acrobat",
"ignore_case": true
},
"data": { "color": "#2E7D32" }
},
{
"id": 3,
"name": ["Работа", "Коммуникации"],
"rule": {
"type": "regex",
"regex": "\\b(teams|telegram|slack|thunderbird|zoom|skype|whatsapp|viber|discord)\\.exe\\b|Mattermost|Element|Riot",
"ignore_case": true
},
"data": { "color": "#1E88E5" }
},
{
"id": 4,
"name": ["Работа", "Администрирование"],
"rule": {
"type": "regex",
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd)\\.exe\\b",
"ignore_case": true
},
"data": { "color": "#6D4C41" }
},
{
"id": 5,
"name": ["Интернет"],
"rule": { "type": "none" },
"data": {}
},
{
"id": 6,
"name": ["Интернет", "Браузер"],
"rule": {
"type": "regex",
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser)\\.exe\\b",
"ignore_case": true
},
"data": { "color": "#00897B" }
},
{
"id": 7,
"name": ["Система"],
"rule": { "type": "none" },
"data": {}
},
{
"id": 8,
"name": ["Система", "Windows"],
"rule": {
"type": "regex",
"regex": "\\b(SearchHost|explorer|ShellExperienceHost|ApplicationFrameHost|RuntimeBroker|sihost|dwm|svchost|fontdrvhost|userinit)\\.exe\\b|\\\\Windows\\\\System32",
"ignore_case": true
},
"data": { "color": "#607D8B" }
},
{
"id": 9,
"name": ["ActivityWatch"],
"rule": {
"type": "regex",
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b",
"ignore_case": true
},
"data": {}
}
]
@@ -0,0 +1,39 @@
[
{
"id": "summary",
"name": "Summary",
"elements": [
{ "type": "top_titles", "size": 3 },
{ "type": "timeline_barchart", "size": 3 },
{ "type": "top_categories", "size": 3 },
{ "type": "category_tree", "size": 3 }
]
},
{
"id": "window",
"name": "Window",
"elements": [
{ "type": "top_apps", "size": 3, "props": {} }
]
},
{
"id": "DLP",
"name": "DLP",
"elements": []
},
{
"id": "worktime",
"name": "Worktime",
"elements": [
{ "type": "top_categories", "size": 3, "props": {} },
{ "type": "timeline_barchart", "size": 3, "props": {} },
{ "type": "category_tree", "size": 3, "props": {} },
{ "type": "top_apps", "size": 3, "props": {} }
]
},
{
"id": "pve_audit",
"name": "PVE Audit",
"elements": []
}
]
@@ -0,0 +1,57 @@
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:14:21.9184268Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\AWatch-rus\\bin",
"stateRoot": "C:\\ProgramData\\AWatch-rus",
"logsRoot": "C:\\ProgramData\\AWatch-rus\\logs",
"collectorScript": "C:\\ProgramData\\AWatch-rus\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\AWatch-rus\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\AWatch-rus\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\AWatch-rus\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\AWatch-rus\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\AWatch-rus\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\Администратор",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_РђРґРјРёРЅРёСЃС_СЂР_С_РѕСЂ]"
}
]
}
@@ -0,0 +1,69 @@
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:09:42.4193209Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\AWatch-rus\\bin",
"stateRoot": "C:\\ProgramData\\AWatch-rus",
"logsRoot": "C:\\ProgramData\\AWatch-rus\\logs",
"collectorScript": "C:\\ProgramData\\AWatch-rus\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\AWatch-rus\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\AWatch-rus\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\AWatch-rus\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\AWatch-rus\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\AWatch-rus\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\user2",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user2]"
},
{
"UserId": "SHARKON2025\\user3",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user3]"
},
{
"UserId": "SHARKON2025\\user4",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user4]"
},
{
"UserId": "SHARKON2025\\user5",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user5]"
}
]
}
@@ -0,0 +1,57 @@
{
"version": 1,
"generatedAtUtc": "2026-04-27T01:09:38.9519788Z",
"server": {
"host": "10.10.10.13",
"port": 5600,
"scheme": "http"
},
"paths": {
"installRoot": "C:\\Program Files\\AWatch-rus\\bin",
"stateRoot": "C:\\ProgramData\\AWatch-rus",
"logsRoot": "C:\\ProgramData\\AWatch-rus\\logs",
"collectorScript": "C:\\ProgramData\\AWatch-rus\\browser-domains-native-collector.ps1",
"endpointCollectorScript": "C:\\ProgramData\\AWatch-rus\\dlp-endpoint-signals-collector.ps1",
"rulesPath": "C:\\ProgramData\\AWatch-rus\\web-category-rules.json",
"policyPath": "C:\\ProgramData\\AWatch-rus\\dlp-policy.json",
"launchScript": "C:\\ProgramData\\AWatch-rus\\launch-watchers.ps1",
"recoveryScript": "C:\\ProgramData\\AWatch-rus\\recovery-loop.ps1"
},
"collector": {
"pollSeconds": 5,
"pulseSeconds": 30
},
"collectors": {
"afkEnabled": true,
"windowEnabled": true
},
"logging": {
"localAgentLogsEnabled": false
},
"incidentCapture": {
"enabled": true,
"screenshotEnabled": true,
"artifactsRoot": "C:\\ProgramData\\AWatch-rus\\incident-artifacts"
},
"sessionEvents": {
"logonEnabled": true,
"bucketPrefix": "aw-session-events"
},
"recovery": {
"intervalSeconds": 180,
"taskName": "ActivityWatch Recovery"
},
"dlp": {
"incidentBucketPrefix": "aw-dlp-incidents",
"enabled": true
},
"package": {
"version": "v0.13.2"
},
"userTasks": [
{
"UserId": "SHARKON2025\\user1",
"LaunchTaskName": "ActivityWatch Launch [SHARKON2025_user1]"
}
]
}
@@ -0,0 +1,25 @@
@{
RootModule = 'ActivityWatch.Windows.Common.psm1'
ModuleVersion = '1.0.0'
GUID = '90b3fcf6-df9f-4f9b-9ee0-8a7de4dc0ee2'
Author = 'igor04091968'
CompanyName = 'Private'
Description = 'Common PowerShell functions for ActivityWatch Windows deployment, hardening and recovery.'
PowerShellVersion = '5.1'
FunctionsToExport = @(
'*-ActivityWatch*',
'Assert-Administrator',
'Normalize-ActivityWatchUsers',
'Get-ActivityWatchPackageUrl',
'Remove-LegacyActivityWatchEntries'
)
CmdletsToExport = @()
VariablesToExport = '*'
AliasesToExport = @()
PrivateData = @{
PSData = @{
Tags = @('ActivityWatch', 'Windows', 'Deployment', 'Recovery')
ProjectUri = 'https://github.com/igor04091968/AWatch-rus'
}
}
}
@@ -0,0 +1,992 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Assert-Administrator {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]::new($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'Запустите этот скрипт из PowerShell с правами администратора.'
}
}
function New-ActivityWatchDirectory {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path)) {
New-Item -Path $Path -ItemType Directory -Force | Out-Null
}
}
function Get-ActivityWatchPackageUrl {
param(
[string]$Version = 'v0.13.2'
)
return "https://github.com/ActivityWatch/activitywatch/releases/download/$Version/activitywatch-$Version-windows-x86_64.zip"
}
function Get-ActivityWatchArchive {
param(
[string]$PackageZipPath,
[string]$PackageUrl,
[string]$Version = 'v0.13.2',
[Parameter(Mandatory = $true)]
[string]$WorkingRoot
)
New-ActivityWatchDirectory -Path $WorkingRoot
if ($PackageZipPath) {
$resolved = Resolve-Path -LiteralPath $PackageZipPath -ErrorAction Stop
return $resolved.Path
}
if (-not $PackageUrl) {
$PackageUrl = Get-ActivityWatchPackageUrl -Version $Version
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$archivePath = Join-Path $WorkingRoot ("activitywatch-{0}.zip" -f $Version.TrimStart('v'))
Invoke-WebRequest -Uri $PackageUrl -OutFile $archivePath
return $archivePath
}
function Get-ActivityWatchPackageRoot {
param(
[Parameter(Mandatory = $true)]
[string]$ExpandedRoot
)
$afkBinary = Get-ChildItem -Path $ExpandedRoot -Filter 'aw-watcher-afk.exe' -File -Recurse |
Select-Object -First 1
if (-not $afkBinary) {
throw "Не удалось найти aw-watcher-afk.exe в $ExpandedRoot."
}
return (Split-Path -Path (Split-Path -Path $afkBinary.FullName -Parent) -Parent)
}
function Install-ActivityWatchPackage {
param(
[Parameter(Mandatory = $true)]
[string]$ArchivePath,
[Parameter(Mandatory = $true)]
[string]$InstallRoot,
[Parameter(Mandatory = $true)]
[string]$WorkingRoot,
[Parameter(Mandatory = $true)]
[string]$BackupRoot
)
New-ActivityWatchDirectory -Path $WorkingRoot
New-ActivityWatchDirectory -Path $BackupRoot
$extractRoot = Join-Path $WorkingRoot ('extract-' + [guid]::NewGuid().Guid)
if (Test-Path -LiteralPath $extractRoot) {
Remove-Item -LiteralPath $extractRoot -Recurse -Force
}
New-ActivityWatchDirectory -Path $extractRoot
Expand-Archive -Path $ArchivePath -DestinationPath $extractRoot -Force
$packageRoot = Get-ActivityWatchPackageRoot -ExpandedRoot $extractRoot
if (Test-Path -LiteralPath $InstallRoot) {
$existingItems = Get-ChildItem -LiteralPath $InstallRoot -Force -ErrorAction SilentlyContinue
if ($existingItems) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$backupPath = Join-Path $BackupRoot ("install-$stamp")
New-ActivityWatchDirectory -Path $backupPath
Copy-Item -Path (Join-Path $InstallRoot '*') -Destination $backupPath -Recurse -Force
Get-ChildItem -LiteralPath $InstallRoot -Force | Remove-Item -Recurse -Force
}
}
else {
New-ActivityWatchDirectory -Path $InstallRoot
}
Copy-Item -Path (Join-Path $packageRoot '*') -Destination $InstallRoot -Recurse -Force
return [pscustomobject]@{
PackageRoot = $packageRoot
ExtractRoot = $extractRoot
BackupRoot = $BackupRoot
}
}
function Get-ActivityWatchExecutableMap {
param(
[Parameter(Mandatory = $true)]
[string]$InstallRoot
)
$map = [ordered]@{
Afk = Join-Path $InstallRoot 'aw-watcher-afk\aw-watcher-afk.exe'
Window = Join-Path $InstallRoot 'aw-watcher-window\aw-watcher-window.exe'
}
foreach ($entry in $map.GetEnumerator()) {
if (-not (Test-Path -LiteralPath $entry.Value)) {
throw "Не найден обязательный исполняемый файл ActivityWatch: $($entry.Value)"
}
}
return [pscustomobject]$map
}
function Normalize-ActivityWatchUsers {
param(
[string[]]$Users,
[string]$UserListPath,
[string]$Domain
)
$collected = New-Object System.Collections.Generic.List[string]
if ($Users) {
foreach ($user in $Users) {
if (-not [string]::IsNullOrWhiteSpace($user)) {
$collected.Add($user.Trim())
}
}
}
if ($UserListPath) {
$resolved = Resolve-Path -LiteralPath $UserListPath -ErrorAction Stop
$extension = [IO.Path]::GetExtension($resolved.Path)
if ($extension -ieq '.csv') {
$rows = Import-Csv -LiteralPath $resolved.Path
foreach ($row in $rows) {
foreach ($column in 'User', 'Username', 'SamAccountName', 'Login') {
if ($row.PSObject.Properties.Name -contains $column) {
$value = [string]$row.$column
if (-not [string]::IsNullOrWhiteSpace($value)) {
$collected.Add($value.Trim())
break
}
}
}
}
}
else {
Get-Content -LiteralPath $resolved.Path | ForEach-Object {
$line = $_.Trim()
if ($line -and -not $line.StartsWith('#')) {
$collected.Add($line)
}
}
}
}
$normalized = $collected |
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
ForEach-Object {
if ($Domain -and ($_ -notmatch '[\\@]')) {
'{0}\{1}' -f $Domain, $_
}
else {
$_
}
} |
Sort-Object -Unique
if (-not $normalized -or $normalized.Count -eq 0) {
throw 'Не удалось определить целевых пользователей. Укажите -Users или -UserListPath.'
}
return @($normalized)
}
function Get-ActivityWatchTaskNameToken {
param(
[Parameter(Mandatory = $true)]
[string]$UserId
)
$buffer = [Text.StringBuilder]::new()
foreach ($character in $UserId.ToCharArray()) {
if ([char]::IsLetterOrDigit($character)) {
[void]$buffer.Append($character)
}
else {
[void]$buffer.Append('_')
}
}
return $buffer.ToString().Trim('_')
}
function New-ActivityWatchUserTaskDefinitions {
param(
[Parameter(Mandatory = $true)]
[string[]]$Users
)
$result = foreach ($user in $Users) {
$token = Get-ActivityWatchTaskNameToken -UserId $user
[pscustomobject]@{
UserId = $user
LaunchTaskName = "ActivityWatch Launch [$token]"
}
}
return @($result)
}
function Copy-ActivityWatchCollectorAssets {
param(
[Parameter(Mandatory = $true)]
[string]$CollectorScriptSource,
[Parameter(Mandatory = $true)]
[string]$EndpointCollectorScriptSource,
[Parameter(Mandatory = $true)]
[string]$SessionCollectorScriptSource,
[Parameter(Mandatory = $true)]
[string]$ExampleRulesSource,
[Parameter(Mandatory = $true)]
[string]$ExamplePolicySource,
[Parameter(Mandatory = $true)]
[string]$StateRoot,
[string]$CustomRulesSource,
[string]$CustomPolicySource
)
New-ActivityWatchDirectory -Path $StateRoot
$collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
$endpointCollectorTarget = Join-Path $StateRoot 'dlp-endpoint-signals-collector.ps1'
$sessionCollectorTarget = Join-Path $StateRoot 'worktime-session-collector.ps1'
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
$examplePolicyTarget = Join-Path $StateRoot 'dlp-policy.example.json'
$policyTarget = Join-Path $StateRoot 'dlp-policy.json'
Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force
Copy-Item -LiteralPath $EndpointCollectorScriptSource -Destination $endpointCollectorTarget -Force
Copy-Item -LiteralPath $SessionCollectorScriptSource -Destination $sessionCollectorTarget -Force
Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force
Copy-Item -LiteralPath $ExamplePolicySource -Destination $examplePolicyTarget -Force
if ($CustomRulesSource) {
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
}
if ($CustomPolicySource) {
$resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop
Copy-Item -LiteralPath $resolvedPolicy.Path -Destination $policyTarget -Force
}
else {
Copy-Item -LiteralPath $examplePolicyTarget -Destination $policyTarget -Force
}
return [pscustomobject]@{
CollectorScript = $collectorTarget
EndpointCollectorScript = $endpointCollectorTarget
SessionCollectorScript = $sessionCollectorTarget
ExampleRules = $exampleRulesTarget
ActiveRules = $rulesTarget
ExamplePolicy = $examplePolicyTarget
ActivePolicy = $policyTarget
}
}
function New-ActivityWatchDeploymentConfig {
param(
[Parameter(Mandatory = $true)]
[string]$ServerHost,
[Parameter(Mandatory = $true)]
[int]$ServerPort,
[Parameter(Mandatory = $true)]
[string]$ServerScheme,
[Parameter(Mandatory = $true)]
[string]$InstallRoot,
[Parameter(Mandatory = $true)]
[string]$StateRoot,
[Parameter(Mandatory = $true)]
[string]$LogsRoot,
[Parameter(Mandatory = $true)]
[string]$CollectorScript,
[Parameter(Mandatory = $true)]
[string]$EndpointCollectorScript,
[Parameter(Mandatory = $true)]
[string]$SessionCollectorScript,
[Parameter(Mandatory = $true)]
[string]$RulesPath,
[Parameter(Mandatory = $true)]
[string]$PolicyPath,
[Parameter(Mandatory = $true)]
[int]$PollSeconds,
[Parameter(Mandatory = $true)]
[int]$PulseSeconds,
[Parameter(Mandatory = $true)]
[int]$RecoveryIntervalSeconds,
[bool]$AfkEnabled = $true,
[bool]$WindowEnabled = $true,
[bool]$LocalAgentLogsEnabled = $true,
[bool]$IncidentCaptureEnabled = $true,
[bool]$IncidentScreenshotEnabled = $true,
[string]$IncidentArtifactsRoot,
[bool]$LogonMarkerEnabled = $true,
[Parameter(Mandatory = $true)]
[string]$LaunchScriptPath,
[Parameter(Mandatory = $true)]
[string]$RecoveryScriptPath,
[Parameter(Mandatory = $true)]
[pscustomobject[]]$UserTasks,
[string]$PackageVersion = 'v0.13.2'
)
$effectiveIncidentArtifactsRoot = if ($IncidentArtifactsRoot) { $IncidentArtifactsRoot } else { Join-Path $StateRoot 'incident-artifacts' }
return [pscustomobject]@{
version = 1
generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
server = [pscustomobject]@{
host = $ServerHost
port = $ServerPort
scheme = $ServerScheme
}
paths = [pscustomobject]@{
installRoot = $InstallRoot
stateRoot = $StateRoot
logsRoot = $LogsRoot
collectorScript = $CollectorScript
endpointCollectorScript = $EndpointCollectorScript
sessionCollectorScript = $SessionCollectorScript
rulesPath = $RulesPath
policyPath = $PolicyPath
launchScript = $LaunchScriptPath
recoveryScript = $RecoveryScriptPath
}
collector = [pscustomobject]@{
pollSeconds = $PollSeconds
pulseSeconds = $PulseSeconds
}
collectors = [pscustomobject]@{
afkEnabled = $AfkEnabled
windowEnabled = $WindowEnabled
}
logging = [pscustomobject]@{
localAgentLogsEnabled = $LocalAgentLogsEnabled
}
incidentCapture = [pscustomobject]@{
enabled = $IncidentCaptureEnabled
screenshotEnabled = $IncidentScreenshotEnabled
artifactsRoot = $effectiveIncidentArtifactsRoot
}
sessionEvents = [pscustomobject]@{
logonEnabled = $LogonMarkerEnabled
bucketPrefix = 'aw-session-events'
}
recovery = [pscustomobject]@{
intervalSeconds = $RecoveryIntervalSeconds
taskName = 'ActivityWatch Recovery'
}
dlp = [pscustomobject]@{
incidentBucketPrefix = 'aw-dlp-incidents'
enabled = $true
}
package = [pscustomobject]@{
version = $PackageVersion
}
userTasks = @($UserTasks)
}
}
function Write-ActivityWatchDeploymentConfig {
param(
[Parameter(Mandatory = $true)]
[pscustomobject]$Config,
[Parameter(Mandatory = $true)]
[string]$Path
)
$directory = Split-Path -Path $Path -Parent
if ($directory) {
New-ActivityWatchDirectory -Path $directory
}
$json = $Config | ConvertTo-Json -Depth 8
Set-Content -LiteralPath $Path -Value $json -Encoding UTF8
}
function Read-ActivityWatchDeploymentConfig {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
if (-not (Test-Path -LiteralPath $Path)) {
throw "Конфигурация развёртывания не найдена: $Path"
}
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
}
function Write-ActivityWatchLaunchScript {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$ConfigPath
)
$content = @"
param(
[string]`$ConfigPath = '$ConfigPath'
)
Set-StrictMode -Version Latest
`$ErrorActionPreference = 'Stop'
function Get-DeploymentConfig {
param([string]`$Path)
return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json
}
function Test-ProcessInSession {
param(
[string]`$Name,
[int]`$SessionId
)
return [bool](Get-Process -Name `$Name -ErrorAction SilentlyContinue | Where-Object { `$_.SessionId -eq `$SessionId } | Select-Object -First 1)
}
function Test-CollectorRunning {
param(
[string]`$ScriptPath,
[int]`$SessionId
)
`$escapedCollector = [Regex]::Escape(`$ScriptPath)
`$processes = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
Where-Object {
(`$_.Name -ieq 'powershell.exe' -or `$_.Name -ieq 'pwsh.exe') -and
`$_.SessionId -eq `$SessionId -and
`$_.CommandLine -match `$escapedCollector
}
return [bool](`$processes | Select-Object -First 1)
}
function Invoke-AwJsonPost {
param(
[Parameter(Mandatory = `$true)][string]`$Uri,
[Parameter(Mandatory = `$true)][string]`$Json
)
`$bytes = [Text.Encoding]::UTF8.GetBytes(`$Json)
Invoke-RestMethod -Method Post -Uri `$Uri -ContentType 'application/json; charset=utf-8' -Body `$bytes | Out-Null
}
function Ensure-Bucket {
param(
[string]`$BucketId,
[string]`$ClientName,
[string]`$BucketType
)
if (`$script:KnownBuckets.ContainsKey(`$BucketId)) {
return
}
try {
Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null
`$script:KnownBuckets[`$BucketId] = `$true
return
}
catch {
}
`$body = @{
client = `$ClientName
type = `$BucketType
hostname = `$script:Hostname
} | ConvertTo-Json -Compress
try {
Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" -Json `$body
}
catch {
try {
Invoke-RestMethod -Method Get -Uri "`$(`$script:ApiBase)/buckets/`$BucketId" | Out-Null
}
catch {
return
}
}
`$script:KnownBuckets[`$BucketId] = `$true
}
function Send-LogonMarkerIfNeeded {
param(
[pscustomobject]`$Config,
[int]`$SessionId
)
`$sessionEvents = if (`$Config.PSObject.Properties.Name -contains 'sessionEvents') { `$Config.sessionEvents } else { `$null }
`$logging = if (`$Config.PSObject.Properties.Name -contains 'logging') { `$Config.logging } else { `$null }
`$logonEnabled = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'logonEnabled') { [bool]`$sessionEvents.logonEnabled } else { `$false }
if (-not `$logonEnabled) {
return
}
`$bucketPrefix = if (`$sessionEvents -and `$sessionEvents.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]`$sessionEvents.bucketPrefix)) {
[string]`$sessionEvents.bucketPrefix
}
else {
'aw-session-events'
}
`$stateRoot = [string]`$Config.paths.stateRoot
`$markerRoots = New-Object System.Collections.Generic.List[string]
if (-not [string]::IsNullOrWhiteSpace(`$env:LOCALAPPDATA)) {
`$markerRoots.Add((Join-Path `$env:LOCALAPPDATA 'AWatch-rus\markers'))
}
if (-not [string]::IsNullOrWhiteSpace(`$stateRoot)) {
`$markerRoots.Add((Join-Path `$stateRoot 'markers'))
}
`$markerDir = `$null
foreach (`$candidate in `$markerRoots) {
try {
if (-not (Test-Path -LiteralPath `$candidate)) {
New-Item -Path `$candidate -ItemType Directory -Force | Out-Null
}
`$probePath = Join-Path `$candidate 'write-test.tmp'
Set-Content -LiteralPath `$probePath -Value 'ok' -Encoding ASCII
Remove-Item -LiteralPath `$probePath -Force -ErrorAction SilentlyContinue
`$markerDir = `$candidate
break
}
catch {
}
}
if (-not `$markerDir) {
return
}
`$markerFile = Join-Path `$markerDir ("logon-{0}-{1}.marker" -f `$env:USERNAME, `$SessionId)
if (Test-Path -LiteralPath `$markerFile) {
return
}
Set-Content -LiteralPath `$markerFile -Value ((Get-Date).ToUniversalTime().ToString('o')) -Encoding UTF8
`$bucketId = ('{0}_{1}' -f `$bucketPrefix, `$script:Hostname)
Ensure-Bucket -BucketId `$bucketId -ClientName 'aw-session-events' -BucketType 'aw.session.event'
`$payload = @{
timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
duration = 0
data = @{
eventType = 'logon'
username = `$env:USERNAME
userId = "`$(`$env:USERDOMAIN)\`$(`$env:USERNAME)"
sessionId = `$SessionId
hostname = `$script:Hostname
source = 'launch-watchers-awatch-rus'
}
} | ConvertTo-Json -Depth 5 -Compress
try {
Invoke-AwJsonPost -Uri "`$(`$script:ApiBase)/buckets/`$bucketId/heartbeat?pulsetime=1" -Json `$payload
}
catch {
Remove-Item -LiteralPath `$markerFile -Force -ErrorAction SilentlyContinue
throw
}
}
function Start-CollectorScriptIfNeeded {
param(
[string]`$ScriptPath,
[string]`$ConfigPath,
[string]`$PowerShellExe,
[int]`$SessionId
)
if (-not (Test-Path -LiteralPath `$ScriptPath)) {
return
}
if (Test-CollectorRunning -ScriptPath `$ScriptPath -SessionId `$SessionId) {
return
}
Start-Process -FilePath `$PowerShellExe -ArgumentList @(
'-NoProfile',
'-WindowStyle', 'Hidden',
'-ExecutionPolicy', 'Bypass',
'-File', `$ScriptPath,
'-ConfigPath', `$ConfigPath
) -WindowStyle Hidden
}
`$config = Get-DeploymentConfig -Path `$ConfigPath
`$sessionId = (Get-Process -Id `$PID).SessionId
`$installRoot = [string]`$config.paths.installRoot
`$script:ApiBase = '{0}://{1}:{2}/api/0' -f [string]`$config.server.scheme, [string]`$config.server.host, [string]`$config.server.port
`$script:Hostname = `$env:COMPUTERNAME
`$script:KnownBuckets = @{}
`$collectorScript = [string]`$config.paths.collectorScript
`$endpointCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]`$config.paths.endpointCollectorScript } else { '' }
`$sessionCollectorScript = if (`$config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]`$config.paths.sessionCollectorScript } else { '' }
`$afkExe = Join-Path `$installRoot 'aw-watcher-afk\aw-watcher-afk.exe'
`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe'
`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port)
`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true }
`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true }
if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) {
throw "Не найден aw-watcher-afk.exe: `$afkExe"
}
if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) {
throw "Не найден aw-watcher-window.exe: `$windowExe"
}
if (`$afkEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) {
Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden
}
if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) {
Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden
}
try {
Send-LogonMarkerIfNeeded -Config `$config -SessionId `$sessionId
}
catch {
}
Start-CollectorScriptIfNeeded -ScriptPath `$collectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
Start-CollectorScriptIfNeeded -ScriptPath `$endpointCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
Start-CollectorScriptIfNeeded -ScriptPath `$sessionCollectorScript -ConfigPath `$ConfigPath -PowerShellExe `$powershellExe -SessionId `$sessionId
"@
Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
}
function Write-ActivityWatchRecoveryScript {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$ConfigPath
)
$content = @"
param(
[string]`$ConfigPath = '$ConfigPath'
)
Set-StrictMode -Version Latest
`$ErrorActionPreference = 'Continue'
function Get-DeploymentConfig {
param([string]`$Path)
return Get-Content -LiteralPath `$Path -Raw | ConvertFrom-Json
}
function Get-RecoveryConfigPaths {
param([string]`$PrimaryConfigPath)
`$paths = New-Object System.Collections.Generic.List[string]
if (`$PrimaryConfigPath -and (Test-Path -LiteralPath `$PrimaryConfigPath)) {
`$paths.Add((Resolve-Path -LiteralPath `$PrimaryConfigPath).Path)
}
`$searchRoot = `$env:ProgramData
if (`$PrimaryConfigPath) {
`$stateRoot = Split-Path -Path `$PrimaryConfigPath -Parent
`$candidateRoot = Split-Path -Path `$stateRoot -Parent
if (`$candidateRoot -and (Test-Path -LiteralPath `$candidateRoot)) {
`$searchRoot = `$candidateRoot
}
}
if (Test-Path -LiteralPath `$searchRoot) {
Get-ChildItem -LiteralPath `$searchRoot -Directory -ErrorAction SilentlyContinue |
Where-Object { `$_.Name -like 'ActivityWatch*' } |
ForEach-Object {
`$candidate = Join-Path `$_.FullName 'deployment-config.json'
if (Test-Path -LiteralPath `$candidate) {
`$paths.Add(`$candidate)
}
}
}
return @(`$paths | Sort-Object -Unique)
}
function Get-RecoveryTaskNames {
param([string[]]`$ConfigPaths)
`$taskNames = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
foreach (`$candidatePath in @(`$ConfigPaths)) {
try {
`$config = Get-DeploymentConfig -Path `$candidatePath
foreach (`$task in @(`$config.userTasks)) {
`$taskName = [string]`$task.launchTaskName
if (-not [string]::IsNullOrWhiteSpace(`$taskName)) {
[void]`$taskNames.Add(`$taskName)
}
}
}
catch {
}
}
return @(`$taskNames)
}
while (`$true) {
`$sleepSeconds = 180
try {
`$configPaths = Get-RecoveryConfigPaths -PrimaryConfigPath `$ConfigPath
foreach (`$taskName in Get-RecoveryTaskNames -ConfigPaths `$configPaths) {
Start-ScheduledTask -TaskName `$taskName -ErrorAction SilentlyContinue
}
`$config = Get-DeploymentConfig -Path `$ConfigPath
if (`$config -and `$config.recovery -and `$config.recovery.intervalSeconds) {
`$sleepSeconds = [Math]::Max([int]`$config.recovery.intervalSeconds, 30)
}
}
catch {
}
Start-Sleep -Seconds `$sleepSeconds
}
"@
Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
}
function Get-ActivityWatchHiddenLauncherPath {
param(
[Parameter(Mandatory = $true)]
[string]$ScriptPath
)
$directory = Split-Path -Path $ScriptPath -Parent
$baseName = [IO.Path]::GetFileNameWithoutExtension($ScriptPath)
return Join-Path $directory ("{0}-hidden.vbs" -f $baseName)
}
function Write-ActivityWatchHiddenPowerShellWrapper {
param(
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$ScriptPath,
[Parameter(Mandatory = $true)]
[string]$ConfigPath
)
$directory = Split-Path -Path $Path -Parent
if ($directory) {
New-ActivityWatchDirectory -Path $directory
}
$powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
$escapedPowerShellExe = $powershellExe.Replace('"', '""')
$escapedScriptPath = $ScriptPath.Replace('"', '""')
$escapedConfigPath = $ConfigPath.Replace('"', '""')
$content = @"
Set shell = CreateObject("WScript.Shell")
shell.Run """$escapedPowerShellExe"" -NoProfile -ExecutionPolicy Bypass -File ""$escapedScriptPath"" -ConfigPath ""$escapedConfigPath""", 0, False
"@
Set-Content -LiteralPath $Path -Value $content -Encoding ASCII
}
function Remove-LegacyActivityWatchEntries {
$legacyTaskNames = @(
'ActivityWatch Watchers',
'ActivityWatch Guard',
'ActivityWatch Heal'
)
foreach ($taskName in $legacyTaskNames) {
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
}
$runKey = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run'
foreach ($name in 'ActivityWatchAFK', 'ActivityWatchWindow', 'ActivityWatchBrowserCollector') {
Remove-ItemProperty -Path $runKey -Name $name -ErrorAction SilentlyContinue
}
}
function Remove-ActivityWatchScheduledTask {
param(
[Parameter(Mandatory = $true)]
[string]$TaskName
)
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
& cmd.exe /c "schtasks /Delete /TN `"$TaskName`" /F >nul 2>&1" | Out-Null
for ($attempt = 0; $attempt -lt 10; $attempt++) {
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if (-not $task) {
return
}
Start-Sleep -Milliseconds 300
}
}
function Set-ActivityWatchScheduledTaskAction {
param(
[Parameter(Mandatory = $true)]
[string]$TaskName,
[Parameter(Mandatory = $true)]
[string]$Execute,
[Parameter(Mandatory = $true)]
[string]$Arguments
)
$taskCommand = ('"{0}" {1}' -f $Execute, $Arguments)
& schtasks.exe /Change /TN $TaskName /TR $taskCommand | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "schtasks.exe /Change завершился с ошибкой для $TaskName"
}
}
function Get-ActivityWatchScheduledTaskByCommand {
param(
[Parameter(Mandatory = $true)]
[string]$TaskName,
[string]$CommandMatch
)
$task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($task) {
return $task
}
if ([string]::IsNullOrWhiteSpace($CommandMatch)) {
return $null
}
foreach ($candidate in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch*' })) {
foreach ($action in @($candidate.Actions)) {
if ([string]$action.Arguments -like "*$CommandMatch*") {
return $candidate
}
}
}
return $null
}
function Register-ActivityWatchUserTasks {
param(
[Parameter(Mandatory = $true)]
[pscustomobject[]]$TaskDefinitions,
[Parameter(Mandatory = $true)]
[string]$LaunchScriptPath,
[Parameter(Mandatory = $true)]
[string]$ConfigPath
)
$wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe'
$launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $LaunchScriptPath
Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $LaunchScriptPath -ConfigPath $ConfigPath
foreach ($definition in $TaskDefinitions) {
$action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`""
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId
$principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
$existingTask = Get-ActivityWatchScheduledTaskByCommand -TaskName $definition.LaunchTaskName -CommandMatch $ConfigPath
if ($existingTask) {
Set-ActivityWatchScheduledTaskAction -TaskName $existingTask.TaskName -Execute $wscriptExe -Arguments $action.Arguments
continue
}
Remove-ActivityWatchScheduledTask -TaskName $definition.LaunchTaskName
Register-ScheduledTask -TaskName $definition.LaunchTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
}
}
function Register-ActivityWatchRecoveryTask {
param(
[Parameter(Mandatory = $true)]
[string]$TaskName,
[Parameter(Mandatory = $true)]
[string]$RecoveryScriptPath,
[Parameter(Mandatory = $true)]
[string]$ConfigPath
)
Remove-ActivityWatchScheduledTask -TaskName $TaskName
$wscriptExe = Join-Path $env:SystemRoot 'System32\wscript.exe'
$launcherPath = Get-ActivityWatchHiddenLauncherPath -ScriptPath $RecoveryScriptPath
Write-ActivityWatchHiddenPowerShellWrapper -Path $launcherPath -ScriptPath $RecoveryScriptPath -ConfigPath $ConfigPath
$action = New-ScheduledTaskAction -Execute $wscriptExe -Argument "//B //NoLogo `"$launcherPath`""
$trigger = New-ScheduledTaskTrigger -AtStartup
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -Hidden -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
}
function Set-ActivityWatchAcl {
param(
[Parameter(Mandatory = $true)]
[string]$InstallRoot,
[Parameter(Mandatory = $true)]
[string]$StateRoot,
[Parameter(Mandatory = $true)]
[string]$LogsRoot
)
foreach ($path in $InstallRoot, $StateRoot, $LogsRoot) {
New-ActivityWatchDirectory -Path $path
}
& icacls $InstallRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls завершился с ошибкой для $InstallRoot"
}
& icacls $StateRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(RX)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls завершился с ошибкой для $StateRoot"
}
& icacls $LogsRoot /inheritance:r /grant:r '*S-1-5-18:(OI)(CI)(F)' '*S-1-5-32-544:(OI)(CI)(F)' '*S-1-5-32-545:(OI)(CI)(M)' | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "icacls завершился с ошибкой для $LogsRoot"
}
}
function Start-ActivityWatchTasks {
param(
[Parameter(Mandatory = $true)]
[pscustomobject[]]$TaskDefinitions,
[string]$RecoveryTaskName = 'ActivityWatch Recovery'
)
foreach ($definition in $TaskDefinitions) {
Start-ScheduledTask -TaskName $definition.LaunchTaskName -ErrorAction SilentlyContinue
}
Start-ScheduledTask -TaskName $RecoveryTaskName -ErrorAction SilentlyContinue
}
Export-ModuleMember -Function *-ActivityWatch*, Assert-Administrator, Normalize-ActivityWatchUsers, Get-ActivityWatchPackageUrl, Remove-LegacyActivityWatchEntries

Some files were not shown because too many files have changed in this diff Show More