chore: initial private ActivityWatch Russian deployment project
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
# ActivityWatch-Russian
|
||||||
|
|
||||||
|
Практический каркас проекта для повторного развёртывания ActivityWatch Server в новом окружении с LXC-контейнером на Proxmox, русифицированным Web UI, systemd-юнитами, шаблонными скриптами деплоя и эксплуатационной документацией.
|
||||||
|
|
||||||
|
## Что входит
|
||||||
|
|
||||||
|
- `docs/preparation.md` — подготовка инфраструктуры и входных параметров.
|
||||||
|
- `docs/deployment.md` — пошаговый деплой LXC и ActivityWatch Server.
|
||||||
|
- `docs/runbook.md` — быстрый runbook для оператора.
|
||||||
|
- `docs/operations.md` — регламент сопровождения, бэкапов, обновлений и rollback.
|
||||||
|
- `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox.
|
||||||
|
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI.
|
||||||
|
|
||||||
|
## Базовый сценарий
|
||||||
|
|
||||||
|
1. Подготовить параметры окружения по `docs/preparation.md`.
|
||||||
|
2. Заполнить единый файл секретов `secrets/deploy.secrets.env` (автоподключение).
|
||||||
|
3. На узле Proxmox создать контейнер через `proxmox/create-ct.sh`.
|
||||||
|
4. Загрузить артефакты и серверный env в CT через `proxmox/push-aw-artifacts.sh`.
|
||||||
|
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`.
|
||||||
|
|
||||||
|
Скрипты `proxmox/create-ct.sh` и `proxmox/push-aw-artifacts.sh` по умолчанию читают:
|
||||||
|
|
||||||
|
- `secrets/deploy.secrets.env`
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
- Никаких реальных секретов, токенов и боевых IP в репозитории.
|
||||||
|
- Все переменные вынесены в `.example` / `.env` шаблоны.
|
||||||
|
- Документация ориентирована на повторяемое развёртывание, а не на одноразовую ручную установку.
|
||||||
|
- Rollback и backup описаны как обязательная часть каждой операции.
|
||||||
|
|
||||||
|
## Минимальная структура
|
||||||
|
|
||||||
|
- CT/LXC на Debian 12
|
||||||
|
- ActivityWatch Server Rust release
|
||||||
|
- Web UI override в `/opt/activitywatch/webui-ru`
|
||||||
|
- systemd unit `activitywatch-server.service`
|
||||||
|
- bind/listen через переменные окружения
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Windows-клиенты и watcher-автоматизация в этой части каркаса не описываются.
|
||||||
|
- Интеграции с InfluxDB/Grafana/LDAP оставлены как следующий слой, не как обязательная база.
|
||||||
|
|
||||||
|
## Быстрые ссылки
|
||||||
|
|
||||||
|
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/docs/FULL_DEPLOYMENT_MANUAL_RU.md`
|
||||||
|
- `docs/preparation.md`
|
||||||
|
- `docs/deployment.md`
|
||||||
|
- `docs/runbook.md`
|
||||||
|
- `docs/operations.md`
|
||||||
|
- `proxmox/create-ct.sh`
|
||||||
|
- `aw-server/install_aw_server.sh`
|
||||||
Executable
+24
@@ -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
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/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:-/opt/activitywatch/webui-ru}"
|
||||||
|
PATCH_JS_SRC="/root/bootstrap/aw-ru-patch.js"
|
||||||
|
SW_CLEANUP_SRC="/root/bootstrap/aw-sw-cleanup.js"
|
||||||
|
INDEX_HTML="$WEBUI_DIR/index.html"
|
||||||
|
SERVICE_WORKER="$WEBUI_DIR/service-worker.js"
|
||||||
|
TS=$(date +%Y%m%d%H%M%S)
|
||||||
|
|
||||||
|
[[ -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 "$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"
|
||||||
|
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"
|
||||||
|
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
|
||||||
|
|
||||||
|
echo "RU patch applied to $WEBUI_DIR"
|
||||||
Executable
+122
@@ -0,0 +1,122 @@
|
|||||||
|
(function () {
|
||||||
|
window.__awRuPatchVersion = "template-v1";
|
||||||
|
document.documentElement.setAttribute("data-aw-ru-patch", "template-v1");
|
||||||
|
|
||||||
|
const exact = new Map([
|
||||||
|
["ActivityWatch", "АктивВотч"],
|
||||||
|
["Home", "Главная"],
|
||||||
|
["Activity", "Активность"],
|
||||||
|
["Timeline", "Таймлайн"],
|
||||||
|
["Trends", "Тренды"],
|
||||||
|
["Report", "Отчеты"],
|
||||||
|
["Settings", "Настройки"],
|
||||||
|
["Search", "Поиск"],
|
||||||
|
["Buckets", "Бакеты"],
|
||||||
|
["Stopwatch", "Секундомер"],
|
||||||
|
["Tools", "Инструменты"],
|
||||||
|
["Raw Data", "Сырые данные"],
|
||||||
|
["Summary", "Сводка"],
|
||||||
|
["All", "Все"],
|
||||||
|
["None", "Нет"],
|
||||||
|
["Date", "Дата"],
|
||||||
|
["Time", "Время"],
|
||||||
|
["Start", "Начало"],
|
||||||
|
["End", "Конец"],
|
||||||
|
["Download", "Скачать"],
|
||||||
|
["Upload", "Загрузить"],
|
||||||
|
["Show", "Показать"],
|
||||||
|
["Hide", "Скрыть"],
|
||||||
|
["Close", "Закрыть"],
|
||||||
|
["Week", "Неделя"],
|
||||||
|
["Month", "Месяц"],
|
||||||
|
["Year", "Год"],
|
||||||
|
["Today", "Сегодня"],
|
||||||
|
["Yesterday", "Вчера"],
|
||||||
|
["No data", "Нет данных"],
|
||||||
|
["Category", "Категория"],
|
||||||
|
["Categories", "Категории"],
|
||||||
|
["Duration", "Длительность"],
|
||||||
|
["Applications", "Приложения"],
|
||||||
|
["Hosts", "Хосты"],
|
||||||
|
["Date Range", "Диапазон дат"],
|
||||||
|
["Generate", "Сформировать"],
|
||||||
|
["Loading", "Загрузка"],
|
||||||
|
["Loading...", "Загрузка..."],
|
||||||
|
["Dark", "Темная"],
|
||||||
|
["Light", "Светлая"],
|
||||||
|
["Save", "Сохранить"],
|
||||||
|
["Cancel", "Отмена"],
|
||||||
|
["Delete", "Удалить"],
|
||||||
|
["API Browser", "API-браузер"],
|
||||||
|
["Documentation", "Документация"]
|
||||||
|
]);
|
||||||
|
|
||||||
|
const partial = [
|
||||||
|
["Top apps", "Топ приложений"],
|
||||||
|
["Top titles", "Топ заголовков"],
|
||||||
|
["Top URLs", "Топ URL"],
|
||||||
|
["Top domains", "Топ доменов"],
|
||||||
|
["Top Browser Domains", "Топ доменов браузера"],
|
||||||
|
["Top Browser URLs", "Топ URL браузера"],
|
||||||
|
["Top Browser Titles", "Топ заголовков браузера"],
|
||||||
|
["Top Categories", "Топ категорий"],
|
||||||
|
["Category Tree", "Дерево категорий"],
|
||||||
|
["Timeline (barchart)", "Таймлайн (гистограмма)"],
|
||||||
|
["Calculate Work Time", "Рассчитать рабочее время"],
|
||||||
|
["Export CSV", "Экспорт CSV"],
|
||||||
|
["Export JSON", "Экспорт JSON"],
|
||||||
|
["No duplicate events found.", "Дубликаты событий не найдены."],
|
||||||
|
["No overlapping events found.", "Пересекающиеся события не найдены."],
|
||||||
|
["No zero-duration events found.", "События нулевой длительности не найдены."]
|
||||||
|
];
|
||||||
|
|
||||||
|
function replaceText(text) {
|
||||||
|
if (!text) return text;
|
||||||
|
if (exact.has(text.trim())) {
|
||||||
|
return text.replace(text.trim(), exact.get(text.trim()));
|
||||||
|
}
|
||||||
|
let result = text;
|
||||||
|
for (const [en, ru] of partial) {
|
||||||
|
result = result.split(en).join(ru);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(root) {
|
||||||
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
|
||||||
|
const nodes = [];
|
||||||
|
while (walker.nextNode()) nodes.push(walker.currentNode);
|
||||||
|
for (const node of nodes) {
|
||||||
|
const nextValue = replaceText(node.nodeValue);
|
||||||
|
if (nextValue !== node.nodeValue) {
|
||||||
|
node.nodeValue = nextValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function translateAttributes(root) {
|
||||||
|
const elements = root.querySelectorAll("[title],[placeholder],[aria-label]");
|
||||||
|
for (const element of elements) {
|
||||||
|
["title", "placeholder", "aria-label"].forEach(function (attr) {
|
||||||
|
const value = element.getAttribute(attr);
|
||||||
|
if (value) {
|
||||||
|
element.setAttribute(attr, replaceText(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPatch() {
|
||||||
|
walk(document.body);
|
||||||
|
translateAttributes(document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(function () {
|
||||||
|
applyPatch();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("load", function () {
|
||||||
|
applyPatch();
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
||||||
|
});
|
||||||
|
})();
|
||||||
Executable
+11
@@ -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
|
||||||
Executable
+18
@@ -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 () {});
|
||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/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
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
Executable
+308
@@ -0,0 +1,308 @@
|
|||||||
|
# Полная инструкция по развёртыванию и поддержке ActivityWatch-Russian
|
||||||
|
|
||||||
|
Документ описывает полный цикл: Proxmox/LXC сервер, установка ActivityWatch Server, RU Web UI patch, развёртывание Windows-клиентов в другом AD-домене, валидация, сопровождение и rollback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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/hardening-recovery.ps1`
|
||||||
|
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows/browser-domains-native-collector.ps1`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) Подготовка
|
||||||
|
|
||||||
|
### 1.1 Требования
|
||||||
|
|
||||||
|
- Proxmox VE 8/9, доступ root (или sudo с правами на `pct`).
|
||||||
|
- Шаблон Debian 12 LXC на хосте Proxmox.
|
||||||
|
- Windows хост(ы) с PowerShell 5.1+ и правами локального администратора.
|
||||||
|
- Сетевой доступ Windows-клиентов до ActivityWatch Server (`5600/tcp`).
|
||||||
|
|
||||||
|
### 1.2 Подготовка единого файла секретов
|
||||||
|
|
||||||
|
Скопируйте шаблон:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env.example \
|
||||||
|
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Заполните в файле `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`:
|
||||||
|
|
||||||
|
- все `CT_*` параметры контейнера;
|
||||||
|
- все `AW_SERVER_*` параметры сервера;
|
||||||
|
- `CT_PASSWORD` (реальный пароль).
|
||||||
|
|
||||||
|
Важно: этот файл подхватывается автоматически скриптами Proxmox.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Развёртывание сервера в Proxmox
|
||||||
|
|
||||||
|
### 2.1 Создать LXC контейнер
|
||||||
|
|
||||||
|
На узле Proxmox:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian
|
||||||
|
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/create-ct.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
По умолчанию читается:
|
||||||
|
|
||||||
|
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`
|
||||||
|
|
||||||
|
При необходимости можно передать другой путь:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/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
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт загружает в CT:
|
||||||
|
|
||||||
|
- `/root/bootstrap/install_aw_server.sh`
|
||||||
|
- `/root/bootstrap/apply_webui_ru_patch.sh`
|
||||||
|
- `/root/bootstrap/activitywatch-server.service`
|
||||||
|
- `/root/bootstrap/aw-ru-patch.js`
|
||||||
|
- `/root/bootstrap/aw-sw-cleanup.js`
|
||||||
|
- `/etc/activitywatch/aw-server.env` (из `AW_SERVER_*`)
|
||||||
|
|
||||||
|
### 2.3 Установить ActivityWatch Server внутри CT
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pct enter <CT_ID>
|
||||||
|
bash /root/bootstrap/install_aw_server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Применить RU patch Web UI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /root/bootstrap/apply_webui_ru_patch.sh
|
||||||
|
systemctl restart activitywatch-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 Проверка сервера
|
||||||
|
|
||||||
|
В CT:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status activitywatch-server.service --no-pager
|
||||||
|
curl -fsS http://127.0.0.1:5600/api/0/info
|
||||||
|
ss -ltnp | grep 5600
|
||||||
|
grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидается:
|
||||||
|
|
||||||
|
- сервис `active (running)`;
|
||||||
|
- API отвечает JSON;
|
||||||
|
- порт 5600 слушается;
|
||||||
|
- в `index.html` присутствуют оба скрипта.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Развёртывание Windows-клиентов (другой AD-домен)
|
||||||
|
|
||||||
|
### 3.1 Подготовка на Windows-хосте
|
||||||
|
|
||||||
|
Скопируйте каталог:
|
||||||
|
|
||||||
|
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/windows`
|
||||||
|
|
||||||
|
например в:
|
||||||
|
|
||||||
|
- `C:\Deploy\ActivityWatch-Russian\windows`
|
||||||
|
|
||||||
|
Откройте **elevated PowerShell**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Массовое доменное развёртывание (рекомендуется)
|
||||||
|
|
||||||
|
Пример со списком пользователей:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Deploy\ActivityWatch-Russian\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
|
||||||
|
```
|
||||||
|
|
||||||
|
Поддерживаемые варианты:
|
||||||
|
|
||||||
|
- `-Users user01,user02`
|
||||||
|
- `-Users 'CONTOSO\user01','CONTOSO\user02'`
|
||||||
|
- `-UserListPath <txt|csv>`
|
||||||
|
|
||||||
|
### 3.3 Single-user развёртывание
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Deploy\ActivityWatch-Russian\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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Recovery / hardening
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
C:\Deploy\ActivityWatch-Russian\windows\hardening-recovery.ps1 `
|
||||||
|
-ConfigPath C:\ProgramData\ActivityWatch\deployment-config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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\`
|
||||||
|
|
||||||
|
Задачи планировщика:
|
||||||
|
|
||||||
|
- `ActivityWatch Launch [<user>]` (per-user, при логоне)
|
||||||
|
- `ActivityWatch Recovery` (system-level recovery)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Полная валидация потока данных
|
||||||
|
|
||||||
|
### 5.1 На Windows-хосте
|
||||||
|
|
||||||
|
Проверить процессы:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Process aw-watcher-afk,aw-watcher-window -ErrorAction SilentlyContinue
|
||||||
|
Get-CimInstance Win32_Process | ? { $_.CommandLine -like '*browser-domains-native-collector.ps1*' } | select ProcessId,SessionId,CommandLine
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить задачи:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ScheduledTask | ? { $_.TaskName -like 'ActivityWatch*' } | select TaskName,State
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 На сервере ActivityWatch API
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS http://127.0.0.1:5600/api/0/buckets | jq 'keys'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемые bucket'ы:
|
||||||
|
|
||||||
|
- `aw-watcher-afk_<HOST>`
|
||||||
|
- `aw-watcher-window_<HOST>`
|
||||||
|
- `aw-watcher-web-<browser>_<HOST>`
|
||||||
|
- `aw-detmir-web-category_<HOST>` (категоризованный поток)
|
||||||
|
|
||||||
|
Проверка событий браузера:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "http://127.0.0.1:5600/api/0/buckets/aw-watcher-web-edge_<HOST>/events?limit=5" | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка категоризации:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -sS "http://127.0.0.1:5600/api/0/buckets/aw-detmir-web-category_<HOST>/events?limit=5" | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) Сопровождение (обязательно)
|
||||||
|
|
||||||
|
### 6.1 Backup перед любыми изменениями
|
||||||
|
|
||||||
|
На Proxmox:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vzdump <CT_ID> --mode snapshot --compress zstd --storage <BACKUP_STORAGE>
|
||||||
|
```
|
||||||
|
|
||||||
|
Конфиги внутри CT:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pct exec <CT_ID> -- tar -C / -czf /root/activitywatch-config-backup.tgz \
|
||||||
|
etc/activitywatch \
|
||||||
|
etc/systemd/system/activitywatch-server.service \
|
||||||
|
opt/activitywatch/webui-ru \
|
||||||
|
opt/activitywatch/releases
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 Обновление сервера
|
||||||
|
|
||||||
|
1. Обновить `AW_SERVER_VERSION` и `AW_SERVER_DOWNLOAD_URL` в
|
||||||
|
`/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`
|
||||||
|
2. Выполнить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/proxmox/push-aw-artifacts.sh
|
||||||
|
pct enter <CT_ID>
|
||||||
|
bash /root/bootstrap/install_aw_server.sh
|
||||||
|
bash /root/bootstrap/apply_webui_ru_patch.sh
|
||||||
|
systemctl restart activitywatch-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Повторить валидацию API/UI.
|
||||||
|
|
||||||
|
### 6.3 Rollback
|
||||||
|
|
||||||
|
RU patch rollback:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /opt/activitywatch/webui-ru/index.html.bak.<timestamp> /opt/activitywatch/webui-ru/index.html
|
||||||
|
systemctl restart activitywatch-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Полный rollback:
|
||||||
|
|
||||||
|
- восстановить CT из snapshot/backup;
|
||||||
|
- проверить API и Web UI;
|
||||||
|
- проверить доступность для Windows-клиентов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) Безопасность
|
||||||
|
|
||||||
|
- Не хранить реальные секреты вне `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/secrets/deploy.secrets.env`.
|
||||||
|
- Не открывать `5600/tcp` в интернет напрямую.
|
||||||
|
- Публиковать через VPN или reverse proxy с ограничением доступа.
|
||||||
|
- Перед изменениями всегда делать backup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
4. В CT выполнены `/root/bootstrap/install_aw_server.sh` и `/root/bootstrap/apply_webui_ru_patch.sh`.
|
||||||
|
5. Сервер API/порт/UI проверены.
|
||||||
|
6. На Windows выполнен `deploy-domain-users.ps1`.
|
||||||
|
7. Проверены процессы, задачи и bucket'ы.
|
||||||
|
8. Зафиксированы параметры и дата ввода.
|
||||||
Executable
+116
@@ -0,0 +1,116 @@
|
|||||||
|
# Deployment
|
||||||
|
|
||||||
|
## 1. Подготовить env-файлы
|
||||||
|
|
||||||
|
На рабочей машине оператора:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp proxmox/ct-vars.example.env /root/activitywatch-ct.env
|
||||||
|
cp aw-server/aw-server.env.example /root/activitywatch-aw.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Заполнить оба файла реальными значениями вне git.
|
||||||
|
|
||||||
|
## 2. Создать CT на Proxmox
|
||||||
|
|
||||||
|
На узле Proxmox:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /path/to/ActivityWatch-Russian
|
||||||
|
./proxmox/create-ct.sh /root/activitywatch-ct.env
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт:
|
||||||
|
|
||||||
|
- валидирует обязательные переменные;
|
||||||
|
- создаёт Debian 12 CT;
|
||||||
|
- запускает контейнер;
|
||||||
|
- выполняет минимальный bootstrap пакетов;
|
||||||
|
- готовит `/root/bootstrap` для дальнейшей загрузки артефактов.
|
||||||
|
|
||||||
|
## 3. Загрузить артефакты в CT
|
||||||
|
|
||||||
|
На узле Proxmox:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./proxmox/push-aw-artifacts.sh /root/activitywatch-ct.env
|
||||||
|
pct push <CT_ID> /root/activitywatch-aw.env /etc/activitywatch/aw-server.env
|
||||||
|
```
|
||||||
|
|
||||||
|
В CT будут загружены:
|
||||||
|
|
||||||
|
- `install_aw_server.sh`
|
||||||
|
- `apply_webui_ru_patch.sh`
|
||||||
|
- `activitywatch-server.service`
|
||||||
|
- `aw-ru-patch.js`
|
||||||
|
- `aw-sw-cleanup.js`
|
||||||
|
|
||||||
|
## 4. Установить ActivityWatch Server
|
||||||
|
|
||||||
|
Внутри CT:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pct enter <CT_ID>
|
||||||
|
bash /root/bootstrap/install_aw_server.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт установки:
|
||||||
|
|
||||||
|
- ставит `curl`, `unzip`, `jq`, `ca-certificates`;
|
||||||
|
- создаёт пользователя `activitywatch`;
|
||||||
|
- скачивает release `aw-server-rust`;
|
||||||
|
- раскладывает бинарник по версиям;
|
||||||
|
- создаёт активный symlink;
|
||||||
|
- копирует systemd unit;
|
||||||
|
- включает и запускает сервис.
|
||||||
|
|
||||||
|
## 5. Применить RU patch для Web UI
|
||||||
|
|
||||||
|
Внутри CT:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash /root/bootstrap/apply_webui_ru_patch.sh
|
||||||
|
systemctl restart activitywatch-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Патч:
|
||||||
|
|
||||||
|
- копирует `aw-ru-patch.js` и `aw-sw-cleanup.js`;
|
||||||
|
- делает backup `index.html`;
|
||||||
|
- добавляет include в `index.html`;
|
||||||
|
- заменяет `service-worker.js` cleanup-версией для сброса старого cache.
|
||||||
|
|
||||||
|
## 6. Проверить сервис
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl status activitywatch-server.service --no-pager
|
||||||
|
curl -fsS http://127.0.0.1:5600/api/0/info
|
||||||
|
ss -ltnp | grep 5600
|
||||||
|
grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
- сервис `active (running)`;
|
||||||
|
- API отвечает;
|
||||||
|
- порт слушается;
|
||||||
|
- в `index.html` есть оба include.
|
||||||
|
|
||||||
|
## 7. После публикации
|
||||||
|
|
||||||
|
Проверить извне:
|
||||||
|
|
||||||
|
- открывается `/`;
|
||||||
|
- API доступен по ожидаемому URL;
|
||||||
|
- после hard refresh видна русификация;
|
||||||
|
- reverse proxy не кэширует старую статику.
|
||||||
|
|
||||||
|
## 8. Что зафиксировать после ввода
|
||||||
|
|
||||||
|
- дата ввода;
|
||||||
|
- узел Proxmox;
|
||||||
|
- `CT_ID`;
|
||||||
|
- IP/FQDN;
|
||||||
|
- версия `aw-server-rust`;
|
||||||
|
- место хранения backup;
|
||||||
|
- фактическая схема публикации.
|
||||||
Executable
+113
@@ -0,0 +1,113 @@
|
|||||||
|
# Operations
|
||||||
|
|
||||||
|
## Эксплуатационная модель
|
||||||
|
|
||||||
|
Система состоит из:
|
||||||
|
|
||||||
|
- узла Proxmox VE;
|
||||||
|
- LXC-контейнера с Debian 12;
|
||||||
|
- `ActivityWatch Server` на Rust;
|
||||||
|
- Web UI override с RU patch;
|
||||||
|
- systemd unit `activitywatch-server.service`.
|
||||||
|
|
||||||
|
Базовые правила:
|
||||||
|
|
||||||
|
- все изменения только через backup-first workflow;
|
||||||
|
- секреты не хранить в git;
|
||||||
|
- каждое изменение фиксировать в ticket/run log;
|
||||||
|
- публичную публикацию делать через отдельный proxy/security layer.
|
||||||
|
|
||||||
|
## Регулярные проверки
|
||||||
|
|
||||||
|
### Ежедневно или перед работами
|
||||||
|
|
||||||
|
- `pct status <CT_ID>`;
|
||||||
|
- `systemctl is-active activitywatch-server.service`;
|
||||||
|
- локальный `curl /api/0/info`;
|
||||||
|
- `df -h`;
|
||||||
|
- `journalctl -p err -b`.
|
||||||
|
|
||||||
|
### Еженедельно
|
||||||
|
|
||||||
|
- проверить свежесть backup/snapshot;
|
||||||
|
- проверить publish URL извне;
|
||||||
|
- проверить срок TLS, если есть reverse proxy;
|
||||||
|
- перепроверить firewall/ACL для `5600/tcp`.
|
||||||
|
|
||||||
|
### Ежемесячно
|
||||||
|
|
||||||
|
- проверить новые релизы ActivityWatch;
|
||||||
|
- сделать dry-run rollback;
|
||||||
|
- обновить runbook при изменениях инфраструктуры.
|
||||||
|
|
||||||
|
## Backup
|
||||||
|
|
||||||
|
Минимум сохранять:
|
||||||
|
|
||||||
|
- backup CT через `vzdump`;
|
||||||
|
- `/etc/activitywatch/aw-server.env`;
|
||||||
|
- `/etc/systemd/system/activitywatch-server.service`;
|
||||||
|
- `/opt/activitywatch/webui-ru/`;
|
||||||
|
- `/opt/activitywatch/releases/`;
|
||||||
|
- конфиг reverse proxy, если он есть.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
### Быстрый rollback RU patch
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp /opt/activitywatch/webui-ru/index.html.bak.<timestamp> /opt/activitywatch/webui-ru/index.html
|
||||||
|
systemctl restart activitywatch-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rollback server release
|
||||||
|
|
||||||
|
1. Остановить сервис.
|
||||||
|
2. Переключить symlink на предыдущий release.
|
||||||
|
3. Проверить права.
|
||||||
|
4. Запустить сервис.
|
||||||
|
5. Проверить API/UI.
|
||||||
|
|
||||||
|
### Полный rollback CT
|
||||||
|
|
||||||
|
- остановить CT;
|
||||||
|
- восстановить snapshot или `vzdump`;
|
||||||
|
- поднять CT;
|
||||||
|
- проверить API и publish path.
|
||||||
|
|
||||||
|
## Обновление
|
||||||
|
|
||||||
|
Порядок:
|
||||||
|
|
||||||
|
1. Сделать backup.
|
||||||
|
2. Скачать новый release в отдельную директорию.
|
||||||
|
3. Не затирать прошлую версию до успешной проверки.
|
||||||
|
4. Проверить совместимость RU patch.
|
||||||
|
5. Перезапустить сервис.
|
||||||
|
6. Проверить API/UI.
|
||||||
|
7. Зафиксировать результат.
|
||||||
|
|
||||||
|
## Эскалация
|
||||||
|
|
||||||
|
Эскалировать сразу, если:
|
||||||
|
|
||||||
|
- потерян доступ к Proxmox или CT;
|
||||||
|
- backup chain повреждён;
|
||||||
|
- UI/API не вернулись после rollback;
|
||||||
|
- изменились маршруты, bridge, VLAN или firewall policy;
|
||||||
|
- нужен новый публичный endpoint.
|
||||||
|
|
||||||
|
## Что не делать
|
||||||
|
|
||||||
|
- не вшивать реальные IP, пароли и токены;
|
||||||
|
- не обновлять поверх рабочего бинарника без backup;
|
||||||
|
- не открывать `5600/tcp` наружу без отдельной защиты;
|
||||||
|
- не править `index.html` вручную без backup.
|
||||||
Executable
+92
@@ -0,0 +1,92 @@
|
|||||||
|
# Preparation
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Подготовить чистое окружение для повторяемого развёртывания отдельного `ActivityWatch Server` в LXC-контейнере на Proxmox без зависимости от старой инсталляции.
|
||||||
|
|
||||||
|
## Что определить заранее
|
||||||
|
|
||||||
|
### Инфраструктурные параметры
|
||||||
|
|
||||||
|
- имя узла Proxmox;
|
||||||
|
- `CT_ID`;
|
||||||
|
- `CT_HOSTNAME`;
|
||||||
|
- `CT_BRIDGE`;
|
||||||
|
- `CT_VLAN`, если нужен;
|
||||||
|
- статический IP/маска/шлюз;
|
||||||
|
- список DNS;
|
||||||
|
- storage для `rootfs`;
|
||||||
|
- размер диска;
|
||||||
|
- лимиты CPU/RAM/SWAP;
|
||||||
|
- timezone;
|
||||||
|
- внутренний или внешний FQDN для публикации.
|
||||||
|
|
||||||
|
### Доступы
|
||||||
|
|
||||||
|
Хранить вне репозитория:
|
||||||
|
|
||||||
|
- SSH/API-доступ к Proxmox;
|
||||||
|
- пароль или ключ для CT;
|
||||||
|
- TLS-сертификаты reverse proxy;
|
||||||
|
- токены внешнего мониторинга или backup target.
|
||||||
|
|
||||||
|
## Рекомендуемая базовая схема
|
||||||
|
|
||||||
|
- Proxmox VE 8/9
|
||||||
|
- Debian 12 LXC
|
||||||
|
- `unprivileged=1`
|
||||||
|
- `nesting=1,keyctl=1`
|
||||||
|
- внутренний listen ActivityWatch: `0.0.0.0:5600`
|
||||||
|
- внешняя публикация только через VPN или reverse proxy
|
||||||
|
|
||||||
|
## Проверки на узле Proxmox
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pveversion
|
||||||
|
pveam update
|
||||||
|
pveam available | grep debian-12
|
||||||
|
pvesm status
|
||||||
|
pct list
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить:
|
||||||
|
|
||||||
|
- есть место под CT и backup;
|
||||||
|
- `CT_ID` не занят;
|
||||||
|
- выбранный bridge/VLAN реально маршрутизируется;
|
||||||
|
- Debian template доступен;
|
||||||
|
- есть маршрут до будущего адреса контейнера.
|
||||||
|
|
||||||
|
## Что подготовить до деплоя
|
||||||
|
|
||||||
|
- заполненный `secrets/deploy.secrets.env` (единый файл для CT + AW server);
|
||||||
|
- согласованный URL релиза `aw-server-rust`;
|
||||||
|
- решение по публикации: VPN или reverse proxy;
|
||||||
|
- решение по backup: `vzdump`, snapshot, rsync, NAS или object storage.
|
||||||
|
|
||||||
|
## Единый файл секретов
|
||||||
|
|
||||||
|
Файл `secrets/deploy.secrets.env` автоматически подхватывается:
|
||||||
|
|
||||||
|
- `proxmox/create-ct.sh`
|
||||||
|
- `proxmox/push-aw-artifacts.sh`
|
||||||
|
|
||||||
|
В нём хранятся:
|
||||||
|
|
||||||
|
- `CT_*` параметры контейнера;
|
||||||
|
- `AW_SERVER_*` параметры сервера.
|
||||||
|
|
||||||
|
## Базовые каталоги внутри CT
|
||||||
|
|
||||||
|
- `/opt/activitywatch/bin`
|
||||||
|
- `/opt/activitywatch/releases`
|
||||||
|
- `/opt/activitywatch/webui-ru`
|
||||||
|
- `/etc/activitywatch`
|
||||||
|
- `/var/lib/activitywatch`
|
||||||
|
- `/var/log/activitywatch`
|
||||||
|
|
||||||
|
## Security-заметки
|
||||||
|
|
||||||
|
- не публиковать `5600/tcp` напрямую в интернет;
|
||||||
|
- не хранить реальные значения в `.example` файлах;
|
||||||
|
- перед вводом в эксплуатацию зафиксировать firewall, DNS и publish path в `docs/operations.md`.
|
||||||
Executable
+91
@@ -0,0 +1,91 @@
|
|||||||
|
# Runbook
|
||||||
|
|
||||||
|
## Быстрый health-check
|
||||||
|
|
||||||
|
### На Proxmox
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pct status <CT_ID>
|
||||||
|
pct config <CT_ID>
|
||||||
|
pct exec <CT_ID> -- systemctl is-active activitywatch-server.service
|
||||||
|
pct exec <CT_ID> -- curl -fsS http://127.0.0.1:5600/api/0/info
|
||||||
|
```
|
||||||
|
|
||||||
|
### Внутри CT
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl status activitywatch-server.service --no-pager
|
||||||
|
journalctl -u activitywatch-server.service -n 100 --no-pager
|
||||||
|
curl -fsS http://127.0.0.1:5600/api/0/info
|
||||||
|
ss -ltnp | grep 5600
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка RU patch
|
||||||
|
|
||||||
|
```sh
|
||||||
|
grep -n 'aw-ru-patch\|aw-sw-cleanup' /opt/activitywatch/webui-ru/index.html
|
||||||
|
ls -l /opt/activitywatch/webui-ru/js/
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить:
|
||||||
|
|
||||||
|
- есть `aw-ru-patch.js`;
|
||||||
|
- есть `aw-sw-cleanup.js`;
|
||||||
|
- `index.html` содержит оба include;
|
||||||
|
- `service-worker.js` заменён cleanup-версией.
|
||||||
|
|
||||||
|
## Типовые инциденты
|
||||||
|
|
||||||
|
### Сервис не стартует
|
||||||
|
|
||||||
|
```sh
|
||||||
|
systemctl cat activitywatch-server.service
|
||||||
|
cat /etc/activitywatch/aw-server.env
|
||||||
|
journalctl -xeu activitywatch-server.service --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Частые причины:
|
||||||
|
|
||||||
|
- битый URL релиза;
|
||||||
|
- неполная распаковка архива;
|
||||||
|
- занят порт;
|
||||||
|
- не созданы каталоги или пользователь;
|
||||||
|
- ошибка в env-файле.
|
||||||
|
|
||||||
|
### API отвечает, но UI без русификации
|
||||||
|
|
||||||
|
Проверить:
|
||||||
|
|
||||||
|
- патч реально вставлен в `index.html`;
|
||||||
|
- browser cache/service worker очищен;
|
||||||
|
- reverse proxy не отдаёт старую статику;
|
||||||
|
- сервис был перезапущен после правок.
|
||||||
|
|
||||||
|
Повторное применение:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash /root/bootstrap/apply_webui_ru_patch.sh
|
||||||
|
systemctl restart activitywatch-server.service
|
||||||
|
```
|
||||||
|
|
||||||
|
### После обновления UI сломался патч
|
||||||
|
|
||||||
|
- сравнить `index.html` с backup;
|
||||||
|
- заново применить patch script;
|
||||||
|
- проверить словарь в `aw-ru-patch.js`;
|
||||||
|
- при необходимости откатить только Web UI override.
|
||||||
|
|
||||||
|
## Перед любыми изменениями
|
||||||
|
|
||||||
|
1. Сделать snapshot или `vzdump`.
|
||||||
|
2. Сохранить текущий `/etc/activitywatch/aw-server.env`.
|
||||||
|
3. Сохранить текущий `index.html`.
|
||||||
|
4. Зафиксировать текущую версию `aw-server-rust`.
|
||||||
|
|
||||||
|
## Критерии готовности
|
||||||
|
|
||||||
|
- systemd unit стартует без ручного вмешательства;
|
||||||
|
- API `/api/0/info` отвечает локально;
|
||||||
|
- UI открывается;
|
||||||
|
- русификация присутствует;
|
||||||
|
- rollback-путь понятен оператору.
|
||||||
Executable
+111
@@ -0,0 +1,111 @@
|
|||||||
|
# Windows deployment
|
||||||
|
|
||||||
|
## Состав пакета
|
||||||
|
|
||||||
|
- `windows/deploy-single-user.ps1` — развёртывание для одного пользователя.
|
||||||
|
- `windows/deploy-domain-users.ps1` — массовое развёртывание по списку пользователей.
|
||||||
|
- `windows/hardening-recovery.ps1` — повторная регистрация задач, ACL и recovery-loop.
|
||||||
|
- `windows/browser-domains-native-collector.ps1` — native collector доменов браузера с категоризацией.
|
||||||
|
- `windows/web-category-rules.example.json` — пример кастомных правил категоризации.
|
||||||
|
|
||||||
|
## Что делает пакет
|
||||||
|
|
||||||
|
- Ставит `aw-watcher-afk` и `aw-watcher-window` из официального Windows ZIP ActivityWatch.
|
||||||
|
- Копирует browser-domain collector в `C:\ProgramData\ActivityWatch`.
|
||||||
|
- Создаёт per-user задачи `ActivityWatch Launch [...]` с запуском при логоне.
|
||||||
|
- Создаёт системную задачу `ActivityWatch Recovery`, которая циклически перезапускает per-user launch tasks.
|
||||||
|
- Применяет ACL к `C:\Program Files\ActivityWatch`, `C:\ProgramData\ActivityWatch` и каталогу логов.
|
||||||
|
- Не содержит хардкодов инфраструктуры: сервер, домен, список пользователей и правила передаются параметрами.
|
||||||
|
|
||||||
|
## Предпосылки
|
||||||
|
|
||||||
|
- Windows 10/11 или Windows Server с PowerShell 5.1+.
|
||||||
|
- Запуск из elevated PowerShell.
|
||||||
|
- Доступ до ActivityWatch Server по `host:port`.
|
||||||
|
- Пользователи домена должны реально входить на хост интерактивно.
|
||||||
|
- Если интернет недоступен, заранее скачайте ActivityWatch ZIP и передайте `-PackageZipPath`.
|
||||||
|
|
||||||
|
## Single-user deploy
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
|
||||||
|
|
||||||
|
.\windows\deploy-single-user.ps1 `
|
||||||
|
-ServerHost aw.example.local `
|
||||||
|
-ServerPort 5600 `
|
||||||
|
-TargetUser 'CONTOSO\svc.activity.user01' `
|
||||||
|
-CustomRulesPath .\windows\web-category-rules.example.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Локальный пользователь:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\windows\deploy-single-user.ps1 `
|
||||||
|
-ServerHost aw-gateway.internal `
|
||||||
|
-ServerPort 5600 `
|
||||||
|
-TargetUser '.\operator01'
|
||||||
|
```
|
||||||
|
|
||||||
|
Оффлайн-установка из локального ZIP:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\windows\deploy-single-user.ps1 `
|
||||||
|
-ServerHost aw.example.local `
|
||||||
|
-TargetUser 'CONTOSO\user01' `
|
||||||
|
-PackageZipPath C:\Temp\activitywatch-v0.13.2-windows-x86_64.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-user / domain deploy
|
||||||
|
|
||||||
|
Поддерживаются:
|
||||||
|
|
||||||
|
- `-Users user1,user2`
|
||||||
|
- `-Users 'CONTOSO\user1','CONTOSO\user2'`
|
||||||
|
- `-UserListPath .\users.txt`
|
||||||
|
- `-UserListPath .\users.csv`
|
||||||
|
|
||||||
|
TXT-формат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
# comments are ignored
|
||||||
|
user01
|
||||||
|
user02
|
||||||
|
user03
|
||||||
|
```
|
||||||
|
|
||||||
|
CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Login`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\windows\deploy-domain-users.ps1 `
|
||||||
|
-ServerHost aw.example.local `
|
||||||
|
-ServerPort 5600 `
|
||||||
|
-Domain CONTOSO `
|
||||||
|
-UserListPath C:\Temp\aw-users.txt `
|
||||||
|
-CustomRulesPath C:\Temp\web-category-rules.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен.
|
||||||
|
|
||||||
|
## Категоризация доменов
|
||||||
|
|
||||||
|
- Встроенные категории покрывают базовые рабочие, нейтральные и личные домены.
|
||||||
|
- Для кастомизации скопируйте `windows/web-category-rules.example.json` и отредактируйте домены.
|
||||||
|
- Передайте файл через `-CustomRulesPath`; он будет сохранён как `C:\ProgramData\ActivityWatch\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'а.
|
||||||
|
|
||||||
|
## Повторный прогон
|
||||||
|
|
||||||
|
- Скрипты идемпотентны: переустанавливают задачи и обновляют runtime-файлы.
|
||||||
|
- Предыдущая установка ActivityWatch бэкапится в `C:\ProgramData\ActivityWatch\backups\install-YYYYMMDD-HHMMSS`.
|
||||||
|
- Для жёсткого восстановления запускайте `windows/hardening-recovery.ps1`.
|
||||||
Executable
+102
@@ -0,0 +1,102 @@
|
|||||||
|
# Windows troubleshooting
|
||||||
|
|
||||||
|
## Частые проблемы
|
||||||
|
|
||||||
|
### `Run this script from an elevated PowerShell session`
|
||||||
|
|
||||||
|
- Откройте PowerShell через `Run as administrator`.
|
||||||
|
- Проверьте, что UAC не понизил токен.
|
||||||
|
|
||||||
|
### Задачи созданы, но watcher'ы не стартуют
|
||||||
|
|
||||||
|
- Убедитесь, что пользователь реально вошёл в интерактивную сессию.
|
||||||
|
- Проверьте задачу `ActivityWatch Launch [...]` в Task Scheduler.
|
||||||
|
- Запустите вручную:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]'
|
||||||
|
```
|
||||||
|
|
||||||
|
- Если доменный логон ещё ни разу не происходил на хосте, сначала выполните вход этим пользователем.
|
||||||
|
|
||||||
|
### Collector не видит URL
|
||||||
|
|
||||||
|
- Скрипт работает через UI Automation и foreground window.
|
||||||
|
- Некоторые браузеры/страницы могут скрывать адресную строку или блокировать UIA.
|
||||||
|
- Проверьте лог `C:\ProgramData\ActivityWatch\logs\browser-domains-<user>.log`.
|
||||||
|
- Убедитесь, что активное окно — поддерживаемый браузер: Edge, Chrome, Brave, Vivaldi, Opera, Firefox.
|
||||||
|
|
||||||
|
### Сервер недоступен
|
||||||
|
|
||||||
|
- Проверьте TCP-доступ:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection aw.example.local -Port 5600
|
||||||
|
```
|
||||||
|
|
||||||
|
- Проверьте локально API:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest http://aw.example.local:5600/api/0/info
|
||||||
|
```
|
||||||
|
|
||||||
|
- Если нужен HTTPS reverse proxy, задайте `-ServerScheme https`.
|
||||||
|
|
||||||
|
### Неправильная категоризация домена
|
||||||
|
|
||||||
|
- Проверьте содержимое `C:\ProgramData\ActivityWatch\web-category-rules.json`.
|
||||||
|
- Пользовательские правила должны быть валидным JSON.
|
||||||
|
- Один и тот же домен лучше определять только в одной категории.
|
||||||
|
- После изменения правил достаточно перезапустить collector или задачу пользователя:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Stop-Process -Name powershell -ErrorAction SilentlyContinue
|
||||||
|
Start-ScheduledTask -TaskName 'ActivityWatch Launch [CONTOSO_user01]'
|
||||||
|
```
|
||||||
|
|
||||||
|
### ACL сломаны или пользователи удалили runtime-файлы
|
||||||
|
|
||||||
|
- Запустите:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\windows\hardening-recovery.ps1 -RepairPackage
|
||||||
|
```
|
||||||
|
|
||||||
|
- Если сервер и пользователи уже есть в `deployment-config.json`, дополнительные параметры не нужны.
|
||||||
|
|
||||||
|
## Диагностика
|
||||||
|
|
||||||
|
Проверить задачи:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ScheduledTask -TaskName 'ActivityWatch*' | Select-Object TaskName, State
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить процессы в пользовательской сессии:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Process aw-watcher-afk, aw-watcher-window -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object ProcessName, Id, SessionId, StartTime
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить collector:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-CimInstance Win32_Process |
|
||||||
|
Where-Object { $_.Name -in 'powershell.exe', 'pwsh.exe' -and $_.CommandLine -match 'browser-domains-native-collector.ps1' } |
|
||||||
|
Select-Object ProcessId, SessionId, CommandLine
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверить конфиг:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Content C:\ProgramData\ActivityWatch\deployment-config.json -Raw
|
||||||
|
```
|
||||||
|
|
||||||
|
## Когда запускать hardening/recovery
|
||||||
|
|
||||||
|
- После ручной чистки задач.
|
||||||
|
- После неудачного обновления ActivityWatch.
|
||||||
|
- После переноса сервера на другой host/port.
|
||||||
|
- После обновления списка пользователей.
|
||||||
|
- После изменения кастомных правил, если нужен централизованный repair pass.
|
||||||
Executable
+109
@@ -0,0 +1,109 @@
|
|||||||
|
# Windows validation
|
||||||
|
|
||||||
|
## Базовая проверка после развёртывания
|
||||||
|
|
||||||
|
### 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'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемый результат — везде `True`.
|
||||||
|
|
||||||
|
### 2. Проверить задачи
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ScheduledTask -TaskName 'ActivityWatch*' |
|
||||||
|
Select-Object TaskName, Author, State
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
- по одной задаче `ActivityWatch Launch [...]` на пользователя;
|
||||||
|
- одна задача `ActivityWatch Recovery`.
|
||||||
|
|
||||||
|
### 3. Проверить процессы после логина пользователя
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-Process aw-watcher-afk, aw-watcher-window -ErrorAction SilentlyContinue |
|
||||||
|
Select-Object ProcessName, SessionId, StartTime
|
||||||
|
```
|
||||||
|
|
||||||
|
И collector:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-CimInstance Win32_Process |
|
||||||
|
Where-Object { $_.Name -in 'powershell.exe', 'pwsh.exe' -and $_.CommandLine -match 'browser-domains-native-collector.ps1' } |
|
||||||
|
Select-Object ProcessId, SessionId, CommandLine
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Проверить сетевую связность
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection aw.example.local -Port 5600
|
||||||
|
Invoke-WebRequest http://aw.example.local:5600/api/0/info
|
||||||
|
```
|
||||||
|
|
||||||
|
Если используется HTTPS:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest https://aw.example.local/api/0/info
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Проверить buckets на сервере
|
||||||
|
|
||||||
|
После открытия нескольких сайтов в браузере на сервере должны появиться:
|
||||||
|
|
||||||
|
- `aw-watcher-window_<hostname>`
|
||||||
|
- `aw-watcher-web-edge_<hostname>` или другой browser bucket
|
||||||
|
- `aw-watcher-web-category_<hostname>`
|
||||||
|
|
||||||
|
Проверка через API:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Invoke-WebRequest http://aw.example.local:5600/api/0/buckets | Select-Object -ExpandProperty Content
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка категоризации
|
||||||
|
|
||||||
|
1. Откройте сайт из кастомного правила.
|
||||||
|
2. Подождите `PollSeconds + PulseSeconds`.
|
||||||
|
3. Проверьте category bucket на сервере.
|
||||||
|
4. Убедитесь, что поля `domain`, `rootDomain`, `category`, `categoryGroup`, `categoryRule` заполнены.
|
||||||
|
|
||||||
|
## Проверка восстановления
|
||||||
|
|
||||||
|
1. Завершите `aw-watcher-afk.exe` и `aw-watcher-window.exe` у тестового пользователя.
|
||||||
|
2. Подождите до `RecoveryIntervalSeconds`.
|
||||||
|
3. Убедитесь, что per-user launch task стартовала процессы заново.
|
||||||
|
|
||||||
|
Ручной запуск recovery:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Start-ScheduledTask -TaskName 'ActivityWatch Recovery'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Проверка ACL
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
icacls 'C:\Program Files\ActivityWatch'
|
||||||
|
icacls 'C:\ProgramData\ActivityWatch'
|
||||||
|
icacls 'C:\ProgramData\ActivityWatch\logs'
|
||||||
|
```
|
||||||
|
|
||||||
|
Ожидаемо:
|
||||||
|
|
||||||
|
- `SYSTEM` и `Administrators` имеют `F`;
|
||||||
|
- `Users` имеет `RX` на install/state;
|
||||||
|
- `Users` имеет `M` на `logs`.
|
||||||
|
|
||||||
|
## Критерий готовности к массовому развёртыванию
|
||||||
|
|
||||||
|
- Установка проходит без ручного редактирования скриптов.
|
||||||
|
- Все параметры инфраструктуры передаются снаружи.
|
||||||
|
- Повторный запуск не ломает текущую установку.
|
||||||
|
- Recovery восстанавливает запуск watcher'ов после остановки.
|
||||||
|
- Collector пишет domain/category события без расширений браузера.
|
||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
DEFAULT_ENV_FILE="$SCRIPT_DIR/../secrets/deploy.secrets.env"
|
||||||
|
|
||||||
|
if [ "$#" -gt 1 ]; then
|
||||||
|
echo "usage: $0 [ /path/to/deploy.secrets.env ]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENV_FILE="${1:-$DEFAULT_ENV_FILE}"
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
echo "env file not found: $ENV_FILE" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$ENV_FILE"
|
||||||
|
|
||||||
|
required_vars="CT_ID CT_HOSTNAME CT_STORAGE CT_TEMPLATE CT_ROOTFS_SIZE CT_CORES CT_MEMORY CT_SWAP CT_BRIDGE CT_IP CT_GW CT_PASSWORD CT_UNPRIVILEGED CT_ONBOOT CT_FEATURES"
|
||||||
|
for var_name in $required_vars; do
|
||||||
|
eval "var_value=\${$var_name:-}"
|
||||||
|
if [ -z "$var_value" ]; then
|
||||||
|
echo "missing required variable: $var_name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if pct status "$CT_ID" >/dev/null 2>&1; then
|
||||||
|
echo "CT $CT_ID already exists" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
NET0="name=eth0,bridge=$CT_BRIDGE,ip=$CT_IP,gw=$CT_GW"
|
||||||
|
if [ -n "${CT_VLAN:-}" ]; then
|
||||||
|
NET0="$NET0,tag=$CT_VLAN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 "$NET0" \
|
||||||
|
--nameserver "${CT_NAMESERVER:-}" \
|
||||||
|
--searchdomain "${CT_SEARCHDOMAIN:-}" \
|
||||||
|
--ostype debian
|
||||||
|
|
||||||
|
pct start "$CT_ID"
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
pct exec "$CT_ID" -- bash -lc '
|
||||||
|
set -eu
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y curl ca-certificates bash unzip xz-utils jq
|
||||||
|
mkdir -p /root/bootstrap /etc/activitywatch
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "CT $CT_ID created and bootstrapped"
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Copy this file outside git and fill with real values.
|
||||||
|
|
||||||
|
PVE_NODE=pve-example
|
||||||
|
CT_ID=203
|
||||||
|
CT_HOSTNAME=activitywatch-server
|
||||||
|
CT_STORAGE=local-lvm
|
||||||
|
CT_TEMPLATE=local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst
|
||||||
|
CT_ROOTFS_SIZE=8G
|
||||||
|
CT_CORES=2
|
||||||
|
CT_MEMORY=2048
|
||||||
|
CT_SWAP=512
|
||||||
|
CT_BRIDGE=vmbr10
|
||||||
|
CT_IP=10.20.30.13/24
|
||||||
|
CT_GW=10.20.30.1
|
||||||
|
CT_VLAN=
|
||||||
|
CT_NAMESERVER=1.1.1.1 8.8.8.8
|
||||||
|
CT_SEARCHDOMAIN=example.internal
|
||||||
|
CT_PASSWORD=CHANGE_ME
|
||||||
|
CT_UNPRIVILEGED=1
|
||||||
|
CT_ONBOOT=1
|
||||||
|
CT_FEATURES=nesting=1,keyctl=1
|
||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||||
|
PROJECT_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||||
|
DEFAULT_ENV_FILE="$PROJECT_ROOT/secrets/deploy.secrets.env"
|
||||||
|
|
||||||
|
if [ "$#" -gt 1 ]; then
|
||||||
|
echo "usage: $0 [ /path/to/deploy.secrets.env ]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ENV_FILE="${1:-$DEFAULT_ENV_FILE}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$ENV_FILE"
|
||||||
|
|
||||||
|
: "${CT_ID:?missing CT_ID}"
|
||||||
|
|
||||||
|
pct exec "$CT_ID" -- mkdir -p /root/bootstrap
|
||||||
|
|
||||||
|
for file_name in \
|
||||||
|
install_aw_server.sh \
|
||||||
|
apply_webui_ru_patch.sh \
|
||||||
|
activitywatch-server.service \
|
||||||
|
aw-server.env.example \
|
||||||
|
aw-ru-patch.js \
|
||||||
|
aw-sw-cleanup.js
|
||||||
|
do
|
||||||
|
pct push "$CT_ID" "$PROJECT_ROOT/aw-server/$file_name" "/root/bootstrap/$file_name"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "${AW_SERVER_VERSION:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_DOWNLOAD_URL:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_BIND_HOST:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_PORT:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_WEBUI_DIR:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_DATA_DIR:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_LOG_DIR:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_USER:-}" ] &&
|
||||||
|
[ -n "${AW_SERVER_GROUP:-}" ]; then
|
||||||
|
tmp_env=$(mktemp)
|
||||||
|
trap 'rm -f "$tmp_env"' EXIT
|
||||||
|
{
|
||||||
|
echo "AW_SERVER_VERSION=$AW_SERVER_VERSION"
|
||||||
|
echo "AW_SERVER_DOWNLOAD_URL=$AW_SERVER_DOWNLOAD_URL"
|
||||||
|
echo "AW_SERVER_BIND_HOST=$AW_SERVER_BIND_HOST"
|
||||||
|
echo "AW_SERVER_PORT=$AW_SERVER_PORT"
|
||||||
|
echo "AW_SERVER_WEBUI_DIR=$AW_SERVER_WEBUI_DIR"
|
||||||
|
echo "AW_SERVER_DATA_DIR=$AW_SERVER_DATA_DIR"
|
||||||
|
echo "AW_SERVER_LOG_DIR=$AW_SERVER_LOG_DIR"
|
||||||
|
echo "AW_SERVER_USER=$AW_SERVER_USER"
|
||||||
|
echo "AW_SERVER_GROUP=$AW_SERVER_GROUP"
|
||||||
|
} > "$tmp_env"
|
||||||
|
pct push "$CT_ID" "$tmp_env" "/etc/activitywatch/aw-server.env"
|
||||||
|
pct exec "$CT_ID" -- chmod 0600 /etc/activitywatch/aw-server.env
|
||||||
|
echo "Server env pushed to CT $CT_ID:/etc/activitywatch/aw-server.env"
|
||||||
|
else
|
||||||
|
echo "WARN: AW_SERVER_* variables are incomplete in $ENV_FILE; /etc/activitywatch/aw-server.env was not updated" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Bootstrap artifacts pushed to CT $CT_ID:/root/bootstrap"
|
||||||
Executable
+1
@@ -0,0 +1 @@
|
|||||||
|
deploy.secrets.env
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# Copy to deploy.secrets.env and fill real values.
|
||||||
|
|
||||||
|
PVE_NODE=pve-example
|
||||||
|
CT_ID=203
|
||||||
|
CT_HOSTNAME=activitywatch-server
|
||||||
|
CT_STORAGE=local-lvm
|
||||||
|
CT_TEMPLATE=local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst
|
||||||
|
CT_ROOTFS_SIZE=8G
|
||||||
|
CT_CORES=2
|
||||||
|
CT_MEMORY=2048
|
||||||
|
CT_SWAP=512
|
||||||
|
CT_BRIDGE=vmbr10
|
||||||
|
CT_IP=10.20.30.13/24
|
||||||
|
CT_GW=10.20.30.1
|
||||||
|
CT_VLAN=
|
||||||
|
CT_NAMESERVER=1.1.1.1 8.8.8.8
|
||||||
|
CT_SEARCHDOMAIN=example.internal
|
||||||
|
CT_PASSWORD=CHANGE_ME
|
||||||
|
CT_UNPRIVILEGED=1
|
||||||
|
CT_ONBOOT=1
|
||||||
|
CT_FEATURES=nesting=1,keyctl=1
|
||||||
|
|
||||||
|
AW_SERVER_VERSION=0.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
|
||||||
Executable
+597
@@ -0,0 +1,597 @@
|
|||||||
|
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 'Run this script from an elevated PowerShell session.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "Cannot find aw-watcher-afk.exe under $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 "Missing required ActivityWatch binary: $($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 'No target users resolved. Provide -Users or -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]$ExampleRulesSource,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$StateRoot,
|
||||||
|
[string]$CustomRulesSource
|
||||||
|
)
|
||||||
|
|
||||||
|
New-ActivityWatchDirectory -Path $StateRoot
|
||||||
|
|
||||||
|
$collectorTarget = Join-Path $StateRoot 'browser-domains-native-collector.ps1'
|
||||||
|
$exampleRulesTarget = Join-Path $StateRoot 'web-category-rules.example.json'
|
||||||
|
$rulesTarget = Join-Path $StateRoot 'web-category-rules.json'
|
||||||
|
|
||||||
|
Copy-Item -LiteralPath $CollectorScriptSource -Destination $collectorTarget -Force
|
||||||
|
Copy-Item -LiteralPath $ExampleRulesSource -Destination $exampleRulesTarget -Force
|
||||||
|
|
||||||
|
if ($CustomRulesSource) {
|
||||||
|
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
|
||||||
|
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
CollectorScript = $collectorTarget
|
||||||
|
ExampleRules = $exampleRulesTarget
|
||||||
|
ActiveRules = $rulesTarget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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]$RulesPath,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[int]$PollSeconds,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[int]$PulseSeconds,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[int]$RecoveryIntervalSeconds,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$LaunchScriptPath,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$RecoveryScriptPath,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject[]]$UserTasks,
|
||||||
|
[string]$PackageVersion = 'v0.13.2'
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
rulesPath = $RulesPath
|
||||||
|
launchScript = $LaunchScriptPath
|
||||||
|
recoveryScript = $RecoveryScriptPath
|
||||||
|
}
|
||||||
|
collector = [pscustomobject]@{
|
||||||
|
pollSeconds = $PollSeconds
|
||||||
|
pulseSeconds = $PulseSeconds
|
||||||
|
}
|
||||||
|
recovery = [pscustomobject]@{
|
||||||
|
intervalSeconds = $RecoveryIntervalSeconds
|
||||||
|
taskName = 'ActivityWatch Recovery'
|
||||||
|
}
|
||||||
|
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 "Deployment config not found: $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]`$CollectorScript,
|
||||||
|
[int]`$SessionId
|
||||||
|
)
|
||||||
|
|
||||||
|
`$escapedCollector = [Regex]::Escape(`$CollectorScript)
|
||||||
|
`$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)
|
||||||
|
}
|
||||||
|
|
||||||
|
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||||
|
`$sessionId = (Get-Process -Id `$PID).SessionId
|
||||||
|
`$installRoot = [string]`$config.paths.installRoot
|
||||||
|
`$collectorScript = [string]`$config.paths.collectorScript
|
||||||
|
`$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'
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath `$afkExe)) {
|
||||||
|
throw "Missing aw-watcher-afk.exe: `$afkExe"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath `$windowExe)) {
|
||||||
|
throw "Missing aw-watcher-window.exe: `$windowExe"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) {
|
||||||
|
Start-Process -FilePath `$afkExe -ArgumentList `$serverArgs -WindowStyle Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) {
|
||||||
|
Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Test-Path -LiteralPath `$collectorScript) -and -not (Test-CollectorRunning -CollectorScript `$collectorScript -SessionId `$sessionId)) {
|
||||||
|
Start-Process -FilePath `$powershellExe -ArgumentList @(
|
||||||
|
'-NoProfile',
|
||||||
|
'-WindowStyle', 'Hidden',
|
||||||
|
'-ExecutionPolicy', 'Bypass',
|
||||||
|
'-File', `$collectorScript,
|
||||||
|
'-ConfigPath', `$ConfigPath
|
||||||
|
) -WindowStyle Hidden
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
while (`$true) {
|
||||||
|
try {
|
||||||
|
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||||
|
foreach (`$task in @(`$config.userTasks)) {
|
||||||
|
Start-ScheduledTask -TaskName ([string]`$task.launchTaskName) -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||||
|
Start-Sleep -Seconds ([int]`$config.recovery.intervalSeconds)
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
Set-Content -LiteralPath $Path -Value $content -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
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 Register-ActivityWatchUserTasks {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[pscustomobject[]]$TaskDefinitions,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$LaunchScriptPath,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ConfigPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||||
|
|
||||||
|
foreach ($definition in $TaskDefinitions) {
|
||||||
|
Unregister-ScheduledTask -TaskName $definition.LaunchTaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$action = New-ScheduledTaskAction -Execute $powershellExe -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$LaunchScriptPath`" -ConfigPath `"$ConfigPath`""
|
||||||
|
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $definition.UserId
|
||||||
|
$principal = New-ScheduledTaskPrincipal -UserId $definition.UserId -LogonType InteractiveToken -RunLevel Highest
|
||||||
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 0)
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
$powershellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
|
||||||
|
$action = New-ScheduledTaskAction -Execute $powershellExe -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$RecoveryScriptPath`" -ConfigPath `"$ConfigPath`""
|
||||||
|
$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 failed for $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 failed for $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 failed for $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
|
||||||
Executable
+434
@@ -0,0 +1,434 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
||||||
|
[string]$ServerHost,
|
||||||
|
[int]$ServerPort,
|
||||||
|
[ValidateSet('http', 'https')]
|
||||||
|
[string]$ServerScheme,
|
||||||
|
[string]$RulesPath,
|
||||||
|
[string]$LogPath,
|
||||||
|
[int]$PollSeconds,
|
||||||
|
[int]$PulseSeconds
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
Add-Type -AssemblyName UIAutomationClient
|
||||||
|
Add-Type -AssemblyName UIAutomationTypes
|
||||||
|
|
||||||
|
Add-Type @"
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
public static class NativeAwMethods {
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
public static extern int GetWindowTextLength(IntPtr hWnd);
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
|
||||||
|
function Get-DeploymentConfig {
|
||||||
|
param([string]$Path)
|
||||||
|
if ($Path -and (Test-Path -LiteralPath $Path)) {
|
||||||
|
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$deploymentConfig = Get-DeploymentConfig -Path $ConfigPath
|
||||||
|
$resolvedServerHost = if ($ServerHost) { $ServerHost } elseif ($deploymentConfig) { [string]$deploymentConfig.server.host } else { throw 'ServerHost is required.' }
|
||||||
|
$resolvedServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($deploymentConfig) { [int]$deploymentConfig.server.port } else { 5600 }
|
||||||
|
$resolvedServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($deploymentConfig) { [string]$deploymentConfig.server.scheme } else { 'http' }
|
||||||
|
$resolvedRulesPath = if ($RulesPath) { $RulesPath } elseif ($deploymentConfig) { [string]$deploymentConfig.paths.rulesPath } else { 'C:\ProgramData\ActivityWatch\web-category-rules.json' }
|
||||||
|
$resolvedPollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pollSeconds } else { 5 }
|
||||||
|
$resolvedPulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($deploymentConfig) { [int]$deploymentConfig.collector.pulseSeconds } else { 30 }
|
||||||
|
$resolvedLogsRoot = if ($deploymentConfig) { [string]$deploymentConfig.paths.logsRoot } else { 'C:\ProgramData\ActivityWatch\logs' }
|
||||||
|
$resolvedLogPath = if ($LogPath) { $LogPath } else { Join-Path $resolvedLogsRoot ("browser-domains-{0}.log" -f $env:USERNAME) }
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $resolvedLogsRoot)) {
|
||||||
|
New-Item -Path $resolvedLogsRoot -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$script:ApiBase = '{0}://{1}:{2}/api/0' -f $resolvedServerScheme, $resolvedServerHost, $resolvedServerPort
|
||||||
|
$script:Hostname = $env:COMPUTERNAME
|
||||||
|
$script:SessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
$script:KnownBuckets = @{}
|
||||||
|
$script:LogPath = $resolvedLogPath
|
||||||
|
$script:BrowserMap = @{
|
||||||
|
msedge = 'edge'
|
||||||
|
chrome = 'chrome'
|
||||||
|
brave = 'brave'
|
||||||
|
vivaldi = 'vivaldi'
|
||||||
|
opera = 'opera'
|
||||||
|
firefox = 'firefox'
|
||||||
|
}
|
||||||
|
$script:CategoryRules = @(
|
||||||
|
@{ Name = 'work_business_systems'; Group = 'work'; Domains = @('bitrix24.ru', '1c.ru', 'sbis.ru', 'kontur.ru', 'diadoc.ru', 'nalog.gov.ru', 'gosuslugi.ru') }
|
||||||
|
@{ Name = 'work_docs_collab'; Group = 'work'; Domains = @('office.com', 'sharepoint.com', 'docs.google.com', 'drive.google.com', 'notion.so', 'miro.com') }
|
||||||
|
@{ Name = 'work_dev'; Group = 'work'; Domains = @('github.com', 'gitlab.com', 'bitbucket.org', 'youtrack.cloud', 'atlassian.net') }
|
||||||
|
@{ Name = 'work_communication'; Group = 'work'; Domains = @('teams.microsoft.com', 'outlook.office.com', 'web.telegram.org', 'slack.com', 'zoom.us') }
|
||||||
|
@{ Name = 'neutral_search_reference'; Group = 'neutral'; Domains = @('google.com', 'google.ru', 'yandex.ru', 'bing.com', 'duckduckgo.com', 'wikipedia.org') }
|
||||||
|
@{ Name = 'neutral_news'; Group = 'neutral'; Domains = @('rbc.ru', 'tass.ru', 'ria.ru', 'kommersant.ru', 'vedomosti.ru') }
|
||||||
|
@{ Name = 'personal_social'; Group = 'personal'; Domains = @('vk.com', 'ok.ru', 'facebook.com', 'instagram.com', 'tiktok.com', 'x.com', 'twitter.com') }
|
||||||
|
@{ Name = 'personal_video'; Group = 'personal'; Domains = @('youtube.com', 'youtu.be', 'rutube.ru', 'twitch.tv', 'kinopoisk.ru') }
|
||||||
|
@{ Name = 'personal_marketplace'; Group = 'personal'; Domains = @('ozon.ru', 'wildberries.ru', 'avito.ru', 'aliexpress.com', 'market.yandex.ru') }
|
||||||
|
@{ Name = 'personal_entertainment'; Group = 'personal'; Domains = @('dzen.ru', 'pikabu.ru', 'dtf.ru', 'playground.ru') }
|
||||||
|
)
|
||||||
|
|
||||||
|
function Write-CollectorLog {
|
||||||
|
param([string]$Message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Add-Content -LiteralPath $script:LogPath -Value ('{0} {1}' -f (Get-Date -Format s), $Message)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-DomainMatch {
|
||||||
|
param(
|
||||||
|
[string]$Host,
|
||||||
|
[string]$RuleDomain
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Host) -or [string]::IsNullOrWhiteSpace($RuleDomain)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$left = $Host.ToLowerInvariant()
|
||||||
|
$right = $RuleDomain.ToLowerInvariant()
|
||||||
|
return $left -eq $right -or $left.EndsWith('.' + $right)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-HostFromUrl {
|
||||||
|
param([string]$Url)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Url)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$uri = [Uri]$Url
|
||||||
|
$host = $uri.Host.ToLowerInvariant()
|
||||||
|
if ($host.StartsWith('www.')) {
|
||||||
|
return $host.Substring(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
return $host
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RootDomain {
|
||||||
|
param([string]$Host)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Host)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $Host.Split('.')
|
||||||
|
if ($parts.Count -le 2) {
|
||||||
|
return $Host
|
||||||
|
}
|
||||||
|
|
||||||
|
$suffix = ('{0}.{1}' -f $parts[$parts.Count - 2], $parts[$parts.Count - 1]).ToLowerInvariant()
|
||||||
|
$compoundTlds = @('co.uk', 'com.au', 'co.jp', 'com.br', 'co.in', 'com.tr', 'com.cn')
|
||||||
|
if (($compoundTlds -contains $suffix) -and $parts.Count -ge 3) {
|
||||||
|
return ('{0}.{1}' -f $parts[$parts.Count - 3], $suffix).ToLowerInvariant()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-NormalizedUrl {
|
||||||
|
param([AllowNull()][string]$Value)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Value)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidate = $Value.Trim()
|
||||||
|
if ($candidate.Length -lt 4) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidate -match '^(?i)(search|find|address and search|search with|новая вкладка|new tab)') {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidate -match '^(?i)(https?|file|ftp|chrome|edge|about|view-source)://') {
|
||||||
|
return $candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidate -match '^(?i)localhost([/:]|$)') {
|
||||||
|
return "http://$candidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($candidate -match '^[a-z0-9.-]+\.[a-z]{2,}([/:?#].*)?$') {
|
||||||
|
return "https://$candidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Load-CustomCategoryRules {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not $Path -or -not (Test-Path -LiteralPath $Path)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$parsed = Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
$rules = @()
|
||||||
|
|
||||||
|
if ($parsed.rules) {
|
||||||
|
$sourceRules = @($parsed.rules)
|
||||||
|
}
|
||||||
|
elseif ($parsed -is [System.Collections.IEnumerable]) {
|
||||||
|
$sourceRules = @($parsed)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sourceRules = @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rule in $sourceRules) {
|
||||||
|
if (-not $rule) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = [string]$rule.name
|
||||||
|
$group = [string]$rule.group
|
||||||
|
$domains = @($rule.domains | ForEach-Object { ([string]$_).Trim().ToLowerInvariant() } | Where-Object { $_ })
|
||||||
|
|
||||||
|
if ($name -and $group -and $domains.Count -gt 0) {
|
||||||
|
$rules += @{
|
||||||
|
Name = $name
|
||||||
|
Group = $group
|
||||||
|
Domains = $domains
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rules.Count -gt 0) {
|
||||||
|
$script:CategoryRules = @($rules) + @($script:CategoryRules)
|
||||||
|
Write-CollectorLog ("custom rules loaded: {0}" -f $rules.Count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-CollectorLog ("custom rules load failed: {0}" -f $_.Exception.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WebCategory {
|
||||||
|
param([string]$Host)
|
||||||
|
|
||||||
|
foreach ($rule in $script:CategoryRules) {
|
||||||
|
foreach ($domain in $rule.Domains) {
|
||||||
|
if (Test-DomainMatch -Host $Host -RuleDomain $domain) {
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Name = [string]$rule.Name
|
||||||
|
Group = [string]$rule.Group
|
||||||
|
Rule = [string]$domain
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Name = 'uncategorized'
|
||||||
|
Group = 'neutral'
|
||||||
|
Rule = 'none'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-ForegroundWindowContext {
|
||||||
|
$handle = [NativeAwMethods]::GetForegroundWindow()
|
||||||
|
if ($handle -eq [IntPtr]::Zero) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$processId = [uint32]0
|
||||||
|
[void][NativeAwMethods]::GetWindowThreadProcessId($handle, [ref]$processId)
|
||||||
|
if (-not $processId) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$process = Get-Process -Id ([int]$processId) -ErrorAction SilentlyContinue
|
||||||
|
if (-not $process) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$textLength = [NativeAwMethods]::GetWindowTextLength($handle)
|
||||||
|
$builder = [Text.StringBuilder]::new([Math]::Max($textLength + 1, 260))
|
||||||
|
[void][NativeAwMethods]::GetWindowText($handle, $builder, $builder.Capacity)
|
||||||
|
|
||||||
|
return [pscustomobject]@{
|
||||||
|
Handle = $handle
|
||||||
|
ProcessName = $process.ProcessName.ToLowerInvariant()
|
||||||
|
Title = $builder.ToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-BrowserUrlFromWindow {
|
||||||
|
param([IntPtr]$Handle)
|
||||||
|
|
||||||
|
$root = [System.Windows.Automation.AutomationElement]::FromHandle($Handle)
|
||||||
|
if (-not $root) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$editCondition = [System.Windows.Automation.PropertyCondition]::new(
|
||||||
|
[System.Windows.Automation.AutomationElement]::ControlTypeProperty,
|
||||||
|
[System.Windows.Automation.ControlType]::Edit
|
||||||
|
)
|
||||||
|
|
||||||
|
$edits = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $editCondition)
|
||||||
|
foreach ($edit in $edits) {
|
||||||
|
$valuePattern = $null
|
||||||
|
if ($edit.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$valuePattern)) {
|
||||||
|
$candidate = ConvertTo-NormalizedUrl -Value $valuePattern.Current.Value
|
||||||
|
if ($candidate) {
|
||||||
|
return $candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateFromName = ConvertTo-NormalizedUrl -Value $edit.Current.Name
|
||||||
|
if ($candidateFromName) {
|
||||||
|
return $candidateFromName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[string]$BucketId,
|
||||||
|
[string]$ClientName,
|
||||||
|
[string]$BucketType = 'web.tab.current'
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($script:KnownBuckets.ContainsKey($BucketId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = $ClientName
|
||||||
|
type = $BucketType
|
||||||
|
hostname = $script:Hostname
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json' -Body $body | Out-Null
|
||||||
|
$script:KnownBuckets[$BucketId] = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-Heartbeat {
|
||||||
|
param(
|
||||||
|
[string]$BucketId,
|
||||||
|
[string]$Url,
|
||||||
|
[string]$Title,
|
||||||
|
[string]$BrowserKey,
|
||||||
|
[string]$ProcessName
|
||||||
|
)
|
||||||
|
|
||||||
|
$event = @{
|
||||||
|
timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
url = $Url
|
||||||
|
title = $Title
|
||||||
|
browser = $BrowserKey
|
||||||
|
app = "$ProcessName.exe"
|
||||||
|
source = 'uia-native'
|
||||||
|
sessionId = $script:SessionId
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 4 -Compress
|
||||||
|
|
||||||
|
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-CategoryHeartbeat {
|
||||||
|
param(
|
||||||
|
[string]$Url,
|
||||||
|
[string]$Title,
|
||||||
|
[string]$BrowserKey,
|
||||||
|
[string]$ProcessName,
|
||||||
|
[string]$Domain,
|
||||||
|
[string]$RootDomain,
|
||||||
|
[string]$Category,
|
||||||
|
[string]$CategoryGroup,
|
||||||
|
[string]$CategoryRule
|
||||||
|
)
|
||||||
|
|
||||||
|
$bucketId = 'aw-watcher-web-category_' + $script:Hostname
|
||||||
|
Ensure-Bucket -BucketId $bucketId -ClientName 'aw-watcher-web-category' -BucketType 'aw.web.category'
|
||||||
|
|
||||||
|
$event = @{
|
||||||
|
timestamp = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
url = $Url
|
||||||
|
title = $Title
|
||||||
|
browser = $BrowserKey
|
||||||
|
app = "$ProcessName.exe"
|
||||||
|
domain = $Domain
|
||||||
|
rootDomain = $RootDomain
|
||||||
|
category = $Category
|
||||||
|
categoryGroup = $CategoryGroup
|
||||||
|
categoryRule = $CategoryRule
|
||||||
|
source = 'uia-native'
|
||||||
|
sessionId = $script:SessionId
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 4 -Compress
|
||||||
|
|
||||||
|
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$resolvedPulseSeconds" -ContentType 'application/json' -Body $event | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Load-CustomCategoryRules -Path $resolvedRulesPath
|
||||||
|
Write-CollectorLog ("collector started against {0}" -f $script:ApiBase)
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
try {
|
||||||
|
$context = Get-ForegroundWindowContext
|
||||||
|
if ($context -and $script:BrowserMap.ContainsKey($context.ProcessName)) {
|
||||||
|
$url = Get-BrowserUrlFromWindow -Handle $context.Handle
|
||||||
|
if ($url) {
|
||||||
|
$browserKey = $script:BrowserMap[$context.ProcessName]
|
||||||
|
$domain = Get-HostFromUrl -Url $url
|
||||||
|
if (-not $domain) {
|
||||||
|
$domain = 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootDomain = Get-RootDomain -Host $domain
|
||||||
|
if (-not $rootDomain) {
|
||||||
|
$rootDomain = $domain
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = Get-WebCategory -Host $domain
|
||||||
|
$bucketId = 'aw-watcher-web-{0}_{1}' -f $browserKey, $script:Hostname
|
||||||
|
Ensure-Bucket -BucketId $bucketId -ClientName ('aw-watcher-web-' + $browserKey)
|
||||||
|
Send-Heartbeat -BucketId $bucketId -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName
|
||||||
|
Send-CategoryHeartbeat -Url $url -Title $context.Title -BrowserKey $browserKey -ProcessName $context.ProcessName -Domain $domain -RootDomain $rootDomain -Category $category.Name -CategoryGroup $category.Group -CategoryRule $category.Rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-CollectorLog ("collector error: {0}" -f $_.Exception.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $resolvedPollSeconds
|
||||||
|
}
|
||||||
Executable
+80
@@ -0,0 +1,80 @@
|
|||||||
|
[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
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
|
||||||
|
Import-Module $modulePath -Force
|
||||||
|
|
||||||
|
Assert-Administrator
|
||||||
|
|
||||||
|
$targetUsers = Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain
|
||||||
|
$workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy'
|
||||||
|
$backupRoot = Join-Path $StateRoot 'backups'
|
||||||
|
$logsRoot = Join-Path $StateRoot 'logs'
|
||||||
|
$configPath = Join-Path $StateRoot 'deployment-config.json'
|
||||||
|
$launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1'
|
||||||
|
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||||
|
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||||
|
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||||
|
|
||||||
|
New-ActivityWatchDirectory -Path $StateRoot
|
||||||
|
New-ActivityWatchDirectory -Path $logsRoot
|
||||||
|
|
||||||
|
$archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $Version -WorkingRoot $workingRoot
|
||||||
|
Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null
|
||||||
|
Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
||||||
|
|
||||||
|
$assetResult = Copy-ActivityWatchCollectorAssets -CollectorScriptSource $collectorSource -ExampleRulesSource $exampleRulesSource -StateRoot $StateRoot -CustomRulesSource $CustomRulesPath
|
||||||
|
$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $targetUsers
|
||||||
|
|
||||||
|
Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath
|
||||||
|
Write-ActivityWatchRecoveryScript -Path $recoveryScriptPath -ConfigPath $configPath
|
||||||
|
|
||||||
|
$config = New-ActivityWatchDeploymentConfig `
|
||||||
|
-ServerHost $ServerHost `
|
||||||
|
-ServerPort $ServerPort `
|
||||||
|
-ServerScheme $ServerScheme `
|
||||||
|
-InstallRoot $InstallRoot `
|
||||||
|
-StateRoot $StateRoot `
|
||||||
|
-LogsRoot $logsRoot `
|
||||||
|
-CollectorScript $assetResult.CollectorScript `
|
||||||
|
-RulesPath $assetResult.ActiveRules `
|
||||||
|
-PollSeconds $PollSeconds `
|
||||||
|
-PulseSeconds $PulseSeconds `
|
||||||
|
-RecoveryIntervalSeconds $RecoveryIntervalSeconds `
|
||||||
|
-LaunchScriptPath $launchScriptPath `
|
||||||
|
-RecoveryScriptPath $recoveryScriptPath `
|
||||||
|
-UserTasks $taskDefinitions `
|
||||||
|
-PackageVersion $Version
|
||||||
|
|
||||||
|
Write-ActivityWatchDeploymentConfig -Config $config -Path $configPath
|
||||||
|
Remove-LegacyActivityWatchEntries
|
||||||
|
Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot
|
||||||
|
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath
|
||||||
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
|
Write-Host 'ActivityWatch deployed for users:'
|
||||||
|
$targetUsers | ForEach-Object { Write-Host " - $_" }
|
||||||
|
Write-Host "Server: $ServerScheme://$ServerHost`:$ServerPort"
|
||||||
|
Write-Host "State root: $StateRoot"
|
||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$ServerHost,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$TargetUser,
|
||||||
|
[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
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
|
||||||
|
Import-Module $modulePath -Force
|
||||||
|
|
||||||
|
Assert-Administrator
|
||||||
|
|
||||||
|
$workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy'
|
||||||
|
$backupRoot = Join-Path $StateRoot 'backups'
|
||||||
|
$logsRoot = Join-Path $StateRoot 'logs'
|
||||||
|
$configPath = Join-Path $StateRoot 'deployment-config.json'
|
||||||
|
$launchScriptPath = Join-Path $StateRoot 'launch-watchers.ps1'
|
||||||
|
$recoveryScriptPath = Join-Path $StateRoot 'recovery-loop.ps1'
|
||||||
|
$collectorSource = Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1'
|
||||||
|
$exampleRulesSource = Join-Path $PSScriptRoot 'web-category-rules.example.json'
|
||||||
|
|
||||||
|
New-ActivityWatchDirectory -Path $StateRoot
|
||||||
|
New-ActivityWatchDirectory -Path $logsRoot
|
||||||
|
|
||||||
|
$archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $Version -WorkingRoot $workingRoot
|
||||||
|
Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $InstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null
|
||||||
|
Get-ActivityWatchExecutableMap -InstallRoot $InstallRoot | Out-Null
|
||||||
|
|
||||||
|
$assetResult = Copy-ActivityWatchCollectorAssets -CollectorScriptSource $collectorSource -ExampleRulesSource $exampleRulesSource -StateRoot $StateRoot -CustomRulesSource $CustomRulesPath
|
||||||
|
$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users @($TargetUser)
|
||||||
|
|
||||||
|
Write-ActivityWatchLaunchScript -Path $launchScriptPath -ConfigPath $configPath
|
||||||
|
Write-ActivityWatchRecoveryScript -Path $recoveryScriptPath -ConfigPath $configPath
|
||||||
|
|
||||||
|
$config = New-ActivityWatchDeploymentConfig `
|
||||||
|
-ServerHost $ServerHost `
|
||||||
|
-ServerPort $ServerPort `
|
||||||
|
-ServerScheme $ServerScheme `
|
||||||
|
-InstallRoot $InstallRoot `
|
||||||
|
-StateRoot $StateRoot `
|
||||||
|
-LogsRoot $logsRoot `
|
||||||
|
-CollectorScript $assetResult.CollectorScript `
|
||||||
|
-RulesPath $assetResult.ActiveRules `
|
||||||
|
-PollSeconds $PollSeconds `
|
||||||
|
-PulseSeconds $PulseSeconds `
|
||||||
|
-RecoveryIntervalSeconds $RecoveryIntervalSeconds `
|
||||||
|
-LaunchScriptPath $launchScriptPath `
|
||||||
|
-RecoveryScriptPath $recoveryScriptPath `
|
||||||
|
-UserTasks $taskDefinitions `
|
||||||
|
-PackageVersion $Version
|
||||||
|
|
||||||
|
Write-ActivityWatchDeploymentConfig -Config $config -Path $configPath
|
||||||
|
Remove-LegacyActivityWatchEntries
|
||||||
|
Set-ActivityWatchAcl -InstallRoot $InstallRoot -StateRoot $StateRoot -LogsRoot $logsRoot
|
||||||
|
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $launchScriptPath -ConfigPath $configPath
|
||||||
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
|
Write-Host "ActivityWatch deployed for $TargetUser"
|
||||||
|
Write-Host "Server: $ServerScheme://$ServerHost`:$ServerPort"
|
||||||
|
Write-Host "Install root: $InstallRoot"
|
||||||
|
Write-Host "State root: $StateRoot"
|
||||||
|
Write-Host "Rules file: $($assetResult.ActiveRules)"
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json',
|
||||||
|
[string]$ServerHost,
|
||||||
|
[int]$ServerPort,
|
||||||
|
[ValidateSet('http', 'https')]
|
||||||
|
[string]$ServerScheme,
|
||||||
|
[string[]]$Users,
|
||||||
|
[string]$UserListPath,
|
||||||
|
[string]$Domain,
|
||||||
|
[string]$InstallRoot,
|
||||||
|
[string]$StateRoot,
|
||||||
|
[int]$PollSeconds,
|
||||||
|
[int]$PulseSeconds,
|
||||||
|
[int]$RecoveryIntervalSeconds,
|
||||||
|
[string]$CustomRulesPath,
|
||||||
|
[switch]$RepairPackage,
|
||||||
|
[string]$Version,
|
||||||
|
[string]$PackageUrl,
|
||||||
|
[string]$PackageZipPath
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
|
||||||
|
Import-Module $modulePath -Force
|
||||||
|
|
||||||
|
Assert-Administrator
|
||||||
|
|
||||||
|
$existingConfig = $null
|
||||||
|
if (Test-Path -LiteralPath $ConfigPath) {
|
||||||
|
$existingConfig = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $existingConfig -and (-not $ServerHost)) {
|
||||||
|
throw 'deployment-config.json is missing. Provide -ServerHost and user parameters, or run a deploy script first.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$effectiveStateRoot = if ($StateRoot) { $StateRoot } elseif ($existingConfig) { [string]$existingConfig.paths.stateRoot } else { 'C:\ProgramData\ActivityWatch' }
|
||||||
|
$effectiveInstallRoot = if ($InstallRoot) { $InstallRoot } elseif ($existingConfig) { [string]$existingConfig.paths.installRoot } else { 'C:\Program Files\ActivityWatch' }
|
||||||
|
$effectiveLogsRoot = if ($existingConfig) { [string]$existingConfig.paths.logsRoot } else { Join-Path $effectiveStateRoot 'logs' }
|
||||||
|
$effectiveConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $effectiveStateRoot 'deployment-config.json' }
|
||||||
|
$effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1'
|
||||||
|
$effectiveRecoveryScript = Join-Path $effectiveStateRoot 'recovery-loop.ps1'
|
||||||
|
$effectiveCollector = Join-Path $effectiveStateRoot 'browser-domains-native-collector.ps1'
|
||||||
|
$effectiveRules = Join-Path $effectiveStateRoot 'web-category-rules.json'
|
||||||
|
|
||||||
|
$effectiveServerHost = if ($ServerHost) { $ServerHost } elseif ($existingConfig) { [string]$existingConfig.server.host } else { $null }
|
||||||
|
$effectiveServerPort = if ($PSBoundParameters.ContainsKey('ServerPort')) { $ServerPort } elseif ($existingConfig) { [int]$existingConfig.server.port } else { 5600 }
|
||||||
|
$effectiveServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($existingConfig) { [string]$existingConfig.server.scheme } else { 'http' }
|
||||||
|
$effectivePollSeconds = if ($PSBoundParameters.ContainsKey('PollSeconds')) { $PollSeconds } elseif ($existingConfig) { [int]$existingConfig.collector.pollSeconds } else { 5 }
|
||||||
|
$effectivePulseSeconds = if ($PSBoundParameters.ContainsKey('PulseSeconds')) { $PulseSeconds } elseif ($existingConfig) { [int]$existingConfig.collector.pulseSeconds } else { 30 }
|
||||||
|
$effectiveRecoveryInterval = if ($PSBoundParameters.ContainsKey('RecoveryIntervalSeconds')) { $RecoveryIntervalSeconds } elseif ($existingConfig) { [int]$existingConfig.recovery.intervalSeconds } else { 180 }
|
||||||
|
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
|
||||||
|
|
||||||
|
$effectiveUsers = if ($Users -or $UserListPath) {
|
||||||
|
Normalize-ActivityWatchUsers -Users $Users -UserListPath $UserListPath -Domain $Domain
|
||||||
|
}
|
||||||
|
elseif ($existingConfig) {
|
||||||
|
@($existingConfig.userTasks | ForEach-Object { [string]$_.userId })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw 'Target users are missing.'
|
||||||
|
}
|
||||||
|
|
||||||
|
New-ActivityWatchDirectory -Path $effectiveStateRoot
|
||||||
|
New-ActivityWatchDirectory -Path $effectiveLogsRoot
|
||||||
|
|
||||||
|
if ($RepairPackage) {
|
||||||
|
$workingRoot = Join-Path $env:TEMP 'activitywatch-windows-deploy'
|
||||||
|
$backupRoot = Join-Path $effectiveStateRoot 'backups'
|
||||||
|
$archivePath = Get-ActivityWatchArchive -PackageZipPath $PackageZipPath -PackageUrl $PackageUrl -Version $effectiveVersion -WorkingRoot $workingRoot
|
||||||
|
Install-ActivityWatchPackage -ArchivePath $archivePath -InstallRoot $effectiveInstallRoot -WorkingRoot $workingRoot -BackupRoot $backupRoot | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null
|
||||||
|
|
||||||
|
$assetResult = Copy-ActivityWatchCollectorAssets `
|
||||||
|
-CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') `
|
||||||
|
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
|
||||||
|
-StateRoot $effectiveStateRoot `
|
||||||
|
-CustomRulesSource $CustomRulesPath
|
||||||
|
|
||||||
|
$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $effectiveUsers
|
||||||
|
Write-ActivityWatchLaunchScript -Path $effectiveLaunchScript -ConfigPath $effectiveConfigPath
|
||||||
|
Write-ActivityWatchRecoveryScript -Path $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||||
|
|
||||||
|
$config = New-ActivityWatchDeploymentConfig `
|
||||||
|
-ServerHost $effectiveServerHost `
|
||||||
|
-ServerPort $effectiveServerPort `
|
||||||
|
-ServerScheme $effectiveServerScheme `
|
||||||
|
-InstallRoot $effectiveInstallRoot `
|
||||||
|
-StateRoot $effectiveStateRoot `
|
||||||
|
-LogsRoot $effectiveLogsRoot `
|
||||||
|
-CollectorScript $effectiveCollector `
|
||||||
|
-RulesPath $effectiveRules `
|
||||||
|
-PollSeconds $effectivePollSeconds `
|
||||||
|
-PulseSeconds $effectivePulseSeconds `
|
||||||
|
-RecoveryIntervalSeconds $effectiveRecoveryInterval `
|
||||||
|
-LaunchScriptPath $effectiveLaunchScript `
|
||||||
|
-RecoveryScriptPath $effectiveRecoveryScript `
|
||||||
|
-UserTasks $taskDefinitions `
|
||||||
|
-PackageVersion $effectiveVersion
|
||||||
|
|
||||||
|
Write-ActivityWatchDeploymentConfig -Config $config -Path $effectiveConfigPath
|
||||||
|
Remove-LegacyActivityWatchEntries
|
||||||
|
Set-ActivityWatchAcl -InstallRoot $effectiveInstallRoot -StateRoot $effectiveStateRoot -LogsRoot $effectiveLogsRoot
|
||||||
|
Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $effectiveLaunchScript -ConfigPath $effectiveConfigPath
|
||||||
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||||
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
|
Write-Host 'ActivityWatch hardening/recovery completed.'
|
||||||
|
Write-Host "Config: $effectiveConfigPath"
|
||||||
|
Write-Host "Users repaired: $($effectiveUsers -join ', ')"
|
||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"description": "Override or extend built-in ActivityWatch web categorization rules.",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"name": "work_crm",
|
||||||
|
"group": "work",
|
||||||
|
"domains": [
|
||||||
|
"crm.example.com",
|
||||||
|
"portal.example.org"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "work_erp",
|
||||||
|
"group": "work",
|
||||||
|
"domains": [
|
||||||
|
"erp.example.com",
|
||||||
|
"bi.example.com"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "neutral_training",
|
||||||
|
"group": "neutral",
|
||||||
|
"domains": [
|
||||||
|
"wiki.example.net",
|
||||||
|
"kb.example.net"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personal_social",
|
||||||
|
"group": "personal",
|
||||||
|
"domains": [
|
||||||
|
"social.example.net"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user