diff --git a/aw-server/aw-worktime-influx-exporter.py b/aw-server/aw-worktime-influx-exporter.py index fe3ca15..eb2993f 100644 --- a/aw-server/aw-worktime-influx-exporter.py +++ b/aw-server/aw-worktime-influx-exporter.py @@ -60,6 +60,7 @@ def build_lines_for_day(host, report_date): rows = WORKTIME.aggregate_rows(events, bounds["start"], bounds["end"], host) hourly_rows = WORKTIME.aggregate_hourly_rows(events, bounds["start"], bounds["end"], host) summary = WORKTIME.build_report_summary(rows) + true_active_apps = WORKTIME.build_true_active_apps(host, report_date) lines = [] daily_ts = _timestamp_ns(bounds["start"]) @@ -118,6 +119,28 @@ def build_lines_for_day(host, report_date): daily_ts, ) ) + + for app in true_active_apps: + lines.append( + _line( + "aw_true_active_app_daily", + { + "host": host, + "application": app["application"], + "report_date": report_date.isoformat(), + }, + { + "proved_work_seconds": int(app.get("proved_work_seconds", 0) or 0), + "evidence_events": int(app.get("evidence_events", 0) or 0), + "proved_work_human": app.get("proved_work_human", ""), + "proved_work_hhmm": app.get("proved_work_hhmm", ""), + "last_action": app.get("last_action", ""), + "last_action_local": app.get("last_action_local", ""), + "last_action_utc": app.get("last_action_utc", ""), + }, + daily_ts, + ) + ) return [line for line in lines if line] diff --git a/aw-server/test_aw_worktime_influx_exporter.py b/aw-server/test_aw_worktime_influx_exporter.py index 1c2d5f1..c4ee32e 100644 --- a/aw-server/test_aw_worktime_influx_exporter.py +++ b/aw-server/test_aw_worktime_influx_exporter.py @@ -28,8 +28,40 @@ def test_build_lines_for_day_emits_daily_hourly_and_summary(monkeypatch): ] monkeypatch.setattr(MODULE.WORKTIME, "fetch_events_for_date", lambda host, report_date: (bounds, events)) + monkeypatch.setattr(MODULE.WORKTIME, "build_true_active_apps", lambda host, report_date: []) lines = MODULE.build_lines_for_day("SHARKON2025", date(2026, 5, 14)) assert any(line.startswith("aw_rdp_worktime_daily,") for line in lines) assert any(line.startswith("aw_rdp_worktime_hourly,") for line in lines) assert any(line.startswith("aw_rdp_worktime_summary_daily,") for line in lines) + + +def test_build_lines_for_day_emits_true_active_app_daily(monkeypatch): + bounds = MODULE.WORKTIME.get_report_bounds(date(2026, 5, 14)) + + monkeypatch.setattr(MODULE.WORKTIME, "fetch_events_for_date", lambda host, report_date: (bounds, [])) + monkeypatch.setattr( + MODULE.WORKTIME, + "build_true_active_apps", + lambda host, report_date: [ + { + "application": "1С", + "proved_work_seconds": 2040, + "proved_work_human": "34 мин", + "proved_work_hhmm": "00:34", + "last_action": "1С:Предприятие", + "last_action_local": "09:42", + "last_action_utc": "2026-05-14T06:42:00Z", + "evidence_events": 7, + } + ], + ) + + lines = MODULE.build_lines_for_day("SHARKON2025", date(2026, 5, 14)) + + true_active_line = next(line for line in lines if line.startswith("aw_true_active_app_daily,")) + assert "application=1С" in true_active_line + assert "report_date=2026-05-14" in true_active_line + assert "proved_work_seconds=2040i" in true_active_line + assert "evidence_events=7i" in true_active_line + assert 'last_action="1С:Предприятие"' in true_active_line diff --git a/grafana/detmir-rdp-user-activity-dashboard.json b/grafana/detmir-rdp-user-activity-dashboard.json index 3f897ad..df40c64 100644 --- a/grafana/detmir-rdp-user-activity-dashboard.json +++ b/grafana/detmir-rdp-user-activity-dashboard.json @@ -196,7 +196,7 @@ }, "targets": [ { - "query": "from(bucket: \"aw_metrics\")\n |> range(start: -14d)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_summary_daily\" and (r._field == \"users_count\" or r._field == \"total_active_seconds\") and r.host == \"${host}\")\n |> group(columns:[\"report_date\",\"_field\"])\n |> last()\n |> group()\n |> pivot(rowKey:[\"report_date\"], columnKey:[\"_field\"], valueColumn:\"_value\")\n |> sort(columns:[\"report_date\"])\n |> tail(n:2)\n |> limit(n:1)\n |> map(fn:(r) => ({ r with _value: float(v: r.total_active_seconds) / float(v: r.users_count) / 3600.0 }))", + "query": "from(bucket: \"aw_metrics\")\n |> range(start: -14d)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_summary_daily\" and (r._field == \"users_count\" or r._field == \"total_active_seconds\") and r.host == \"${host}\")\n |> group(columns:[\"report_date\",\"_field\"])\n |> last()\n |> group()\n |> pivot(rowKey:[\"report_date\"], columnKey:[\"_field\"], valueColumn:\"_value\")\n |> sort(columns:[\"report_date\"])\n |> tail(n:2)\n |> limit(n:1)\n |> map(fn:(r) => ({ r with _value: float(v: r.total_active_seconds) / float(v: r.users_count) / 3600.0 }))\n |> keep(columns:[\"_value\"])", "refId": "A" } ], @@ -497,6 +497,206 @@ ], "title": "Подробно: последние 7 дней по сотрудникам", "type": "table" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influxdb_aw" + }, + "description": "Строгий показатель: время засчитывается только когда есть foreground window, not-AFK и дополнительное evidence-событие действия пользователя.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + } + ] + }, + "unit": "h" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 8, + "x": 0, + "y": 33 + }, + "id": 11, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "targets": [ + { + "query": "import \"date\"\nimport \"strings\"\nimport \"timezone\"\noption location = timezone.location(name: \"Europe/Moscow\")\ntoday = strings.substring(v: string(v: date.add(d: 3h, to: date.truncate(t: now(), unit: 1d))), start: 0, end: 10)\nfrom(bucket: \"aw_metrics\")\n |> range(start: -3d)\n |> filter(fn: (r) => r._measurement == \"aw_true_active_app_daily\" and r._field == \"proved_work_seconds\" and r.host == \"${host}\" and r.report_date == today)\n |> group(columns:[\"application\"])\n |> last()\n |> group()\n |> sum()\n |> map(fn:(r)=>({ r with _value: float(v:r._value) / 3600.0 }))", + "refId": "A" + } + ], + "title": "Сегодня: доказанная работа по приложениям", + "type": "stat" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influxdb_aw" + }, + "description": "Приложения, где работа доказана связкой: активная RDP-сессия, не AFK, foreground окно и evidence-событие.", + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Часы" + }, + "properties": [ + { + "id": "unit", + "value": "h" + }, + { + "id": "decimals", + "value": 2 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Evidence" + }, + "properties": [ + { + "id": "decimals", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 16, + "x": 8, + "y": 33 + }, + "id": 12, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "targets": [ + { + "query": "import \"date\"\nimport \"strings\"\nimport \"timezone\"\noption location = timezone.location(name: \"Europe/Moscow\")\ntoday = strings.substring(v: string(v: date.add(d: 3h, to: date.truncate(t: now(), unit: 1d))), start: 0, end: 10)\nbase = from(bucket: \"aw_metrics\")\n |> range(start: -3d)\n |> filter(fn: (r) => r._measurement == \"aw_true_active_app_daily\" and r.host == \"${host}\" and r.report_date == today)\n\nnums = base\n |> filter(fn: (r) => r._field == \"proved_work_seconds\" or r._field == \"evidence_events\")\n |> group(columns:[\"application\",\"_field\"])\n |> last()\n |> group()\n |> pivot(rowKey:[\"application\",\"report_date\"], columnKey:[\"_field\"], valueColumn:\"_value\")\n\ntexts = base\n |> filter(fn: (r) => r._field == \"last_action\" or r._field == \"last_action_local\")\n |> group(columns:[\"application\",\"_field\"])\n |> last()\n |> group()\n |> pivot(rowKey:[\"application\",\"report_date\"], columnKey:[\"_field\"], valueColumn:\"_value\")\n\njoin(tables: {n: nums, t: texts}, on: [\"application\", \"report_date\"])\n |> map(fn:(r)=>({ r with hours: float(v:r.proved_work_seconds) / 3600.0 }))\n |> sort(columns:[\"proved_work_seconds\"], desc:true)\n |> keep(columns:[\"report_date\",\"application\",\"hours\",\"last_action_local\",\"last_action\",\"evidence_events\"])\n |> rename(columns:{report_date:\"Дата\", application:\"Приложение\", hours:\"Часы\", last_action_local:\"Последнее действие\", last_action:\"Контекст\", evidence_events:\"Evidence\"})", + "refId": "A" + } + ], + "title": "Сегодня: доказанная работа по приложениям - детали", + "type": "table" + }, + { + "datasource": { + "type": "influxdb", + "uid": "influxdb_aw" + }, + "description": "История доказанной работы по приложениям. Нули означают, что foreground/AFK есть, но нет достаточного evidence действия.", + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "bars", + "lineInterpolation": "linear", + "barAlignment": 0, + "lineWidth": 1, + "fillOpacity": 35, + "gradientMode": "none", + "spanNulls": false, + "insertNulls": false, + "showPoints": "never", + "pointSize": 5, + "stacking": { + "mode": "normal", + "group": "A" + }, + "axisPlacement": "auto", + "axisLabel": "", + "axisColorMode": "text", + "axisBorderShow": false, + "scaleDistribution": { + "type": "linear" + }, + "hideFrom": { + "tooltip": false, + "viz": false, + "legend": false + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "unit": "h" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 42 + }, + "id": 13, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "targets": [ + { + "query": "from(bucket: \"aw_metrics\")\n |> range(start: -14d)\n |> filter(fn: (r) => r._measurement == \"aw_true_active_app_daily\" and r._field == \"proved_work_seconds\" and r.host == \"${host}\")\n |> group(columns:[\"application\",\"report_date\"])\n |> last()\n |> map(fn:(r)=>({ r with _value: float(v:r._value) / 3600.0 }))\n |> group(columns:[\"application\"])", + "refId": "A" + } + ], + "title": "Доказанная работа по дням и приложениям", + "type": "timeseries" } ], "refresh": "10m",