feat(aw): expand ru webui patch and monitoring assets

This commit is contained in:
igor04091968
2026-04-26 08:54:41 +03:00
parent f54284a0f9
commit 7ef8166504
28 changed files with 1713 additions and 22 deletions
+23
View File
@@ -15,6 +15,8 @@ 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)
TRENDS_NEEDLE='this.activityStore.query_category_time_by_period(r)'
TRENDS_REPLACEMENT='this.activityStore.ensure_loaded(r)'
[[ -f "$PATCH_JS_SRC" ]] || { echo "missing $PATCH_JS_SRC" >&2; exit 1; }
[[ -f "$SW_CLEANUP_SRC" ]] || { echo "missing $SW_CLEANUP_SRC" >&2; exit 1; }
@@ -30,4 +32,25 @@ sed -i 's#</head>#<script src="/js/aw-sw-cleanup.js"></script></head>#' "$INDEX_
sed -i 's#</body>#<script defer="defer" src="/js/aw-ru-patch.js"></script></body>#' "$INDEX_HTML"
cp "$SW_CLEANUP_SRC" "$SERVICE_WORKER"
trends_chunk="$(grep -Rsl "$TRENDS_NEEDLE" "$WEBUI_DIR/js"/*.js 2>/dev/null | head -n 1 || true)"
if [[ -n "$trends_chunk" ]]; then
cp "$trends_chunk" "$trends_chunk.bak.$TS"
python3 - "$trends_chunk" "$TRENDS_NEEDLE" "$TRENDS_REPLACEMENT" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
old = sys.argv[2]
new = sys.argv[3]
content = path.read_text()
if old in content:
path.write_text(content.replace(old, new, 1))
print(f"Trends hotfix applied to {path}")
else:
print(f"Trends hotfix already present in {path}")
PY
else
echo "Trends hotfix skipped: chunk not found"
fi
echo "RU patch applied to $WEBUI_DIR"
+259 -3
View File
@@ -1,11 +1,12 @@
(function () {
window.__awRuPatchVersion = "template-v1";
document.documentElement.setAttribute("data-aw-ru-patch", "template-v1");
window.__awRuPatchVersion = "template-v2";
document.documentElement.setAttribute("data-aw-ru-patch", "template-v2");
const exact = new Map([
["ActivityWatch", "АктивВотч"],
["Home", "Главная"],
["Activity", "Активность"],
["Developer settings", "Настройки разработчика"],
["Timeline", "Таймлайн"],
["Trends", "Тренды"],
["Report", "Отчеты"],
@@ -13,6 +14,8 @@
["Search", "Поиск"],
["Buckets", "Бакеты"],
["Stopwatch", "Секундомер"],
["Categorization helper", "Помощник категоризации"],
["Categorization", "Категоризация"],
["Tools", "Инструменты"],
["Raw Data", "Сырые данные"],
["Summary", "Сводка"],
@@ -20,8 +23,47 @@
["None", "Нет"],
["Date", "Дата"],
["Time", "Время"],
["to", "до"],
["Start of day", "Начало дня"],
["Start of week", "Начало недели"],
["Duration default value", "Значение длительности по умолчанию"],
["Landing page", "Стартовая страница"],
["Theme", "Тема"],
["New release notification", "Уведомление о новом релизе"],
["Use fallback colors", "Использовать резервные цвета"],
["Always count as active pattern", "Шаблон всегда считать активным"],
["Hostname", "Имя хоста"],
["Hostname:", "Имя хоста:"],
["Range", "Диапазон"],
["Range:", "Диапазон:"],
["Options", "Параметры"],
["Toggles", "Переключатели"],
["Enabled", "Включено"],
["Host", "Хост"],
["Version", "Версия"],
["day", "день"],
["week", "неделя"],
["month", "месяц"],
["year", "год"],
["Monday", "Понедельник"],
["Saturday", "Суббота"],
["Sunday", "Воскресенье"],
["Start", "Начало"],
["Stop", "Конец"],
["End", "Конец"],
["Refresh", "Обновить"],
["Apply", "Применить"],
["Filters", "Фильтры"],
["Show options", "Показать параметры"],
["Show last", "Показать за"],
["Show from", "Показать с"],
["Last update", "Последнее обновление"],
["Events shown", "Показано событий"],
["Window", "Окно"],
["New view", "Новый вид"],
["Edit view", "Изменить вид"],
["Show percent", "Показать проценты"],
["Top Window Titles", "Топ заголовков окон"],
["Download", "Скачать"],
["Upload", "Загрузить"],
["Show", "Показать"],
@@ -48,10 +90,143 @@
["Cancel", "Отмена"],
["Delete", "Удалить"],
["API Browser", "API-браузер"],
["Documentation", "Документация"]
["Documentation", "Документация"],
["Restore defaults", "Восстановить значения по умолчанию"],
["Import", "Импорт"],
["Export", "Экспорт"],
["Category Builder", "Конструктор категорий"],
["Query Explorer", "Конструктор запросов"],
["Event List", "Список событий"],
["Raw JSON", "Сырой JSON"],
["Expand list", "Развернуть список"],
["Alerts", "Оповещения"],
["Graph", "Граф"],
["Developer zone", "Зона разработчика"],
["History", "История"],
["Running", "Запущено"],
["Edit", "Изменить"],
["No stopwatch running", "Нет активного секундомера"],
["No label", "Без метки"],
["Start new", "Новый запуск"],
["Check", "Проверить"],
["Name", "Имя"],
["New alert", "Новое правило"],
["Custom regex", "Свое регулярное выражение"],
["Use existing categories", "Использовать существующие категории"],
["Choose a tag...", "Выберите тег..."],
["Rule", "Правило"],
["Open", "Открыть"],
["More", "Еще"],
["Bucket ID", "ID бакета"],
["Updated", "Обновлено"],
["First seen", "Впервые замечен"],
["Last updated", "Последнее обновление"],
["Toggle navigation", "Переключить навигацию"],
["unknown", "неизвестно"],
["Uncategorized", "Без категории"]
]);
const partial = [
["Hello early user,", "Здравствуйте, ранний пользователь,"],
["early days for ActivityWatch.", "ранний этап для ActivityWatch."],
["still early days for ActivityWatch.", "все еще ранний этап для ActivityWatch."],
["It's still early days for ActivityWatch.", "ActivityWatch еще находится на раннем этапе."],
["We've come a long way but we need users (like you!) to provide feedback and help us turn ActivityWatch into a successful project.", "Мы уже прошли большой путь, но нам нужны пользователи вроде вас, чтобы давать обратную связь и помогать развивать проект."],
["Early users like you mean a lot to us, and we hope you'll reach out to us with any ideas you have for improvements!", "Такие ранние пользователи очень важны, и мы рассчитываем на ваши идеи по улучшению системы."],
["If you have a minute, we'd really appreciate you taking our short user survey!", "Если у вас есть минута, пожалуйста, пройдите наш короткий опрос пользователей."],
["If you have a minute to spare, please take the time to fill out our user survey, vote on features in the forum, or just share ActivityWatch with your friends and colleagues.", "Если у вас есть немного времени, пожалуйста, заполните наш пользовательский опрос, проголосуйте за функции на форуме или просто расскажите об ActivityWatch друзьям и коллегам."],
["Spread the word", "Расскажите другим"],
["It's still early days for ActivityWatch. We've come a long way but we need users (like you!) to provide feedback and help us turn ActivityWatch into a successful project. Early users like you mean a lot to us, and we hope you'll reach out to us with any ideas you have for improvements!", "ActivityWatch еще находится на раннем этапе. Мы уже прошли большой путь, но нам нужны пользователи вроде вас, чтобы давать обратную связь и помогать развивать проект. Такие ранние пользователи очень важны, и мы рассчитываем на ваши идеи по улучшению системы."],
["If you are a developer, we hope you can contribute by writing a watcher, visualization, or something else, and share it with us on the forum!", "Если вы разработчик, вы можете помочь проекту: написать watcher, визуализацию или что-то еще и поделиться этим на форуме."],
["Thank you for using ActivityWatch!", "Спасибо за использование ActivityWatch!"],
["If you are not interested in this message, then just ignore it. We won't show it very often.", "Если это сообщение вам не нужно, просто проигнорируйте его. Мы показываем его нечасто."],
["Trends for ", "Тренды за "],
["Activity for ", "Активность за "],
["Активность for ", "Активность за "],
["7 days", "7 дней"],
["30 days", "30 дней"],
["Time active:", "Активное время:"],
["This feature is still in early development.", "Эта функция пока находится на ранней стадии разработки."],
["This tool will help you create categories from your uncategorized time.", "Этот инструмент поможет создавать категории из некатегоризированного времени."],
["Note: These settings are meant for developers who (hopefully) know what they are doing, and as such, may break things unexpectedly.", "Примечание: эти настройки предназначены для разработчиков, которые понимают, что делают, и поэтому могут неожиданно что-нибудь сломать."],
["It works by fetching all uncategorized time for a recent timeperiod, and then finds the most common words (by time, not count) each of which may then either be ignored (if too broad/irrelevant), or used to create a new (sub)category, or to append the word to a pre-existing category rule. Words with less than 60s of time will not be shown.", "Инструмент получает некатегоризированное время за недавний период и ищет самые частые слова по длительности, а не по количеству. Их можно игнорировать, если они слишком общие, использовать для создания новой подкатегории или добавить в уже существующее правило. Слова с длительностью меньше 60 секунд не показываются."],
["When you're done, you can inspect the categories in the Settings page.", "После завершения вы сможете проверить категории на странице Настройки."],
['The time at which days "start", since humans don\'t always go to bed before midnight. Set to 04:00 by default.', 'Время, с которого начинается новый день, так как люди не всегда ложатся спать до полуночи. По умолчанию установлено 04:00.'],
["The weekday which starts a new week.", "День недели, с которого начинается новая неделя."],
["The default duration used for 'show last' in the timeline view.", "Длительность по умолчанию для режима 'показать последние' в таймлайне."],
["The page to open when opening ActivityWatch, or clicking the logo in the top menu.", "Страница, которая открывается при запуске ActivityWatch или при нажатии на логотип в верхнем меню."],
["Change color theme of the application (you need to change categories colors manually to be suitable with dark mode).", "Изменение цветовой темы приложения. Цвета категорий для темного режима нужно настраивать вручную."],
["Devmode enables some features that are still work-in-progress.", "Devmode включает некоторые функции, которые всё ещё находятся в стадии разработки."],
["Querying an entire year is a very heavy operation, and is likely to lead to timeouts. However, the query might be fast enough if you're running aw-server-rust.", "Запрос за целый год является очень тяжёлой операцией и может приводить к таймаутам. Но если у вас работает aw-server-rust, такой запрос может выполняться достаточно быстро."],
["Multidevice query is where events are collected from several hosts in the Activity view. It is an early experiment, that currently does not support browser buckets (or the audible-as-active feature).", "Multidevice query собирает события с нескольких хостов в представлении Активность. Это ранний эксперимент, который пока не поддерживает бакеты браузера и функцию активной вкладки со звуком."],
["The maximum amount of time a server request can take before timing out. Setting this to a high value can be useful for large queries. Note that you need to reload the web UI for it to apply.", "Максимальное время выполнения серверного запроса до срабатывания таймаута. Увеличенное значение может быть полезно для больших запросов. Чтобы изменение вступило в силу, нужно перезагрузить Web UI."],
["We will send you a notification if there is a new release available for download, this check will happen at most once per day.", "При появлении нового релиза для скачивания будет показано уведомление. Проверка выполняется не чаще одного раза в день."],
["Uses the old coloring style for some visualizations when uncategorized or no category color.", "Использует старую схему раскраски для некоторых визуализаций, когда категория не задана или у нее нет цвета."],
["Apps or titles matching this regular expression will never be counted as AFK.", "Приложения или заголовки, подходящие под это регулярное выражение, никогда не будут считаться AFK."],
["Can be used to count time as active, despite no input (like meetings, or games with controllers). An empty string disables it.", "Позволяет считать время активным даже без ввода, например на встречах или в играх с контроллером. Пустая строка отключает функцию."],
["Example expression:", "Пример выражения:"],
["Rules for categorizing events. An event can only have one category. If several categories match, the deepest one will be chosen.", "Правила категоризации событий. Событие может иметь только одну категорию. Если подходят несколько, будет выбрана самая глубокая."],
["You can use the Category Builder to quickly create categories from uncategorized activity.", "Через Конструктор категорий можно быстро создавать категории из некатегоризированной активности."],
["You can also find and share categorization rule presets on the forum.", "Готовые наборы правил категоризации можно находить и публиковать на форуме."],
["For help on how to write categorization rules, see the documentation.", "Как писать правила категоризации, описано в документации."],
["Generate a report of time spent on a certain category of device activity.", "Сформировать отчет по времени в выбранной категории активности устройства."],
["See the documentation for help on how to write queries.", "Как писать запросы, смотрите в документации."],
["See the documentation for help", "См. документацию для справки"],
["See the documentation", "См. документацию"],
["on how to write queries.", "по написанию запросов."],
["Number of events:", "Количество событий:"],
["Query", "Запрос"],
["EventsShowing", "Показано событий"],
["Drag to pan and scroll to zoom", "Перетаскивайте для прокрутки и используйте колесо мыши для масштабирования"],
["Made with", "Сделано с"],
["by the", "командой"],
["Сделано скомандой", "Сделано командой"],
["ActivityWatch developers", "разработчиков ActivityWatch"],
["Report a bug", "Сообщить об ошибке"],
["Ask for help", "Получить помощь"],
["Vote on features", "Голосовать за функции"],
["Donate", "Поддержать"],
["TwitterGitHub", "Twitter GitHub"],
["Using bucket:", "Используется бакет:"],
["This is an early experiment. Data entered here is not shown in the Activity view, yet.", "Это ранний эксперимент. Данные, введенные здесь, пока не отображаются в представлении Активность."],
["Started ", "Запущен "],
["hours ago", "часов назад"],
["days ago", "дней назад"],
["0s ago", "0 с назад"],
["minutes", "минут"],
["Generate a report", "Сформировать отчет"],
["Goal name:", "Имя цели:"],
["Category:", "Категория:"],
["Current:", "Текущее:"],
["Toggle autorefresh every ", "Автообновление каждые "],
["No events match selected criteria. Timeline is not updated.", "Нет событий, соответствующих выбранным критериям. Таймлайн не обновлен."],
["Last update:", "Последнее обновление:"],
["See PR aw-webui#365 for more information.", "Подробности см. в PR aw-webui#365."],
["See PR aw-webui#365", "См. PR aw-webui#365"],
["for more information.", "для дополнительной информации."],
["Displays a graph of categories and their transitions.", "Показывает граф категорий и переходов между ними."],
["Max category depth", "Максимальная глубина категории"],
["Exclude uncategorized", "Исключать некатегоризированное"],
["Just some tools to aid in development and debugging.", "Набор инструментов для разработки и отладки."],
["Nothing to see here right now...", "Сейчас здесь ничего полезного нет..."],
["Are you looking to collect more data? Check out the docs for more watchers.", "Нужно собирать больше данных? Посмотрите документацию по дополнительным watcher-модулям."],
["Are you looking to collect more data?", "Нужно собирать больше данных?"],
["Check out the docs for more watchers.", "Посмотрите документацию по дополнительным watcher-модулям."],
["Check out the docs for more watchers", "Посмотрите документацию по дополнительным watcher-модулям"],
["Click to sort ascending", "Нажмите для сортировки по возрастанию"],
["Host:", "Хост:"],
["Hostname:", "Имя хоста:"],
["Range:", "Диапазон:"],
["serverVersion", "Версия"],
["Version:", "Версия:"],
["Last updated:", "Последнее обновление:"],
["First seen:", "Впервые замечен:"],
["When you're done, you can inspect the categories", "После завершения вы сможете проверить категории"],
["in the Settings page.", "на странице Настройки."],
["Activity (", "Активность ("],
["Exclude time away from computer", "Исключать время отсутствия за компьютером"],
['Common words in "Uncategorized" events', 'Частые слова в событиях "Без категории"'],
["No words with significant duration. You're good to go!", "Нет слов со значимой длительностью. Здесь всё в порядке."],
["Top apps", "Топ приложений"],
["Top titles", "Топ заголовков"],
["Top URLs", "Топ URL"],
@@ -70,6 +245,16 @@
["No zero-duration events found.", "События нулевой длительности не найдены."]
];
const hiddenNavLabels = new Set([
"Raw Data",
"Сырые данные"
]);
const hiddenNavHrefPatterns = [
/\/raw-data\b/i,
/\/raw\b/i
];
function replaceText(text) {
if (!text) return text;
if (exact.has(text.trim())) {
@@ -106,9 +291,77 @@
}
}
function injectStyles() {
if (document.getElementById("aw-ru-hide-noise-style")) return;
const style = document.createElement("style");
style.id = "aw-ru-hide-noise-style";
style.textContent = [
'[href*="/raw-data"], [href*="/raw"], a[data-testid*="raw"], button[data-testid*="raw"] { display: none !important; }',
'[aria-label="Raw Data"], [aria-label="Сырые данные"] { display: none !important; }'
].join("\n");
document.head.appendChild(style);
}
function hideNoiseNavigation(root) {
const links = root.querySelectorAll("a, button, [role='button']");
for (const element of links) {
const text = (element.textContent || "").trim();
const href = (element.getAttribute("href") || "").trim();
if (hiddenNavLabels.has(text) || hiddenNavHrefPatterns.some((pattern) => pattern.test(href))) {
const container = element.closest("li, nav, div") || element;
container.style.display = "none";
}
}
}
let trendsRedirectInFlight = false;
function getTrendsHostFromSettings(settings) {
if (!settings || typeof settings !== "object") return "";
const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : "";
const match = landingpage.match(/\/activity\/([^/]+)/);
return match && match[1] ? match[1] : "";
}
function getTrendsPath(hash) {
if (!hash) return "";
const normalized = hash.startsWith("#") ? hash.slice(1) : hash;
return normalized.split("?")[0];
}
function shouldRedirectTrends(hash) {
const path = getTrendsPath(hash);
return path === "/trends" || path === "/trends/";
}
function redirectBareTrendsRoute() {
if (trendsRedirectInFlight || !shouldRedirectTrends(window.location.hash)) return;
trendsRedirectInFlight = true;
fetch("/api/0/settings/", { credentials: "same-origin" })
.then(function (response) {
if (!response.ok) throw new Error("settings-fetch-failed");
return response.json();
})
.then(function (settings) {
const host = getTrendsHostFromSettings(settings);
if (!host || !shouldRedirectTrends(window.location.hash)) return;
const target = "#/trends/" + encodeURIComponent(host);
if (window.location.hash !== target) {
window.location.replace(target);
}
})
.catch(function () {})
.finally(function () {
trendsRedirectInFlight = false;
});
}
function applyPatch() {
injectStyles();
walk(document.body);
translateAttributes(document.body);
hideNoiseNavigation(document.body);
redirectBareTrendsRoute();
}
const observer = new MutationObserver(function () {
@@ -119,4 +372,7 @@
applyPatch();
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
});
window.addEventListener("hashchange", function () {
redirectBareTrendsRoute();
});
})();
+113
View File
@@ -0,0 +1,113 @@
# 1С-Бухгалтерия: сбор и анализ в Grafana
Документ описывает внедрение контура мониторинга 1С в Grafana через Prometheus и SQL Exporter.
## 1. Целевая схема
1. База 1С (PostgreSQL или MS SQL) содержит KPI views.
2. `sql-exporter` опрашивает views read-only пользователем.
3. `prometheus` собирает метрики и считает alert-rules.
4. `grafana` визуализирует KPI и алерты.
## 2. Подготовка
Рабочий каталог:
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c`
Подготовка env:
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c
cp /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/.env.example \
/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/.env
```
Обязательно изменить:
- `GRAFANA_ADMIN_PASSWORD`
- `ONEC_DSN`
## 3. SQL-представления KPI
Используйте шаблон под вашу СУБД:
- PostgreSQL: `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`
- MS SQL: `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`
После адаптации шаблона под фактические таблицы 1С создайте views в БД.
Быстрое профилирование схемы 1С (PostgreSQL):
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
Проверка KPI views перед запуском:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
## 4. Запуск стека
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c
docker compose up -d
```
## 5. Верификация
Проверка метрик sql-exporter:
```bash
curl -fsS http://127.0.0.1:9399/metrics | rg "onec_"
```
Проверка Prometheus:
```bash
curl -fsS http://127.0.0.1:9090/-/healthy
```
Проверка Grafana:
- URL: `http://<host>:3000`
- Дашборд: `1C Бухгалтерия — Overview`
- Дашборд: `1C Бухгалтерия — SRE`
E2E health-check:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh
```
## 6. KPI и алерты
Метрики:
- `onec_unposted_documents_total`
- `onec_sales_amount_today`
- `onec_overdue_receivables_total`
- `onec_posting_errors_total`
- `onec_data_freshness_seconds`
Алерты:
- `OneCUnpostedDocumentsHigh` (warning)
- `OneCOverdueReceivablesCritical` (critical)
- `OneCSQLExporterDown` (critical)
- `OneCDataFreshnessCritical` (critical)
Файл правил:
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/alerts.yml`
## 7. Эксплуатационный минимум
- Доступ к БД 1С только read-only.
- Права только на KPI views.
- Ротация пароля read-only пользователя не реже 90 дней.
- Бэкап `grafana-data` и `prometheus-data` (docker volumes).
+39
View File
@@ -22,6 +22,7 @@
- Применяет ACL к `C:\Program Files\ActivityWatch`, `C:\ProgramData\ActivityWatch` и каталогу логов.
- Не содержит хардкодов инфраструктуры: сервер, домен, список пользователей и правила передаются параметрами.
- Корректно регистрирует задачи через `-LogonType Interactive` (совместимо с Windows Server, где `InteractiveToken` не поддерживается).
- Поддерживает отключение шумных watcher'ов через `-AfkEnabled:$false` и `-WindowEnabled:$false`.
## Предпосылки
@@ -44,6 +45,31 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process
-CustomPolicyPath .\windows\dlp-policy.example.json
```
Чтобы убрать `afkstatus` из веб-интерфейса, отключите AFK watcher:
```powershell
.\windows\deploy-single-user.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-TargetUser 'CONTOSO\svc.activity.user01' `
-AfkEnabled:$false `
-CustomRulesPath .\windows\web-category-rules.example.json `
-CustomPolicyPath .\windows\dlp-policy.example.json
```
Если нужен только `window`-сигнал без AFK:
```powershell
.\windows\deploy-single-user.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-TargetUser 'CONTOSO\svc.activity.user01' `
-AfkEnabled:$false `
-WindowEnabled:$true `
-CustomRulesPath .\windows\web-category-rules.example.json `
-CustomPolicyPath .\windows\dlp-policy.example.json
```
Локальный пользователь:
```powershell
@@ -94,6 +120,19 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo
-CustomPolicyPath C:\Temp\dlp-policy.json
```
Для quiet-профиля без `afkstatus`:
```powershell
.\windows\deploy-domain-users.ps1 `
-ServerHost aw.example.local `
-ServerPort 5600 `
-Domain CONTOSO `
-UserListPath C:\Temp\aw-users.txt `
-AfkEnabled:$false `
-CustomRulesPath C:\Temp\web-category-rules.json `
-CustomPolicyPath C:\Temp\dlp-policy.json
```
Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен.
## Рекомендуемый phased rollout (изолированный профиль)
+2
View File
@@ -36,6 +36,8 @@ C:\Deploy\AWatch-rus\windows\deploy-ensemble.ps1 `
4. Опционально запускает контроль (`validate-deployment.ps1`) при `-ValidateAfterDeploy`.
5. Пишет итоговый JSON-отчёт в `<StateRoot>\ensemble-report-*.json`.
Для quiet-профиля без `afkstatus` используйте `-AfkEnabled:$false`; если не нужен window watcher, добавьте `-WindowEnabled:$false`.
## Быстрый health-check
```powershell
+7
View File
@@ -0,0 +1,7 @@
COMPOSE_PROJECT_NAME=awrus-1c
GRAFANA_ADMIN_USER=admin
GRAFANA_ADMIN_PASSWORD=change_me_now
GRAFANA_PORT=3000
PROMETHEUS_PORT=9090
SQL_EXPORTER_PORT=9399
ONEC_DSN=postgres://onec_reader:change_me@10.10.10.20:5432/onec_db?sslmode=disable
+1
View File
@@ -0,0 +1 @@
.env
+94
View File
@@ -0,0 +1,94 @@
# 1С-Бухгалтерия → Grafana (production scaffold)
Готовый каркас для непрерывного сбора KPI из 1С и анализа в Grafana:
- `sql-exporter` читает SQL-представления KPI из БД 1С;
- `prometheus` собирает метрики и применяет alert-rules;
- `grafana` поднимает datasource и дашборд автоматически.
## Полные пути
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/.env.example`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/docker-compose.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/sql_exporter.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql-exporter/collectors/onec_accounting_kpi.collector.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/prometheus.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/alerts.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/prometheus/recording_rules.yml`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/grafana/dashboards/1c-accounting-overview.json`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/grafana/dashboards/1c-accounting-sre.json`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh`
- `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh`
## Быстрый запуск
1. Подготовьте env:
```bash
cd /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c
cp .env.example .env
```
2. В `.env` задайте:
- `GRAFANA_ADMIN_USER`, `GRAFANA_ADMIN_PASSWORD`;
- `ONEC_DSN` (DSN read-only пользователя в БД 1С).
3. В БД 1С создайте KPI-представления:
- для PostgreSQL возьмите `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/postgres_views_template.sql`;
- для MS SQL возьмите `/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/sql/mssql_views_template.sql`.
4. Поднимите стек:
```bash
docker compose up -d
```
5. Проверка:
```bash
curl -fsS http://127.0.0.1:9399/metrics | head
curl -fsS http://127.0.0.1:9090/-/healthy
```
Откройте Grafana: `http://<host>:3000`.
## Быстрая диагностика
Профилирование структуры 1С (PostgreSQL):
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/discover_postgres_1c.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
Проверка KPI views:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/validate_kpi_views.sh \
"postgres://user:pass@db-host:5432/db?sslmode=disable"
```
Проверка end-to-end пайплайна:
```bash
sh /mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c/tools/check_pipeline.sh
```
## Что контролируется
- Непроведенные документы (`onec_unposted_documents_total`)
- Продажи текущего дня (`onec_sales_amount_today`)
- Просроченная дебиторка (`onec_overdue_receivables_total`)
- Ошибки проведения за 24ч (`onec_posting_errors_total`)
- Свежесть данных из 1С (`onec_data_freshness_seconds`)
## Принципы безопасности
- только read-only аккаунт к БД 1С;
- минимальные SQL-права только на KPI views;
- секреты только в `.env`, не в git.
+58
View File
@@ -0,0 +1,58 @@
services:
sql-exporter:
image: ghcr.io/burningalchemist/sql_exporter:latest
container_name: awrus-sql-exporter
restart: unless-stopped
env_file:
- .env
command:
- "-config.file=/etc/sql_exporter/sql_exporter.yml"
volumes:
- ./sql-exporter/sql_exporter.yml:/etc/sql_exporter/sql_exporter.yml:ro
- ./sql-exporter/collectors:/etc/sql_exporter/collectors:ro
ports:
- "${SQL_EXPORTER_PORT:-9399}:9399"
prometheus:
image: prom/prometheus:v2.54.1
container_name: awrus-prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus/alerts.yml:/etc/prometheus/alerts.yml:ro
- ./prometheus/recording_rules.yml:/etc/prometheus/recording_rules.yml:ro
- prometheus-data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.path=/prometheus"
- "--storage.tsdb.retention.time=30d"
- "--web.enable-lifecycle"
ports:
- "${PROMETHEUS_PORT:-9090}:9090"
depends_on:
- sql-exporter
grafana:
image: grafana/grafana-oss:11.2.2
container_name: awrus-grafana
restart: unless-stopped
env_file:
- .env
environment:
GF_SECURITY_ADMIN_USER: "${GRAFANA_ADMIN_USER}"
GF_SECURITY_ADMIN_PASSWORD: "${GRAFANA_ADMIN_PASSWORD}"
GF_USERS_DEFAULT_THEME: "dark"
GF_INSTALL_PLUGINS: ""
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./grafana/dashboards:/var/lib/grafana/dashboards:ro
ports:
- "${GRAFANA_PORT:-3000}:3000"
depends_on:
- prometheus
volumes:
prometheus-data:
grafana-data:
+342
View File
@@ -0,0 +1,342 @@
{
"annotations": {
"list": []
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 100
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_unposted_documents_total)",
"refId": "A"
}
],
"title": "Непроведенные документы",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 500000
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_overdue_receivables_total)",
"refId": "A"
}
],
"title": "Просроченная дебиторка",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_sales_amount_today)",
"refId": "A"
}
],
"title": "Продажи за день",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 6,
"x": 18,
"y": 0
},
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "sum(onec_posting_errors_total)",
"refId": "A"
}
],
"title": "Ошибки проведения (24ч)",
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 8
},
"id": 5,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "onec_sales_amount_today",
"legendFormat": "{{company}} {{currency}}",
"refId": "A"
}
],
"title": "Динамика продаж за день",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 8
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "11.2.2",
"targets": [
{
"expr": "onec_overdue_receivables_total",
"legendFormat": "{{company}} {{aging_bucket}} {{currency}}",
"refId": "A"
}
],
"title": "Просроченная дебиторка по корзинам",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"style": "dark",
"tags": [
"1c",
"buh"
],
"templating": {
"list": []
},
"time": {
"from": "now-24h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "1C Бухгалтерия — Overview",
"uid": "onec-accounting-overview",
"version": 1,
"weekStart": ""
}
+109
View File
@@ -0,0 +1,109 @@
{
"annotations": { "list": [] },
"editable": true,
"graphTooltip": 0,
"id": null,
"links": [],
"panels": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"thresholds": {
"mode": "absolute",
"steps": [
{ "color": "green", "value": null },
{ "color": "red", "value": 7200 }
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 0 },
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [{ "expr": "max(onec_data_freshness_seconds)", "refId": "A" }],
"title": "Свежесть данных (сек)",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"0": { "text": "DOWN" },
"1": { "text": "UP" }
},
"type": "value"
}
]
},
"overrides": []
},
"gridPos": { "h": 8, "w": 8, "x": 8, "y": 0 },
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [{ "expr": "max(up{job=\"onec_sql_exporter\"})", "refId": "A" }],
"title": "SQL Exporter",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": {}, "overrides": [] },
"gridPos": { "h": 8, "w": 8, "x": 16, "y": 0 },
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "none",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [{ "expr": "sum(ALERTS{alertstate=\"firing\",service=\"onec\"})", "refId": "A" }],
"title": "Активные алерты 1С",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": {}, "overrides": [] },
"gridPos": { "h": 10, "w": 24, "x": 0, "y": 8 },
"id": 4,
"options": {
"legend": { "displayMode": "list", "placement": "bottom", "showLegend": true },
"tooltip": { "mode": "single", "sort": "none" }
},
"targets": [
{
"expr": "onec_data_freshness_seconds",
"legendFormat": "{{company}} {{source}}",
"refId": "A"
}
],
"title": "Свежесть источников 1С",
"type": "timeseries"
}
],
"refresh": "30s",
"schemaVersion": 39,
"style": "dark",
"tags": ["1c", "sre"],
"templating": { "list": [] },
"time": { "from": "now-24h", "to": "now" },
"timezone": "",
"title": "1C Бухгалтерия — SRE",
"uid": "onec-accounting-sre",
"version": 1,
"weekStart": ""
}
+11
View File
@@ -0,0 +1,11 @@
apiVersion: 1
providers:
- name: 1C-Buhgalteria
orgId: 1
folder: "1C"
type: file
disableDeletion: true
editable: false
options:
path: /var/lib/grafana/dashboards
+10
View File
@@ -0,0 +1,10 @@
apiVersion: 1
datasources:
- name: Prometheus
uid: prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: false
+42
View File
@@ -0,0 +1,42 @@
groups:
- name: onec_accounting_alerts
rules:
- alert: OneCUnpostedDocumentsHigh
expr: onec_unposted_documents_total > 100
for: 15m
labels:
severity: warning
service: onec
annotations:
summary: "Высокий объем непроведенных документов ({{ $labels.company }})"
description: "В базе 1С накопилось {{ $value }} непроведенных документов."
- alert: OneCOverdueReceivablesCritical
expr: sum by (company, currency) (onec_overdue_receivables_total) > 500000
for: 15m
labels:
severity: critical
service: onec
annotations:
summary: "Критичная просроченная дебиторка ({{ $labels.company }})"
description: "Просроченная дебиторская задолженность превысила порог: {{ $value }} {{ $labels.currency }}."
- alert: OneCSQLExporterDown
expr: up{job="onec_sql_exporter"} == 0
for: 3m
labels:
severity: critical
service: monitoring
annotations:
summary: "SQL Exporter недоступен"
description: "Prometheus не может получить метрики 1С от sql-exporter."
- alert: OneCDataFreshnessCritical
expr: onec_data_freshness_seconds > 7200
for: 10m
labels:
severity: critical
service: onec
annotations:
summary: "Данные 1С устарели ({{ $labels.company }})"
description: "Последнее обновление источника {{ $labels.source }} старше 2 часов: {{ $value }} сек."
+16
View File
@@ -0,0 +1,16 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
- /etc/prometheus/recording_rules.yml
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["localhost:9090"]
- job_name: onec_sql_exporter
static_configs:
- targets: ["sql-exporter:9399"]
+11
View File
@@ -0,0 +1,11 @@
groups:
- name: onec_accounting_recording
rules:
- record: onec_overdue_receivables_total:company_currency
expr: sum by (company, currency) (onec_overdue_receivables_total)
- record: onec_unposted_documents_total:company
expr: sum by (company) (onec_unposted_documents_total)
- record: onec_sales_amount_today:company_currency
expr: sum by (company, currency) (onec_sales_amount_today)
@@ -0,0 +1,55 @@
collector_name: onec_accounting_kpi
metrics:
- metric_name: onec_unposted_documents_total
type: gauge
help: "Количество непроведенных документов в 1С."
key_labels:
- company
values: [value]
query: |
SELECT company, value
FROM onec_kpi_unposted_documents;
- metric_name: onec_sales_amount_today
type: gauge
help: "Сумма продаж за текущие сутки (валюта учета)."
key_labels:
- company
- currency
values: [value]
query: |
SELECT company, currency, value
FROM onec_kpi_sales_today;
- metric_name: onec_overdue_receivables_total
type: gauge
help: "Сумма просроченной дебиторской задолженности."
key_labels:
- company
- aging_bucket
- currency
values: [value]
query: |
SELECT company, aging_bucket, currency, value
FROM onec_kpi_overdue_receivables;
- metric_name: onec_posting_errors_total
type: gauge
help: "Количество ошибок проведения документов за последние 24 часа."
key_labels:
- company
values: [value]
query: |
SELECT company, value
FROM onec_kpi_posting_errors_24h;
- metric_name: onec_data_freshness_seconds
type: gauge
help: "Возраст последних данных 1С в секундах."
key_labels:
- company
- source
values: [value]
query: |
SELECT company, source, value
FROM onec_kpi_data_freshness;
+13
View File
@@ -0,0 +1,13 @@
global:
scrape_timeout_offset: 500ms
min_interval: 15s
max_connections: 3
max_idle_connections: 3
target:
data_source_name: "${ONEC_DSN}"
collectors:
- onec_accounting_kpi
collector_files:
- "/etc/sql_exporter/collectors/*.collector.yml"
+86
View File
@@ -0,0 +1,86 @@
-- Шаблон для MS SQL-базы 1С.
-- ВНИМАНИЕ: замените dbo.onec_raw_* на реальные объекты вашей конфигурации 1С.
IF OBJECT_ID('dbo.onec_kpi_unposted_documents', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_unposted_documents;
GO
CREATE VIEW dbo.onec_kpi_unposted_documents AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(COUNT(*) AS float) AS value
FROM dbo.onec_raw_documents d
WHERE d.posted = 0;
GO
IF OBJECT_ID('dbo.onec_kpi_sales_today', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_sales_today;
GO
CREATE VIEW dbo.onec_kpi_sales_today AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(ISNULL(d.currency, 'RUB') AS nvarchar(16)) AS currency,
CAST(ISNULL(SUM(d.amount), 0) AS float) AS value
FROM dbo.onec_raw_sales_documents d
WHERE d.posted = 1
AND CAST(d.doc_date AS date) = CAST(GETDATE() AS date)
GROUP BY ISNULL(d.currency, 'RUB');
GO
IF OBJECT_ID('dbo.onec_kpi_overdue_receivables', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_overdue_receivables;
GO
CREATE VIEW dbo.onec_kpi_overdue_receivables AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END AS nvarchar(16)
) AS aging_bucket,
CAST(ISNULL(currency, 'RUB') AS nvarchar(16)) AS currency,
CAST(SUM(amount_due) AS float) AS value
FROM (
SELECT
r.currency,
r.amount_due,
CASE WHEN DATEDIFF(day, CAST(r.due_date AS date), CAST(GETDATE() AS date)) > 0
THEN DATEDIFF(day, CAST(r.due_date AS date), CAST(GETDATE() AS date))
ELSE 0 END AS age_days
FROM dbo.onec_raw_receivables r
WHERE r.amount_due > 0
) x
WHERE age_days > 0
GROUP BY
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END,
ISNULL(currency, 'RUB');
GO
IF OBJECT_ID('dbo.onec_kpi_posting_errors_24h', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_posting_errors_24h;
GO
CREATE VIEW dbo.onec_kpi_posting_errors_24h AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST(COUNT(*) AS float) AS value
FROM dbo.onec_raw_posting_errors e
WHERE e.error_at >= DATEADD(hour, -24, GETDATE());
GO
IF OBJECT_ID('dbo.onec_kpi_data_freshness', 'V') IS NOT NULL DROP VIEW dbo.onec_kpi_data_freshness;
GO
CREATE VIEW dbo.onec_kpi_data_freshness AS
SELECT
CAST('main' AS nvarchar(64)) AS company,
CAST('documents' AS nvarchar(64)) AS source,
CAST(
CASE
WHEN MAX(d.updated_at) IS NULL THEN 999999
ELSE DATEDIFF(second, MAX(d.updated_at), GETDATE())
END AS float
) AS value
FROM dbo.onec_raw_documents d;
GO
+62
View File
@@ -0,0 +1,62 @@
-- Шаблон для PostgreSQL-базы 1С.
-- ВНИМАНИЕ: замените onec_raw.* на реальные таблицы/представления вашей конфигурации 1С.
CREATE OR REPLACE VIEW onec_kpi_unposted_documents AS
SELECT
'main'::text AS company,
COUNT(*)::double precision AS value
FROM onec_raw.documents d
WHERE d.posted = false;
CREATE OR REPLACE VIEW onec_kpi_sales_today AS
SELECT
'main'::text AS company,
COALESCE(d.currency, 'RUB')::text AS currency,
COALESCE(SUM(d.amount), 0)::double precision AS value
FROM onec_raw.sales_documents d
WHERE d.posted = true
AND d.doc_date::date = CURRENT_DATE
GROUP BY COALESCE(d.currency, 'RUB');
CREATE OR REPLACE VIEW onec_kpi_overdue_receivables AS
SELECT
'main'::text AS company,
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END::text AS aging_bucket,
COALESCE(currency, 'RUB')::text AS currency,
COALESCE(SUM(amount_due), 0)::double precision AS value
FROM (
SELECT
r.currency,
r.amount_due,
GREATEST((CURRENT_DATE - r.due_date::date), 0) AS age_days
FROM onec_raw.receivables r
WHERE r.amount_due > 0
) x
WHERE age_days > 0
GROUP BY
CASE
WHEN age_days BETWEEN 1 AND 7 THEN '1-7'
WHEN age_days BETWEEN 8 AND 30 THEN '8-30'
WHEN age_days BETWEEN 31 AND 90 THEN '31-90'
ELSE '90+'
END,
COALESCE(currency, 'RUB');
CREATE OR REPLACE VIEW onec_kpi_posting_errors_24h AS
SELECT
'main'::text AS company,
COUNT(*)::double precision AS value
FROM onec_raw.posting_errors e
WHERE e.error_at >= NOW() - INTERVAL '24 hours';
CREATE OR REPLACE VIEW onec_kpi_data_freshness AS
SELECT
'main'::text AS company,
'documents'::text AS source,
COALESCE(EXTRACT(EPOCH FROM (NOW() - MAX(d.updated_at))), 999999)::double precision AS value
FROM onec_raw.documents d;
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -eu
STACK_DIR="${1:-/mnt/usb_hdd2/Projects/ActivityWatch-Russian/grafana-1c}"
echo "[*] Checking endpoints"
curl -fsS http://127.0.0.1:9399/metrics >/tmp/awrus-onec-metrics.out
curl -fsS http://127.0.0.1:9090/-/healthy >/tmp/awrus-prom-healthy.out
curl -fsS "http://127.0.0.1:9090/api/v1/query?query=up%7Bjob%3D%22onec_sql_exporter%22%7D" >/tmp/awrus-prom-up.json
curl -fsS "http://127.0.0.1:9090/api/v1/query?query=onec_data_freshness_seconds" >/tmp/awrus-prom-freshness.json
echo "[*] Checking container status"
cd "$STACK_DIR"
docker compose ps
echo "[+] Pipeline health artifacts:"
echo " /tmp/awrus-onec-metrics.out"
echo " /tmp/awrus-prom-healthy.out"
echo " /tmp/awrus-prom-up.json"
echo " /tmp/awrus-prom-freshness.json"
+46
View File
@@ -0,0 +1,46 @@
#!/bin/sh
set -eu
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found" >&2
exit 1
fi
if [ "${1:-}" = "" ]; then
echo "Usage: $0 \"postgres://user:pass@host:5432/db?sslmode=disable\"" >&2
exit 1
fi
DSN="$1"
OUT_DIR="${2:-/tmp/onec-schema-$(date +%Y%m%d-%H%M%S)}"
mkdir -p "$OUT_DIR"
export PGPASSWORD=""
echo "[*] Dumping schemas, tables, columns, candidate accounting objects into $OUT_DIR"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "\dn+" >"$OUT_DIR/schemas.txt"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "\dt+ *.*" >"$OUT_DIR/tables.txt"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "
SELECT table_schema, table_name, column_name, data_type
FROM information_schema.columns
ORDER BY table_schema, table_name, ordinal_position;
" >"$OUT_DIR/columns.tsv"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_type='BASE TABLE'
AND (
table_name ILIKE '%doc%' OR
table_name ILIKE '%sale%' OR
table_name ILIKE '%realiz%' OR
table_name ILIKE '%debt%' OR
table_name ILIKE '%debitor%' OR
table_name ILIKE '%error%' OR
table_name ILIKE '%post%'
)
ORDER BY table_schema, table_name;
" >"$OUT_DIR/candidate_tables.tsv"
echo "[+] Done: $OUT_DIR"
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
set -eu
if ! command -v psql >/dev/null 2>&1; then
echo "psql not found" >&2
exit 1
fi
if [ "${1:-}" = "" ]; then
echo "Usage: $0 \"postgres://user:pass@host:5432/db?sslmode=disable\"" >&2
exit 1
fi
DSN="$1"
VIEWS="
onec_kpi_unposted_documents
onec_kpi_sales_today
onec_kpi_overdue_receivables
onec_kpi_posting_errors_24h
onec_kpi_data_freshness
"
echo "[*] Checking required KPI views"
for view_name in $VIEWS; do
exists="$(psql "$DSN" -X -A -t -v ON_ERROR_STOP=1 -c "SELECT EXISTS (
SELECT 1
FROM information_schema.views
WHERE table_name='${view_name}'
);")"
if [ "$exists" != "t" ]; then
echo "[-] Missing view: $view_name" >&2
exit 2
fi
done
echo "[*] Running test queries"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_unposted_documents LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_sales_today LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_overdue_receivables LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_posting_errors_24h LIMIT 5;"
psql "$DSN" -X -v ON_ERROR_STOP=1 -c "SELECT * FROM onec_kpi_data_freshness LIMIT 5;"
echo "[+] KPI views are valid"
+12 -4
View File
@@ -317,6 +317,8 @@ function New-ActivityWatchDeploymentConfig {
[int]$PulseSeconds,
[Parameter(Mandatory = $true)]
[int]$RecoveryIntervalSeconds,
[bool]$AfkEnabled = $true,
[bool]$WindowEnabled = $true,
[Parameter(Mandatory = $true)]
[string]$LaunchScriptPath,
[Parameter(Mandatory = $true)]
@@ -349,6 +351,10 @@ function New-ActivityWatchDeploymentConfig {
pollSeconds = $PollSeconds
pulseSeconds = $PulseSeconds
}
collectors = [pscustomobject]@{
afkEnabled = $AfkEnabled
windowEnabled = $WindowEnabled
}
recovery = [pscustomobject]@{
intervalSeconds = $RecoveryIntervalSeconds
taskName = 'ActivityWatch Recovery'
@@ -475,20 +481,22 @@ function Start-CollectorScriptIfNeeded {
`$windowExe = Join-Path `$installRoot 'aw-watcher-window\aw-watcher-window.exe'
`$serverArgs = @('--host', [string]`$config.server.host, '--port', [string]`$config.server.port)
`$powershellExe = Join-Path `$env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
`$afkEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]`$config.collectors.afkEnabled } else { `$true }
`$windowEnabled = if (`$config.PSObject.Properties.Name -contains 'collectors' -and `$config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]`$config.collectors.windowEnabled } else { `$true }
if (-not (Test-Path -LiteralPath `$afkExe)) {
if (`$afkEnabled -and -not (Test-Path -LiteralPath `$afkExe)) {
throw "Missing aw-watcher-afk.exe: `$afkExe"
}
if (-not (Test-Path -LiteralPath `$windowExe)) {
if (`$windowEnabled -and -not (Test-Path -LiteralPath `$windowExe)) {
throw "Missing aw-watcher-window.exe: `$windowExe"
}
if (-not (Test-ProcessInSession -Name 'aw-watcher-afk' -SessionId `$sessionId)) {
if (`$afkEnabled -and -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)) {
if (`$windowEnabled -and -not (Test-ProcessInSession -Name 'aw-watcher-window' -SessionId `$sessionId)) {
Start-Process -FilePath `$windowExe -ArgumentList `$serverArgs -WindowStyle Hidden
}
+4
View File
@@ -16,6 +16,8 @@ param(
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
[bool]$AfkEnabled = $true,
[bool]$WindowEnabled = $true,
[string]$CustomRulesPath,
[string]$CustomPolicyPath
)
@@ -74,6 +76,8 @@ $config = New-ActivityWatchDeploymentConfig `
-PollSeconds $PollSeconds `
-PulseSeconds $PulseSeconds `
-RecoveryIntervalSeconds $RecoveryIntervalSeconds `
-AfkEnabled $AfkEnabled `
-WindowEnabled $WindowEnabled `
-LaunchScriptPath $launchScriptPath `
-RecoveryScriptPath $recoveryScriptPath `
-UserTasks $taskDefinitions `
+4
View File
@@ -15,6 +15,8 @@ param(
[int]$PollSeconds = 5,
[int]$PulseSeconds = 30,
[int]$RecoveryIntervalSeconds = 180,
[bool]$AfkEnabled = $true,
[bool]$WindowEnabled = $true,
[string]$CustomRulesPath,
[string]$CustomPolicyPath
)
@@ -72,6 +74,8 @@ $config = New-ActivityWatchDeploymentConfig `
-PollSeconds $PollSeconds `
-PulseSeconds $PulseSeconds `
-RecoveryIntervalSeconds $RecoveryIntervalSeconds `
-AfkEnabled $AfkEnabled `
-WindowEnabled $WindowEnabled `
-LaunchScriptPath $launchScriptPath `
-RecoveryScriptPath $recoveryScriptPath `
-UserTasks $taskDefinitions `
+223 -3
View File
@@ -30,6 +30,16 @@ function Write-EndpointLog {
}
}
function Invoke-AwJsonPost {
param(
[Parameter(Mandatory = $true)][string]$Uri,
[Parameter(Mandatory = $true)][string]$Json
)
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
}
function Ensure-Bucket {
param(
[string]$BucketId,
@@ -47,7 +57,7 @@ function Ensure-Bucket {
hostname = $script:Hostname
} | ConvertTo-Json -Compress
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$BucketId" -ContentType 'application/json' -Body $body | Out-Null
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$BucketId" -Json $body
$script:KnownBuckets[$BucketId] = $true
}
@@ -72,7 +82,7 @@ function Send-EndpointSignalHeartbeat {
} + $Data
} | ConvertTo-Json -Depth 6 -Compress
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -ContentType 'application/json' -Body $payload | Out-Null
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload
}
function Send-DlpIncidentHeartbeat {
@@ -104,7 +114,7 @@ function Send-DlpIncidentHeartbeat {
} + $Data
} | ConvertTo-Json -Depth 7 -Compress
Invoke-RestMethod -Method Post -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -ContentType 'application/json' -Body $payload | Out-Null
Invoke-AwJsonPost -Uri "$($script:ApiBase)/buckets/$bucketId/heartbeat?pulsetime=$script:PulseSeconds" -Json $payload
}
function Get-StringHash {
@@ -288,6 +298,159 @@ function Evaluate-PrintRules {
}
}
function Test-LooksLikeMojibakeQuestionMarks {
param([AllowNull()][string]$Value)
if ([string]::IsNullOrWhiteSpace($Value)) { return $true }
return $Value -match '\?{2,}'
}
function Get-PrintServiceEventSummary {
param([Parameter(Mandatory = $true)]$Event)
$props = @($Event.Properties)
$propertyValues = @()
foreach ($prop in $props) {
$propertyValues += [string]$prop.Value
}
[pscustomobject]@{
RecordId = [string]$Event.RecordId
TimeCreated = if ($Event.TimeCreated) { $Event.TimeCreated.ToString('o') } else { '' }
PropertyCount = $props.Count
DocumentName = if ($props.Count -ge 1) { [string]$props[0].Value } else { '' }
Owner = if ($props.Count -ge 2) { [string]$props[1].Value } else { '' }
PrinterName = if ($props.Count -ge 4) { [string]$props[3].Value } else { '' }
PropertyValues = $propertyValues
}
}
function Get-PrintServiceDocumentFallback {
param(
[Parameter(Mandatory = $true)]$EventSummary,
[string]$Owner,
[string]$PrinterName
)
$preferred = [string]$EventSummary.DocumentName
if (-not (Test-LooksLikeMojibakeQuestionMarks -Value $preferred) -and $preferred -notmatch '^[0-9]+$') {
return $preferred
}
$pathCandidates = New-Object System.Collections.Generic.List[string]
$textCandidates = New-Object System.Collections.Generic.List[string]
foreach ($value in @($EventSummary.PropertyValues)) {
$candidate = [string]$value
if ([string]::IsNullOrWhiteSpace($candidate)) { continue }
if ($candidate -eq $preferred) { continue }
if ($Owner -and $candidate -like "*$Owner*") { continue }
if ($PrinterName -and $candidate -like "*$PrinterName*") { continue }
if (Test-LooksLikeMojibakeQuestionMarks -Value $candidate) { continue }
if ($candidate -match '[\\/:]' -and $candidate -match '\.[A-Za-z0-9]{1,8}$') {
$pathCandidates.Add($candidate)
continue
}
if ($candidate -match '^[0-9]+$') {
continue
}
$textCandidates.Add($candidate)
}
foreach ($candidate in @($pathCandidates)) {
$leaf = Split-Path -Path $candidate -Leaf
if (-not [string]::IsNullOrWhiteSpace($leaf)) {
return $leaf
}
return $candidate
}
foreach ($candidate in @($textCandidates)) {
return $candidate
}
return $null
}
function Write-PrintServiceEventTrace {
param(
[Parameter(Mandatory = $true)]$EventSummary,
[string]$Phase,
[string]$MatchReason,
[string]$ResolvedDocument
)
$properties = if ($EventSummary.PropertyValues) {
($EventSummary.PropertyValues -join ' | ')
}
else {
''
}
Write-EndpointLog (
'printservice-307 phase={0} recordId={1} time={2} owner={3} printer={4} document={5} resolved={6} properties=[{7}] reason={8}' -f
$Phase,
$EventSummary.RecordId,
$EventSummary.TimeCreated,
$EventSummary.Owner,
$EventSummary.PrinterName,
$EventSummary.DocumentName,
$ResolvedDocument,
$properties,
$MatchReason
)
}
function Get-BetterDocumentNameFromPrintServiceEvents {
param(
[string]$Owner,
[string]$PrinterName
)
try {
$startTime = (Get-Date).AddMinutes(-15)
$events = Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-PrintService/Operational'
Id = 307
StartTime = $startTime
} -MaxEvents 200 -ErrorAction Stop
foreach ($event in @($events)) {
$summary = Get-PrintServiceEventSummary -Event $event
$matchReason = 'scan'
$resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $Owner -PrinterName $PrinterName
if ($Owner -and $summary.Owner -and ($summary.Owner -notlike "*$Owner*")) {
$matchReason = 'owner-mismatch'
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument $resolvedDocument
continue
}
if ($PrinterName -and $summary.PrinterName -and ($summary.PrinterName -notlike "*$PrinterName*")) {
$matchReason = 'printer-mismatch'
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument $resolvedDocument
continue
}
if ([string]::IsNullOrWhiteSpace($resolvedDocument)) {
$matchReason = 'no-document-candidate'
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'scan' -MatchReason $matchReason -ResolvedDocument ''
continue
}
$matchReason = if (Test-LooksLikeMojibakeQuestionMarks -Value $summary.DocumentName) { 'fallback-used' } else { 'direct' }
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'selected' -MatchReason $matchReason -ResolvedDocument $resolvedDocument
return $resolvedDocument
}
}
catch {
}
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 }
@@ -308,6 +471,7 @@ $script:KnownBuckets = @{}
$script:Cooldown = @{}
$script:SeenUsb = @{}
$script:SeenPrintJob = @{}
$script:SeenPrintEvent = @{}
$script:LastClipboardHash = $null
$script:PulseSeconds = [Math]::Max($resolvedPollSeconds * 3, 30)
$script:LogPath = $resolvedLogPath
@@ -377,10 +541,19 @@ while ($true) {
$printerName = [string]$job.Name
$documentName = [string]$job.Document
$owner = [string]$job.Owner
$documentNameOriginal = $documentName
if (Test-LooksLikeMojibakeQuestionMarks -Value $documentName) {
$eventDocumentName = Get-BetterDocumentNameFromPrintServiceEvents -Owner $owner -PrinterName $printerName
if ($eventDocumentName) {
$documentName = $eventDocumentName
}
}
Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{
printerName = $printerName
documentName = $documentName
documentNameOriginal = $documentNameOriginal
owner = $owner
}
Evaluate-PrintRules -PrinterName $printerName -DocumentName $documentName -Owner $owner
@@ -396,6 +569,53 @@ while ($true) {
}
catch {
}
try {
$printEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Microsoft-Windows-PrintService/Operational'
Id = 307
StartTime = (Get-Date).AddMinutes(-20)
} -MaxEvents 200 -ErrorAction SilentlyContinue
foreach ($event in @($printEvents)) {
$recordId = [string]$event.RecordId
if (-not $recordId) { continue }
if ($script:SeenPrintEvent.ContainsKey($recordId)) { continue }
$script:SeenPrintEvent[$recordId] = (Get-Date).ToUniversalTime()
$summary = Get-PrintServiceEventSummary -Event $event
$documentName = [string]$summary.DocumentName
$owner = [string]$summary.Owner
$printerName = [string]$summary.PrinterName
$resolvedDocument = Get-PrintServiceDocumentFallback -EventSummary $summary -Owner $owner -PrinterName $printerName
Write-PrintServiceEventTrace -EventSummary $summary -Phase 'emit' -MatchReason 'raw-scan' -ResolvedDocument $resolvedDocument
if (-not [string]::IsNullOrWhiteSpace($owner) -and $owner -notlike "*$env:USERNAME*") {
continue
}
Send-EndpointSignalHeartbeat -SignalType 'print_job' -Data @{
printerName = $printerName
documentName = if ($resolvedDocument) { $resolvedDocument } else { $documentName }
documentNameOriginal = $documentName
owner = $owner
eventRecordId = $recordId
eventSource = 'printservice-307'
}
Evaluate-PrintRules -PrinterName $printerName -DocumentName (if ($resolvedDocument) { $resolvedDocument } else { $documentName }) -Owner $owner
}
$cleanupBeforeEvent = (Get-Date).ToUniversalTime().AddHours(-8)
foreach ($k in @($script:SeenPrintEvent.Keys)) {
$ts = [datetime]$script:SeenPrintEvent[$k]
if ($ts -lt $cleanupBeforeEvent) {
$script:SeenPrintEvent.Remove($k)
}
}
}
catch {
}
}
catch {
Write-EndpointLog ("collector error: {0}" -f $_.Exception.Message)
+7 -12
View File
@@ -13,8 +13,9 @@ param(
[int]$PollSeconds,
[int]$PulseSeconds,
[int]$RecoveryIntervalSeconds,
[bool]$AfkEnabled,
[bool]$WindowEnabled,
[string]$CustomRulesPath,
[string]$CustomPolicyPath,
[switch]$RepairPackage,
[string]$Version,
[string]$PackageUrl,
@@ -45,9 +46,7 @@ $effectiveConfigPath = if ($ConfigPath) { $ConfigPath } else { Join-Path $effect
$effectiveLaunchScript = Join-Path $effectiveStateRoot 'launch-watchers.ps1'
$effectiveRecoveryScript = Join-Path $effectiveStateRoot 'recovery-loop.ps1'
$effectiveCollector = Join-Path $effectiveStateRoot 'browser-domains-native-collector.ps1'
$effectiveEndpointCollector = Join-Path $effectiveStateRoot 'dlp-endpoint-signals-collector.ps1'
$effectiveRules = Join-Path $effectiveStateRoot 'web-category-rules.json'
$effectivePolicy = if ($existingConfig -and $existingConfig.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$existingConfig.paths.policyPath } else { Join-Path $effectiveStateRoot 'dlp-policy.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 }
@@ -55,6 +54,8 @@ $effectiveServerScheme = if ($ServerScheme) { $ServerScheme } elseif ($existingC
$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 }
$effectiveAfkEnabled = if ($PSBoundParameters.ContainsKey('AfkEnabled')) { [bool]$AfkEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$existingConfig.collectors.afkEnabled } else { $true }
$effectiveWindowEnabled = if ($PSBoundParameters.ContainsKey('WindowEnabled')) { [bool]$WindowEnabled } elseif ($existingConfig -and $existingConfig.PSObject.Properties.Name -contains 'collectors' -and $existingConfig.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$existingConfig.collectors.windowEnabled } else { $true }
$effectiveVersion = if ($Version) { $Version } elseif ($existingConfig) { [string]$existingConfig.package.version } else { 'v0.13.2' }
$effectiveUsers = if ($Users -or $UserListPath) {
@@ -81,15 +82,9 @@ Get-ActivityWatchExecutableMap -InstallRoot $effectiveInstallRoot | Out-Null
$assetResult = Copy-ActivityWatchCollectorAssets `
-CollectorScriptSource (Join-Path $PSScriptRoot 'browser-domains-native-collector.ps1') `
-EndpointCollectorScriptSource (Join-Path $PSScriptRoot 'dlp-endpoint-signals-collector.ps1') `
-ExampleRulesSource (Join-Path $PSScriptRoot 'web-category-rules.example.json') `
-ExamplePolicySource (Join-Path $PSScriptRoot 'dlp-policy.example.json') `
-StateRoot $effectiveStateRoot `
-CustomRulesSource $CustomRulesPath `
-CustomPolicySource $CustomPolicyPath
$effectivePolicy = [string]$assetResult.ActivePolicy
$effectiveEndpointCollector = [string]$assetResult.EndpointCollectorScript
-CustomRulesSource $CustomRulesPath
$taskDefinitions = New-ActivityWatchUserTaskDefinitions -Users $effectiveUsers
Write-ActivityWatchLaunchScript -Path $effectiveLaunchScript -ConfigPath $effectiveConfigPath
@@ -103,12 +98,12 @@ $config = New-ActivityWatchDeploymentConfig `
-StateRoot $effectiveStateRoot `
-LogsRoot $effectiveLogsRoot `
-CollectorScript $effectiveCollector `
-EndpointCollectorScript $effectiveEndpointCollector `
-RulesPath $effectiveRules `
-PolicyPath $effectivePolicy `
-PollSeconds $effectivePollSeconds `
-PulseSeconds $effectivePulseSeconds `
-RecoveryIntervalSeconds $effectiveRecoveryInterval `
-AfkEnabled $effectiveAfkEnabled `
-WindowEnabled $effectiveWindowEnabled `
-LaunchScriptPath $effectiveLaunchScript `
-RecoveryScriptPath $effectiveRecoveryScript `
-UserTasks $taskDefinitions `