diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..63ade56 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @igor04091968 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..a12b8c6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Summary + +- what changed +- why it changed +- risk and rollback notes + +## Checklist + +- [ ] No real secrets or credentials committed +- [ ] Server-side scripts validated (`bash -n`) +- [ ] PowerShell scripts validated (`Invoke-ScriptAnalyzer`) +- [ ] Docs updated (full paths and runbook steps) +- [ ] Rollback steps documented diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cc1770e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: shell-and-powershell-ci + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + shell-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install shellcheck + run: sudo apt-get update && sudo apt-get install -y shellcheck + + - name: Run shellcheck + run: | + find . -type f -name "*.sh" -print0 | xargs -0 -r shellcheck + + powershell-analyzer: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install PSScriptAnalyzer + shell: pwsh + run: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module PSScriptAnalyzer -Scope CurrentUser -Force + + - name: Analyze PowerShell scripts + shell: pwsh + run: | + $targets = @( + "windows/*.ps1", + "windows/*.psm1", + "windows/*.psd1" + ) + $issues = Invoke-ScriptAnalyzer -Path $targets -Recurse -Severity Error,Warning + if ($issues) { + $issues | Format-Table -AutoSize + throw "PSScriptAnalyzer detected issues." + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..651cf56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Local secrets +secrets/deploy.secrets.env + +# Runtime / reports +*.log +*.tmp +*.bak +windows/*.report.json + +# IDE +.idea/ +.vscode/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1bf4aba --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,27 @@ +# Contributing + +## Branching + +- Работайте в feature-ветке, не пушьте напрямую в `main`. +- Именование: `feat/...`, `fix/...`, `docs/...`, `chore/...`. + +## Commit style + +- Предпочтительно Conventional Commits: + - `feat(...)` + - `fix(...)` + - `docs(...)` + - `chore(...)` + +## Required checks before PR + +- `bash -n` для всех `*.sh`. +- `Invoke-ScriptAnalyzer` для `windows/*.ps1`, `windows/*.psm1`, `windows/*.psd1`. +- Проверка, что нет секретов (`secrets/deploy.secrets.env` не должен быть в индексе git). +- Обновлены инструкции и runbook при изменении поведения. + +## PR content + +- Изменения и обоснование. +- Риск и rollback. +- Какие команды валидации были выполнены. diff --git a/README.md b/README.md index 3b8e312..a83a668 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ActivityWatch-Russian +# AWatch-rus Практический каркас проекта для повторного развёртывания ActivityWatch Server в новом окружении с LXC-контейнером на Proxmox, русифицированным Web UI, systemd-юнитами, шаблонными скриптами деплоя и эксплуатационной документацией. @@ -8,8 +8,11 @@ - `docs/deployment.md` — пошаговый деплой LXC и ActivityWatch Server. - `docs/runbook.md` — быстрый runbook для оператора. - `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback. +- `docs/windows/ensemble.md` — orchestration-пакет для Windows-деплоя и проверки. - `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. ## Базовый сценарий @@ -20,6 +23,8 @@ 5. Внутри контейнера выполнить `aw-server/install_aw_server.sh`. 6. Применить русификацию Web UI через `aw-server/apply_webui_ru_patch.sh`. 7. Проверить API, Web UI и состояние systemd по `docs/runbook.md`. +8. Развернуть Windows-клиентов через `windows/deploy-ensemble.ps1`. +9. Проверить итог через `windows/validate-deployment.ps1`. Скрипты `proxmox/create-ct.sh` и `proxmox/push-aw-artifacts.sh` по умолчанию читают: @@ -42,15 +47,17 @@ ## Ограничения -- Windows-клиенты и watcher-автоматизация в этой части каркаса не описываются. - Интеграции с InfluxDB/Grafana/LDAP оставлены как следующий слой, не как обязательная база. ## Быстрые ссылки - `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/docs/FULL_DEPLOYMENT_MANUAL_RU.md` +- `/home/igor/tmp/AWatch-rus/docs/windows/ensemble.md` - `docs/preparation.md` - `docs/deployment.md` - `docs/runbook.md` - `docs/operations.md` - `proxmox/create-ct.sh` - `aw-server/install_aw_server.sh` +- `windows/deploy-ensemble.ps1` +- `windows/validate-deployment.ps1` diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..f22443a --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,29 @@ +# Ansible ensemble for AWatch-rus + +Эта директория содержит минимальный Ansible-ensemble для повторяемого развёртывания ActivityWatch Server в Debian/LXC. + +## Файлы + +- `/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` — шаблон переменных. + +## Быстрый запуск + +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` +2. Заполните значения в `inventory.ini` и `group_vars/all.yml`. +3. Запустите: + +```bash +cd /home/igor/tmp/AWatch-rus/ansible +ansible-playbook -i inventory.ini deploy_aw_server.yml +``` + +## Результат + +- Установлен ActivityWatch Server. +- Создан systemd-unit `activitywatch-server.service`. +- Установлен RU Web UI patch. +- Выполнена валидация API `http://127.0.0.1:5600/api/0/info`. diff --git a/ansible/deploy_aw_server.yml b/ansible/deploy_aw_server.yml new file mode 100644 index 0000000..e924f6d --- /dev/null +++ b/ansible/deploy_aw_server.yml @@ -0,0 +1,152 @@ +--- +- name: Deploy AWatch-rus server + 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" + + tasks: + - name: Install base packages + ansible.builtin.apt: + name: + - curl + - unzip + state: present + update_cache: true + + - name: Ensure service account exists + 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: Ensure required directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: "{{ aw_server_user }}" + group: "{{ aw_server_group }}" + mode: "0755" + loop: + - "{{ aw_release_root }}" + - "{{ aw_release_dir }}" + - "{{ aw_server_webui_dir }}" + - "{{ aw_server_data_dir }}" + - "{{ 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: Unpack ActivityWatch release + ansible.builtin.unarchive: + src: "{{ aw_archive_path }}" + dest: "{{ aw_release_dir }}" + remote_src: true + extra_opts: ["-o"] + + - name: Discover extracted AW directory + ansible.builtin.find: + paths: "{{ aw_release_dir }}" + file_type: directory + patterns: "activitywatch*" + register: aw_release_find + + - 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: 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: Sync release content to /opt/activitywatch + ansible.builtin.command: + cmd: "rsync -a --delete {{ aw_release_extracted }}/ /opt/activitywatch/" + + - name: Copy bootstrap files from repository + 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" } + notify: + - Reload systemd + - Restart 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: Insert RU patch scripts into index.html + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' + + - name: Insert RU patch loader before body end + ansible.builtin.replace: + path: "{{ aw_server_webui_dir }}/index.html" + regexp: '' + replace: '' + + - 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: Enable and start service + ansible.builtin.systemd: + name: activitywatch-server.service + enabled: true + state: restarted + daemon_reload: true + + - name: Wait for 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 + + handlers: + - name: Reload systemd + ansible.builtin.systemd: + daemon_reload: true + + - name: Restart activitywatch + ansible.builtin.systemd: + name: activitywatch-server.service + state: restarted diff --git a/ansible/group_vars/all.example.yml b/ansible/group_vars/all.example.yml new file mode 100644 index 0000000..e94af15 --- /dev/null +++ b/ansible/group_vars/all.example.yml @@ -0,0 +1,11 @@ +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: "/home/igor/tmp/AWatch-rus" diff --git a/ansible/inventory.example.ini b/ansible/inventory.example.ini new file mode 100644 index 0000000..b0c2d48 --- /dev/null +++ b/ansible/inventory.example.ini @@ -0,0 +1,2 @@ +[aw_server] +aw-ct ansible_host=10.20.30.13 ansible_user=root ansible_port=22 diff --git a/docs/FULL_DEPLOYMENT_MANUAL_RU.md b/docs/FULL_DEPLOYMENT_MANUAL_RU.md index 6263467..c93d34f 100755 --- a/docs/FULL_DEPLOYMENT_MANUAL_RU.md +++ b/docs/FULL_DEPLOYMENT_MANUAL_RU.md @@ -13,8 +13,11 @@ - `/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` --- @@ -154,6 +157,21 @@ C:\Deploy\ActivityWatch-Russian\windows\deploy-domain-users.ps1 ` - `-Users 'CONTOSO\user01','CONTOSO\user02'` - `-UserListPath ` +### 3.2.1 Ensemble orchestration (рекомендуется для production) + +```powershell +C:\Deploy\ActivityWatch-Russian\windows\deploy-ensemble.ps1 ` + -ServerHost aw.example.local ` + -ServerPort 5600 ` + -Domain CONTOSO ` + -Users user1,user2,user3,user4,user5 ` + -ValidateAfterDeploy +``` + +Отчёт сохраняется в: + +- `C:\ProgramData\ActivityWatch\ensemble-report-YYYYMMDD-HHMMSS.json` + ### 3.3 Single-user развёртывание ```powershell @@ -171,6 +189,14 @@ C:\Deploy\ActivityWatch-Russian\windows\hardening-recovery.ps1 ` -ConfigPath C:\ProgramData\ActivityWatch\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 | ConvertTo-Json -Depth 12 +``` + --- ## 4) Что должно появиться на Windows после установки diff --git a/docs/windows/deployment.md b/docs/windows/deployment.md index ddcc87b..863e543 100755 --- a/docs/windows/deployment.md +++ b/docs/windows/deployment.md @@ -4,7 +4,9 @@ - `windows/deploy-single-user.ps1` — развёртывание для одного пользователя. - `windows/deploy-domain-users.ps1` — массовое развёртывание по списку пользователей. +- `windows/deploy-ensemble.ps1` — orchestration-скрипт полного цикла (deploy + hardening + validation). - `windows/hardening-recovery.ps1` — повторная регистрация задач, ACL и recovery-loop. +- `windows/validate-deployment.ps1` — машинная проверка состояния и JSON-отчёт. - `windows/browser-domains-native-collector.ps1` — native collector доменов браузера с категоризацией. - `windows/web-category-rules.example.json` — пример кастомных правил категоризации. @@ -88,6 +90,21 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен. +## Ensemble deploy (production workflow) + +```powershell +.\windows\deploy-ensemble.ps1 ` + -ServerHost aw.example.local ` + -ServerPort 5600 ` + -Domain CONTOSO ` + -Users user1,user2,user3,user4,user5 ` + -ValidateAfterDeploy +``` + +Итоговый отчёт: + +- `C:\ProgramData\ActivityWatch\ensemble-report-YYYYMMDD-HHMMSS.json` + ## Категоризация доменов - Встроенные категории покрывают базовые рабочие, нейтральные и личные домены. diff --git a/docs/windows/ensemble.md b/docs/windows/ensemble.md new file mode 100644 index 0000000..582ff27 --- /dev/null +++ b/docs/windows/ensemble.md @@ -0,0 +1,49 @@ +# Windows deployment ensemble (PowerShell) + +Документ фиксирует профессиональный workflow развёртывания и контроля ActivityWatch-клиентов в домене Windows. + +## Полные пути + +- `/home/igor/tmp/AWatch-rus/windows/deploy-ensemble.ps1` +- `/home/igor/tmp/AWatch-rus/windows/deploy-domain-users.ps1` +- `/home/igor/tmp/AWatch-rus/windows/deploy-single-user.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/ActivityWatch.Windows.Common.psm1` +- `/home/igor/tmp/AWatch-rus/windows/ActivityWatch.Windows.Common.psd1` + +## Рекомендованный запуск + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process + +C:\Deploy\AWatch-rus\windows\deploy-ensemble.ps1 ` + -ServerHost 10.10.10.13 ` + -ServerPort 5600 ` + -Domain AD ` + -Users user1,user2,user3,user4,user5 ` + -ValidateAfterDeploy +``` + +## Что делает `deploy-ensemble.ps1` + +1. Нормализует список пользователей (`DOMAIN\user`). +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`. + +## Быстрый health-check + +```powershell +$report = C:\Deploy\AWatch-rus\windows\validate-deployment.ps1 ` + -ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json +$report | ConvertTo-Json -Depth 12 +``` + +Ожидается: + +- `overallOk = true` +- нет missing-файлов +- все `ActivityWatch*` Scheduled Task присутствуют +- активны процессы `aw-watcher-afk` и `aw-watcher-window` diff --git a/docs/windows/validation.md b/docs/windows/validation.md index bbb9970..8838a6c 100755 --- a/docs/windows/validation.md +++ b/docs/windows/validation.md @@ -1,5 +1,17 @@ # Windows validation +## Автоматическая проверка (рекомендуется) + +```powershell +$report = .\windows\validate-deployment.ps1 ` + -ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json +$report | ConvertTo-Json -Depth 12 +``` + +Критерий: + +- `overallOk = true` + ## Базовая проверка после развёртывания ### 1. Проверить установленные файлы @@ -59,7 +71,7 @@ Invoke-WebRequest https://aw.example.local/api/0/info - `aw-watcher-window_` - `aw-watcher-web-edge_` или другой browser bucket -- `aw-watcher-web-category_` +- `aw-detmir-web-category_` Проверка через API: diff --git a/windows/ActivityWatch.Windows.Common.psd1 b/windows/ActivityWatch.Windows.Common.psd1 new file mode 100644 index 0000000..9bc5fa8 --- /dev/null +++ b/windows/ActivityWatch.Windows.Common.psd1 @@ -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' + } + } +} diff --git a/windows/deploy-ensemble.ps1 b/windows/deploy-ensemble.ps1 new file mode 100644 index 0000000..5675d08 --- /dev/null +++ b/windows/deploy-ensemble.ps1 @@ -0,0 +1,109 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$ServerHost, + [string[]]$Users, + [string]$UserListPath, + [string]$Domain, + [int]$ServerPort = 5600, + [ValidateSet('http', 'https')] + [string]$ServerScheme = 'http', + [string]$Version = 'v0.13.2', + [string]$PackageUrl, + [string]$PackageZipPath, + [string]$InstallRoot = 'C:\Program Files\ActivityWatch', + [string]$StateRoot = 'C:\ProgramData\ActivityWatch', + [int]$PollSeconds = 5, + [int]$PulseSeconds = 30, + [int]$RecoveryIntervalSeconds = 180, + [string]$CustomRulesPath, + [string]$ReportPath, + [switch]$SkipHardening, + [switch]$ValidateAfterDeploy +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +Assert-Administrator + +$resolvedUsers = Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain +$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' +$effectiveReportPath = if ($ReportPath) { $ReportPath } else { Join-Path $StateRoot "ensemble-report-$timestamp.json" } +$deployScript = Join-Path $PSScriptRoot 'deploy-domain-users.ps1' +$hardeningScript = Join-Path $PSScriptRoot 'hardening-recovery.ps1' +$validationScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' + +if (-not (Test-Path -LiteralPath $deployScript)) { + throw "Missing script: $deployScript" +} + +& $deployScript ` + -ServerHost $ServerHost ` + -Users $resolvedUsers ` + -ServerPort $ServerPort ` + -ServerScheme $ServerScheme ` + -Version $Version ` + -PackageUrl $PackageUrl ` + -PackageZipPath $PackageZipPath ` + -InstallRoot $InstallRoot ` + -StateRoot $StateRoot ` + -PollSeconds $PollSeconds ` + -PulseSeconds $PulseSeconds ` + -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` + -CustomRulesPath $CustomRulesPath + +if (-not $SkipHardening) { + & $hardeningScript ` + -ConfigPath (Join-Path $StateRoot 'deployment-config.json') ` + -ServerHost $ServerHost ` + -ServerPort $ServerPort ` + -ServerScheme $ServerScheme ` + -Users $resolvedUsers ` + -InstallRoot $InstallRoot ` + -StateRoot $StateRoot ` + -PollSeconds $PollSeconds ` + -PulseSeconds $PulseSeconds ` + -RecoveryIntervalSeconds $RecoveryIntervalSeconds ` + -CustomRulesPath $CustomRulesPath +} + +$report = [ordered]@{ + generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') + server = [ordered]@{ + host = $ServerHost + port = $ServerPort + scheme = $ServerScheme + } + packageVersion = $Version + users = @($resolvedUsers) + paths = [ordered]@{ + installRoot = $InstallRoot + stateRoot = $StateRoot + configPath = Join-Path $StateRoot 'deployment-config.json' + } + hardeningApplied = (-not $SkipHardening) +} + +if ($ValidateAfterDeploy) { + if (-not (Test-Path -LiteralPath $validationScript)) { + throw "Missing script: $validationScript" + } + + $validation = & $validationScript -ConfigPath (Join-Path $StateRoot 'deployment-config.json') + $report.validation = $validation +} + +$reportDirectory = Split-Path -Path $effectiveReportPath -Parent +if ($reportDirectory) { + New-ActivityWatchDirectory -Path $reportDirectory +} + +$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8 + +Write-Host 'ActivityWatch ensemble deploy completed.' +Write-Host "Users: $($resolvedUsers -join ', ')" +Write-Host "Report: $effectiveReportPath" diff --git a/windows/validate-deployment.ps1 b/windows/validate-deployment.ps1 new file mode 100644 index 0000000..15a4822 --- /dev/null +++ b/windows/validate-deployment.ps1 @@ -0,0 +1,86 @@ +[CmdletBinding()] +param( + [string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath +$installRoot = [string]$config.paths.installRoot +$stateRoot = [string]$config.paths.stateRoot +$collectorScript = [string]$config.paths.collectorScript +$rulesPath = [string]$config.paths.rulesPath +$launchScript = [string]$config.paths.launchScript +$recoveryScript = [string]$config.paths.recoveryScript + +$requiredFiles = @( + (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'), + (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'), + $collectorScript, + $rulesPath, + $launchScript, + $recoveryScript, + $ConfigPath +) + +$missingFiles = @( + $requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) } +) + +$processNames = @('aw-watcher-afk', 'aw-watcher-window') +$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId + +$taskNames = @() +if ($config.userTasks) { + $taskNames += @($config.userTasks | ForEach-Object { [string]$_.launchTaskName }) +} +$taskNames += [string]$config.recovery.taskName +$taskNames = $taskNames | Sort-Object -Unique + +$tasks = foreach ($taskName in $taskNames) { + $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue + if ($task) { + [pscustomobject]@{ + taskName = $task.TaskName + state = [string]$task.State + present = $true + } + } + else { + [pscustomobject]@{ + taskName = $taskName + state = 'Missing' + present = $false + } + } +} + +$serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port +$result = [ordered]@{ + generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o') + configPath = $ConfigPath + serverUrl = $serverUrl + installRoot = $installRoot + stateRoot = $stateRoot + files = [ordered]@{ + required = $requiredFiles + missing = $missingFiles + ok = ($missingFiles.Count -eq 0) + } + tasks = [ordered]@{ + list = $tasks + ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present })) + } + processes = [ordered]@{ + list = @($runningProcesses) + ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2) + } +} + +$result.overallOk = [bool]($result.files.ok -and $result.tasks.ok -and $result.processes.ok) + +$result