feat(worktime): add management report layer
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
- `docs/dlp-reliability-roadmap.md` — roadmap повышения надёжности DLP-коллекторов.
|
||||
- `docs/powershell-analysis.md` — статический анализ работоспособности DLP PowerShell-скриптов.
|
||||
- `proxmox/` — шаблонные скрипты подготовки и наполнения CT на стороне Proxmox.
|
||||
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit и RU patch для Web UI.
|
||||
- `aw-server/` — установочные скрипты, env-шаблон, systemd unit, RU patch для Web UI и server-side worktime/management report API на `:5610`.
|
||||
- `ansible/` — Ansible-ensemble для автоматизированного сервера (Debian/CT).
|
||||
- `grafana/` — version-controlled Grafana dashboard JSON для RDP/worktime, DLP/ИБ и overview-экранов.
|
||||
- `clickhouse-1c/` — отдельный analytics stack для **файловой 1С**: ETL, ClickHouse schema, detections, company intelligence marts/forecasting, Grafana catalog и AI Investigator contract.
|
||||
|
||||
@@ -17,6 +17,8 @@ AW_SERVER_PUBLIC_HOST=aw-server
|
||||
AW_WORKTIME_REPORT_BASE=http://aw-server:5610
|
||||
AW_WORKTIME_TZ=Europe/Moscow
|
||||
AW_SERVER_URL=http://127.0.0.1:5600
|
||||
AW_WORKTIME_MANAGER_ALIASES_JSON=/etc/activitywatch/worktime-manager-aliases.json
|
||||
AW_WORKTIME_MANAGER_EXCLUDE_USERS=
|
||||
|
||||
# DLP IOC Configuration
|
||||
AW_DLP_IOC_DIR=/opt/activitywatch/dlp-ioc/output
|
||||
|
||||
+1118
-3
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,8 @@
|
||||
var htmlUrl = reportBase + "/reports/worktime/today?format=html&" + dayQuery;
|
||||
var csvUrl = reportBase + "/reports/worktime/today?format=csv&" + dayQuery;
|
||||
var jsonUrl = reportBase + "/reports/worktime/today?" + dayQuery;
|
||||
var managerHtmlUrl = reportBase + "/reports/worktime/management?format=html&" + dayQuery;
|
||||
var managerJsonUrl = reportBase + "/reports/worktime/management?" + dayQuery;
|
||||
var existing = document.getElementById("aw-report-links");
|
||||
if (!existing) return;
|
||||
|
||||
@@ -17,6 +19,8 @@
|
||||
'<a href="' + htmlUrl + '" style="color:#fcd34d" target="_blank">HTML</a> | ' +
|
||||
'<a href="' + csvUrl + '" style="color:#7dd3fc" target="_blank">CSV</a> | ' +
|
||||
'<a href="' + jsonUrl + '" style="color:#86efac" target="_blank">JSON</a> | ' +
|
||||
'<a href="' + managerHtmlUrl + '" style="color:#fca5a5" target="_blank">Менеджмент</a> | ' +
|
||||
'<a href="' + managerJsonUrl + '" style="color:#c4b5fd" target="_blank">Mgmt JSON</a> | ' +
|
||||
'<a href="#" id="aw-report-toggle" style="color:#f9fafb">Панель</a>';
|
||||
|
||||
var panel = document.createElement("div");
|
||||
@@ -40,10 +44,11 @@
|
||||
'<div style="display:flex;align-items:center;justify-content:space-between;padding:10px 14px;background:#0f172a;color:#fff;font:600 13px/1.2 sans-serif">' +
|
||||
'<div>Отчёт по работе в RDP</div>' +
|
||||
'<div style="display:flex;gap:12px;align-items:center">' +
|
||||
'<a href="' + htmlUrl + '" target="_blank" style="color:#93c5fd;text-decoration:none">Открыть</a>' +
|
||||
'<a href="' + managerHtmlUrl + '" target="_blank" style="color:#fca5a5;text-decoration:none">Открыть менеджмент</a>' +
|
||||
'<a href="' + htmlUrl + '" target="_blank" style="color:#93c5fd;text-decoration:none">Открыть RDP</a>' +
|
||||
'<a href="#" id="aw-report-close" style="color:#fff;text-decoration:none">Закрыть</a>' +
|
||||
"</div></div>" +
|
||||
'<iframe src="' + htmlUrl + '" title="Отчёт по работе в RDP" style="border:0;width:100%;height:calc(100% - 42px);background:#fff"></iframe>';
|
||||
'<iframe src="' + managerHtmlUrl + '" title="Управленческий отчёт по работе в RDP" style="border:0;width:100%;height:calc(100% - 42px);background:#fff"></iframe>';
|
||||
|
||||
document.body.appendChild(panel);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
||||
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
||||
WORKTIME_ALIASES_SRC="$BOOTSTRAP_DIR/worktime-manager-aliases.example.json"
|
||||
WORKTIME_UI_BRIDGE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.py"
|
||||
WORKTIME_UI_BRIDGE_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.service"
|
||||
WORKTIME_UI_BRIDGE_TIMER_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.timer"
|
||||
@@ -110,6 +111,14 @@ if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then
|
||||
systemctl --no-pager --full status aw-worktime-api.service || true
|
||||
fi
|
||||
|
||||
if [[ -f "$WORKTIME_ALIASES_SRC" ]]; then
|
||||
install -d -m 0755 /etc/activitywatch
|
||||
install -m 0644 "$WORKTIME_ALIASES_SRC" /etc/activitywatch/worktime-manager-aliases.json.example
|
||||
if [[ ! -f /etc/activitywatch/worktime-manager-aliases.json ]]; then
|
||||
install -m 0644 "$WORKTIME_ALIASES_SRC" /etc/activitywatch/worktime-manager-aliases.json
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$WORKTIME_UI_BRIDGE_SRC" ]]; then
|
||||
install -m 0755 "$WORKTIME_UI_BRIDGE_SRC" /usr/local/bin/aw-worktime-ui-bridge.py
|
||||
fi
|
||||
|
||||
@@ -87,3 +87,262 @@ def test_aggregate_hourly_rows_splits_interval_by_local_hour():
|
||||
)
|
||||
assert [row["hour_local"] for row in rows] == ["09:00", "10:00"]
|
||||
assert [row["active_seconds"] for row in rows] == [600, 3000]
|
||||
|
||||
|
||||
def test_build_management_payload_creates_actions_for_missing_and_late_users():
|
||||
rows = [
|
||||
{
|
||||
"user": "user1",
|
||||
"user_id": "SHARKON2025\\user1",
|
||||
"active_seconds": 0,
|
||||
"active_hhmm": "00:00",
|
||||
"first_activity": "",
|
||||
"last_activity": "",
|
||||
"idle_seconds": 86400,
|
||||
"sessions_count": 1,
|
||||
"samples_count": 10,
|
||||
"active_samples": 0,
|
||||
"_intervals": [],
|
||||
},
|
||||
{
|
||||
"user": "user5",
|
||||
"user_id": "SHARKON2025\\user5",
|
||||
"active_seconds": 5400,
|
||||
"active_hhmm": "01:30",
|
||||
"first_activity": "2026-05-14T08:30:00Z",
|
||||
"last_activity": "2026-05-14T10:00:00Z",
|
||||
"idle_seconds": 81000,
|
||||
"sessions_count": 1,
|
||||
"samples_count": 180,
|
||||
"active_samples": 180,
|
||||
"_intervals": [
|
||||
(
|
||||
datetime(2026, 5, 14, 8, 30, 0, tzinfo=timezone.utc),
|
||||
datetime(2026, 5, 14, 10, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
],
|
||||
},
|
||||
]
|
||||
payload = MODULE.build_management_payload(rows, "SHARKON2025", datetime(2026, 5, 14, tzinfo=timezone.utc).date())
|
||||
assert payload["summary"]["users_count"] == 2
|
||||
assert payload["summary"]["inactive_users"] == 1
|
||||
assert payload["summary"]["actions_count"] >= 2
|
||||
action_ids = {action["action_id"] for action in payload["actions"]}
|
||||
assert "missing_activity" in action_ids
|
||||
assert "late_start_review" in action_ids
|
||||
assert payload["actions"][0]["priority"] == "critical"
|
||||
|
||||
|
||||
def test_build_management_payload_uses_workday_window_not_midnight_activity():
|
||||
rows = [
|
||||
{
|
||||
"user": "администратор",
|
||||
"user_id": "SHARKON2025\\Администратор",
|
||||
"active_seconds": 39600,
|
||||
"active_hhmm": "11:00",
|
||||
"first_activity": "2026-05-14T00:00:00Z",
|
||||
"last_activity": "2026-05-14T11:00:00Z",
|
||||
"idle_seconds": 0,
|
||||
"sessions_count": 1,
|
||||
"samples_count": 100,
|
||||
"active_samples": 100,
|
||||
"_intervals": [
|
||||
(
|
||||
datetime(2026, 5, 14, 0, 0, 0, tzinfo=timezone.utc),
|
||||
datetime(2026, 5, 14, 11, 0, 0, tzinfo=timezone.utc),
|
||||
)
|
||||
],
|
||||
}
|
||||
]
|
||||
payload = MODULE.build_management_payload(rows, "SHARKON2025", datetime(2026, 5, 14, tzinfo=timezone.utc).date())
|
||||
roster = payload["rows"][0]
|
||||
assert roster["calendar_active_hhmm"] == "11:00"
|
||||
assert roster["workday_active_hhmm"] == "02:00"
|
||||
assert roster["coverage_pct"] == 22.22
|
||||
assert payload["summary"]["calendar_total_active_hhmm"] == "11:00"
|
||||
assert payload["summary"]["workday_total_active_hhmm"] == "02:00"
|
||||
|
||||
|
||||
def test_build_management_payload_applies_alias_and_executive_summary():
|
||||
original = MODULE.load_manager_aliases
|
||||
try:
|
||||
MODULE.load_manager_aliases = lambda: {
|
||||
"sharkon2025\\user1": {
|
||||
"display_name": "Иван Петров",
|
||||
"manager": "Руководитель смены",
|
||||
"department": "Бухгалтерия",
|
||||
"role": "Оператор 1С",
|
||||
}
|
||||
}
|
||||
rows = [
|
||||
{
|
||||
"user": "user1",
|
||||
"user_id": "SHARKON2025\\user1",
|
||||
"active_seconds": 0,
|
||||
"active_hhmm": "00:00",
|
||||
"first_activity": "",
|
||||
"last_activity": "",
|
||||
"idle_seconds": 86400,
|
||||
"sessions_count": 1,
|
||||
"samples_count": 10,
|
||||
"active_samples": 0,
|
||||
"_intervals": [],
|
||||
}
|
||||
]
|
||||
payload = MODULE.build_management_payload(rows, "SHARKON2025", datetime(2026, 5, 14, tzinfo=timezone.utc).date())
|
||||
row = payload["rows"][0]
|
||||
assert row["user"] == "Иван Петров"
|
||||
assert row["manager_owner"] == "Руководитель смены"
|
||||
assert row["department"] == "Бухгалтерия"
|
||||
assert payload["actions"][0]["owner"] == "Руководитель смены"
|
||||
assert payload["executive"]["portfolio_state"] == "critical"
|
||||
assert payload["executive"]["focus_items"][0]["owner"] == "Руководитель смены"
|
||||
finally:
|
||||
MODULE.load_manager_aliases = original
|
||||
|
||||
|
||||
def test_render_management_html_contains_action_queue():
|
||||
payload = {
|
||||
"generated_at_utc": "2026-05-14T12:00:00Z",
|
||||
"host": "SHARKON2025",
|
||||
"report_date": "2026-05-14",
|
||||
"report_timezone": "Europe/Moscow",
|
||||
"workday": {
|
||||
"start_local": "2026-05-14T09:00:00+03:00",
|
||||
"end_local": "2026-05-14T18:00:00+03:00",
|
||||
"expected_seconds_per_user": 32400,
|
||||
"expected_hhmm_per_user": "09:00",
|
||||
"target_coverage_pct": 75,
|
||||
"low_coverage_pct": 35,
|
||||
},
|
||||
"summary": {
|
||||
"users_count": 1,
|
||||
"active_users": 0,
|
||||
"inactive_users": 1,
|
||||
"on_target_users": 0,
|
||||
"below_target_users": 0,
|
||||
"portfolio_coverage_pct": 0.0,
|
||||
"actions_count": 1,
|
||||
"critical_actions_count": 1,
|
||||
"high_actions_count": 0,
|
||||
"total_active_seconds": 0,
|
||||
"total_active_hhmm": "00:00",
|
||||
"calendar_total_active_hhmm": "00:00",
|
||||
"workday_total_active_hhmm": "00:00",
|
||||
"first_activity": "",
|
||||
"last_activity": "",
|
||||
"top_user": "",
|
||||
"top_user_active_hhmm": "00:00",
|
||||
},
|
||||
"actions": [
|
||||
{
|
||||
"action_id": "missing_activity",
|
||||
"priority": "critical",
|
||||
"owner": "Руководитель смены",
|
||||
"user_id": "SHARKON2025\\user1",
|
||||
"deadline_hint": "today",
|
||||
"reason": "Нет активности",
|
||||
"recommended_action": "Проверить сотрудника",
|
||||
"evidence": {},
|
||||
}
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"user": "Иван Петров",
|
||||
"user_id": "SHARKON2025\\user1",
|
||||
"canonical_user_id": "SHARKON2025\\user1",
|
||||
"manager_owner": "Руководитель смены",
|
||||
"department": "Бухгалтерия",
|
||||
"active_hhmm": "00:00",
|
||||
"calendar_active_hhmm": "00:00",
|
||||
"workday_active_hhmm": "00:00",
|
||||
"coverage_pct": 0.0,
|
||||
"status": "inactive",
|
||||
"first_activity_local": "",
|
||||
"last_activity_local": "",
|
||||
"workday_first_activity_local": "",
|
||||
"workday_last_activity_local": "",
|
||||
"sessions_count": 1,
|
||||
}
|
||||
],
|
||||
"executive": {
|
||||
"portfolio_state": "critical",
|
||||
"headline": "Есть 1 критичный вопрос, требующий решения сегодня.",
|
||||
"message": "Активны 0 из 1 сотрудников. Покрытие рабочего окна 0.0%.",
|
||||
"focus_items": [
|
||||
{
|
||||
"priority": "critical",
|
||||
"owner": "Руководитель смены",
|
||||
"title": "missing_activity",
|
||||
"reason": "Нет активности",
|
||||
"recommended_action": "Проверить сотрудника",
|
||||
}
|
||||
],
|
||||
"stale_sources": [],
|
||||
},
|
||||
"trend": [
|
||||
{
|
||||
"report_date": "2026-05-14",
|
||||
"users_count": 1,
|
||||
"active_users": 0,
|
||||
"inactive_users": 1,
|
||||
"workday_total_active_hhmm": "00:00",
|
||||
"portfolio_coverage_pct": 0.0,
|
||||
"actions_count": 1,
|
||||
"critical_actions_count": 1,
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
{
|
||||
"label": "RDP worktime sessions",
|
||||
"status": "ok",
|
||||
"status_label": "fresh",
|
||||
"bucket_id": "aw-worktime-sessions_SHARKON2025",
|
||||
"timestamp": "2026-05-14T12:00:00Z",
|
||||
"age_seconds": 30,
|
||||
"required": True,
|
||||
"summary": "fresh (30s)",
|
||||
"event_summary": "queue=0 failures=0 flushed=10",
|
||||
}
|
||||
],
|
||||
"bucket_id": "aw-worktime-sessions_SHARKON2025",
|
||||
"report_bounds": {
|
||||
"start_utc": "2026-05-13T21:00:00Z",
|
||||
"end_utc": "2026-05-14T20:59:59Z",
|
||||
},
|
||||
}
|
||||
html = MODULE.render_management_html(payload, selected_day="today")
|
||||
assert "Управленческий отчёт по работе в RDP" in html
|
||||
assert "Очередь действий руководителя" in html
|
||||
assert "missing_activity" in html
|
||||
assert "Тренд за" in html
|
||||
assert "Свежесть источников данных" in html
|
||||
assert "Что делать сегодня" in html
|
||||
assert "Иван Петров" in html
|
||||
assert "Руководитель смены" in html
|
||||
|
||||
|
||||
def test_build_source_freshness_uses_freshest_candidate_bucket():
|
||||
original = MODULE.latest_bucket_event
|
||||
try:
|
||||
def fake_latest(bucket_id):
|
||||
if bucket_id == "aw-file-operations_SHARKON2025":
|
||||
return {
|
||||
"timestamp": "2026-05-14T12:00:00Z",
|
||||
"data": {"signalType": "collector_health", "queueDepth": 9, "sendFailures": 3, "eventsFlushed": 10},
|
||||
}
|
||||
if bucket_id == "aw-file-operations_10.10.10.13":
|
||||
return {
|
||||
"timestamp": "2026-05-23T12:00:00Z",
|
||||
"data": {"signalType": "collector_health", "queueDepth": 0, "sendFailures": 0, "eventsFlushed": 50},
|
||||
}
|
||||
return None
|
||||
|
||||
MODULE.latest_bucket_event = fake_latest
|
||||
sources, actions = MODULE.build_source_freshness("SHARKON2025")
|
||||
file_source = next(source for source in sources if source["source_id"] == "file_operations")
|
||||
assert file_source["bucket_id"] == "aw-file-operations_10.10.10.13"
|
||||
assert file_source["status"] == "ok"
|
||||
assert not any(action["evidence"].get("source_id") == "file_operations" for action in actions)
|
||||
finally:
|
||||
MODULE.latest_bucket_event = original
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"users": {
|
||||
"SHARKON2025\\USER1": {
|
||||
"display_name": "Иван Петров",
|
||||
"manager": "Руководитель смены",
|
||||
"department": "Бухгалтерия",
|
||||
"role": "Оператор 1С",
|
||||
"notes": "Дневная смена",
|
||||
"exclude": false
|
||||
},
|
||||
"SHARKON2025\\USER4": {
|
||||
"display_name": "Мария Сидорова",
|
||||
"manager": "Руководитель смены",
|
||||
"department": "Бухгалтерия",
|
||||
"role": "Оператор 1С",
|
||||
"exclude": false
|
||||
},
|
||||
"SHARKON2025\\ADMINISTRATOR": {
|
||||
"display_name": "Администратор",
|
||||
"manager": "Ops",
|
||||
"department": "Инфраструктура",
|
||||
"role": "Системный администратор",
|
||||
"exclude": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,18 @@ Live endpoint:
|
||||
|
||||
- `http://aw-local-server:5610/reports/worktime/today?format=html&date=2026-05-15`
|
||||
|
||||
Дополнительно в management-only контуре есть отдельный управленческий отчёт:
|
||||
|
||||
- `http://aw-local-server:5610/reports/worktime/management?format=html&day=today`
|
||||
|
||||
Он показывает не только roster, но и:
|
||||
|
||||
- `Что делать сегодня`;
|
||||
- покрытие рабочего окна против календарной активности;
|
||||
- очередь действий руководителя;
|
||||
- trend;
|
||||
- свежесть источников данных.
|
||||
|
||||
## 7. AW-rus: DLP review, rules и события
|
||||
|
||||
Экран AW-rus по bucket `aw-dlp-endpoint-signals_*`: здесь видны живые DLP-события, review-вердикты, правила, case-management и нижняя лента событий. Это прямое доказательство, что данные реально приходят на сервер до агрегации в InfluxDB/Grafana.
|
||||
|
||||
@@ -264,7 +264,7 @@ Deployment/tooling:
|
||||
| ActivityWatch API | `5600` | основной API buckets/events/settings |
|
||||
| Policy Engine | `5601` | централизованная политика DLP |
|
||||
| Case Management | `5602` | кейсы расследования |
|
||||
| Worktime API | `5610` | отчёты worktime CSV/JSON |
|
||||
| Worktime API | `5610` | отчёты worktime CSV/JSON/HTML и management view `/reports/worktime/management` |
|
||||
|
||||
### 6.2 Systemd units
|
||||
|
||||
@@ -608,4 +608,3 @@ python3 scripts/dlp-admin-cli.py cases list --limit 50
|
||||
- `docs/windows/validation.md`
|
||||
- `docs/runbook.md`
|
||||
- `docs/worktime_aql_detmir.md`
|
||||
|
||||
|
||||
@@ -80,6 +80,42 @@ ls -l /opt/activitywatch/webui-ru/js/
|
||||
Playbook вычисляет `durationDefault` автоматически (включая смены через полночь) и выставляет:
|
||||
`/api/0/settings/startOfDay` и `/api/0/settings/durationDefault`.
|
||||
|
||||
### Management report API
|
||||
|
||||
На `:5610` сейчас есть два server-side отчёта:
|
||||
|
||||
- классический `RDP worktime`:
|
||||
- `/reports/worktime/today`
|
||||
- форматы: `json`, `csv`, `html`
|
||||
- управленческий `management`:
|
||||
- `/reports/worktime/management`
|
||||
- форматы: `json`, `csv`, `html`
|
||||
|
||||
Управленческий отчёт дополнительно показывает:
|
||||
|
||||
- рабочее окно отдельно от календарной активности;
|
||||
- очередь действий руководителя;
|
||||
- тренд за несколько дней;
|
||||
- свежесть источников данных;
|
||||
- executive summary `Что делать сегодня`.
|
||||
|
||||
Практические проверки:
|
||||
|
||||
```sh
|
||||
curl -fsS 'http://127.0.0.1:5610/reports/worktime/today?day=today' | jq '.[:5]'
|
||||
curl -fsS 'http://127.0.0.1:5610/reports/worktime/management?day=today' | jq '.summary,.executive'
|
||||
curl -fsS 'http://127.0.0.1:5610/reports/worktime/management?format=html&day=today' | head
|
||||
```
|
||||
|
||||
Важно:
|
||||
|
||||
- первый `management`-запрос после очистки cache может быть тяжёлым, потому что
|
||||
API пересчитывает trend и source freshness;
|
||||
- повторные запросы должны быть быстрыми за счёт cache в
|
||||
`/var/lib/activitywatch/worktime-cache`;
|
||||
- alias-файл для нормализации сотрудников по умолчанию:
|
||||
`/etc/activitywatch/worktime-manager-aliases.json`.
|
||||
|
||||
## Типовые инциденты
|
||||
|
||||
### Hayabusa: операторский сценарий по умолчанию
|
||||
|
||||
Reference in New Issue
Block a user