feat(worktime): publish RDP activity to Grafana

This commit is contained in:
igor04091968
2026-05-15 01:23:23 +03:00
parent 22de305c8a
commit 873e43fb2e
10 changed files with 726 additions and 18 deletions
+48
View File
@@ -313,6 +313,13 @@
AW_SERVER_URL=http://127.0.0.1:5600
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
AW_WORKTIME_INFLUX_ENABLED={{ 'true' if (aw_worktime_influx_enabled | default(false) | bool) else 'false' }}
AW_WORKTIME_INFLUX_URL={{ aw_worktime_influx_url | default('') }}
AW_WORKTIME_INFLUX_ORG={{ aw_worktime_influx_org | default('proxmox') }}
AW_WORKTIME_INFLUX_BUCKET={{ aw_worktime_influx_bucket | default('aw_metrics') }}
AW_WORKTIME_INFLUX_HOSTS={{ aw_worktime_influx_hosts | default('SHARKON2025') }}
AW_WORKTIME_INFLUX_DAYS={{ aw_worktime_influx_days | default('today,yesterday') }}
AW_WORKTIME_INFLUX_TOKEN={{ aw_worktime_influx_token | default('') }}
- name: Создать каталог DLP policy engine
ansible.builtin.file:
@@ -652,6 +659,15 @@
group: root
mode: "0755"
- name: Установить скрипт AW worktime Influx exporter
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/aw-worktime-influx-exporter.py"
dest: /usr/local/bin/aw-worktime-influx-exporter.py
owner: root
group: root
mode: "0755"
when: aw_worktime_influx_enabled | default(false) | bool
- name: Установить скрипт aw-health-check
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/health-check.sh"
@@ -700,6 +716,24 @@
group: root
mode: "0644"
- name: Установить systemd unit AW worktime Influx exporter
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/aw-worktime-influx-exporter.service"
dest: /etc/systemd/system/aw-worktime-influx-exporter.service
owner: root
group: root
mode: "0644"
when: aw_worktime_influx_enabled | default(false) | bool
- name: Установить systemd timer AW worktime Influx exporter
ansible.builtin.copy:
src: "{{ aw_repo_root }}/aw-server/aw-worktime-influx-exporter.timer"
dest: /etc/systemd/system/aw-worktime-influx-exporter.timer
owner: root
group: root
mode: "0644"
when: aw_worktime_influx_enabled | default(false) | bool
- name: Перезагрузить systemd после установки AW worktime API
ansible.builtin.systemd:
daemon_reload: true
@@ -790,6 +824,20 @@
state: started
failed_when: false
- name: Включить и перезапустить AW worktime Influx exporter timer
ansible.builtin.systemd:
name: aw-worktime-influx-exporter.timer
enabled: true
state: restarted
when: aw_worktime_influx_enabled | default(false) | bool
- name: Выполнить разовый прогон AW worktime Influx exporter
ansible.builtin.systemd:
name: aw-worktime-influx-exporter.service
state: started
failed_when: false
when: aw_worktime_influx_enabled | default(false) | bool
- name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper)
ansible.builtin.command:
cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh"
+7
View File
@@ -10,6 +10,13 @@ aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_worktime_report_base: "http://10.10.10.13:5610"
aw_worktime_timezone: "Europe/Moscow"
aw_worktime_influx_enabled: false
aw_worktime_influx_url: "http://10.10.10.10:8086"
aw_worktime_influx_org: "proxmox"
aw_worktime_influx_bucket: "aw_metrics"
aw_worktime_influx_hosts: "SHARKON2025"
aw_worktime_influx_days: "today,yesterday"
aw_worktime_influx_token: ""
aw_repo_root: "{{ playbook_dir | dirname }}"
+6
View File
@@ -10,6 +10,12 @@ aw_server_user: "activitywatch"
aw_server_group: "activitywatch"
aw_worktime_report_base: "http://10.10.10.13:5610"
aw_worktime_timezone: "Europe/Moscow"
aw_worktime_influx_enabled: false
aw_worktime_influx_url: "http://10.10.10.10:8086"
aw_worktime_influx_org: "proxmox"
aw_worktime_influx_bucket: "aw_metrics"
aw_worktime_influx_hosts: "SHARKON2025"
aw_worktime_influx_days: "today,yesterday"
aw_repo_root: "/mnt/usb_hdd2/Projects/ActivityWatch-Russian"
+80 -18
View File
@@ -99,6 +99,21 @@ def resolve_report_date(day=None, date_text=None):
return now_local.date()
def get_report_bounds(report_date):
start_local = datetime(report_date.year, report_date.month, report_date.day, tzinfo=REPORT_TZ)
end_local = start_local + timedelta(days=1) - timedelta(seconds=1)
start = start_local.astimezone(timezone.utc)
end = end_local.astimezone(timezone.utc)
end_exclusive = end + timedelta(seconds=1)
return {
"start_local": start_local,
"end_local": end_local,
"start": start,
"end": end,
"end_exclusive": end_exclusive,
}
def _is_machine_user(user: str):
u = (user or "").strip().lower()
return u.endswith("$") or u in {"system", "localservice", "networkservice"}
@@ -169,7 +184,8 @@ def _merge_intervals(intervals):
return merged
def aggregate_rows(events, start, end, host):
def _collect_user_rows(events, start, end, host):
end_exclusive = end + timedelta(seconds=1)
by_user = {}
by_identity = {}
@@ -212,10 +228,14 @@ def aggregate_rows(events, start, end, host):
if active:
row["active_samples"] += 1
interval_start = sample["_ts"]
interval_end = min(sample["_ts"] + timedelta(seconds=sample_seconds), end + timedelta(seconds=1))
interval_end = min(sample["_ts"] + timedelta(seconds=sample_seconds), end_exclusive)
if interval_end > interval_start:
row["intervals"].append((interval_start, interval_end))
return by_user
def aggregate_rows(events, start, end, host):
by_user = _collect_user_rows(events, start, end, host)
rows = []
full_range = int((end - start).total_seconds()) + 1
for username in sorted(by_user):
@@ -242,6 +262,62 @@ def aggregate_rows(events, start, end, host):
return rows
def aggregate_hourly_rows(events, start, end, host):
by_user = _collect_user_rows(events, start, end, host)
rows = []
for username in sorted(by_user):
row = by_user[username]
merged = _merge_intervals(row["intervals"])
per_bucket = {}
for interval_start, interval_end in merged:
cursor = interval_start
while cursor < interval_end:
bucket_local = cursor.astimezone(REPORT_TZ).replace(minute=0, second=0, microsecond=0)
bucket_start = bucket_local.astimezone(timezone.utc)
bucket_end = (bucket_local + timedelta(hours=1)).astimezone(timezone.utc)
overlap_start = max(interval_start, bucket_start)
overlap_end = min(interval_end, bucket_end)
if overlap_end > overlap_start:
key = bucket_start
per_bucket[key] = per_bucket.get(key, 0) + int((overlap_end - overlap_start).total_seconds())
cursor = bucket_end
for bucket_start in sorted(per_bucket):
active_seconds = per_bucket[bucket_start]
if active_seconds <= 0:
continue
bucket_local = bucket_start.astimezone(REPORT_TZ)
rows.append(
{
"user": row["user"],
"user_id": row["user_id"],
"bucket_start_utc": to_iso_utc(bucket_start),
"bucket_start_local": bucket_local.isoformat(),
"report_date": bucket_local.date().isoformat(),
"hour_local": bucket_local.strftime("%H:00"),
"active_seconds": active_seconds,
"active_hhmm": hhmm(active_seconds),
}
)
return rows
def fetch_events_for_date(host, report_date):
bounds = get_report_bounds(report_date)
bucket_id = get_sessions_bucket_id(host)
try:
get(f"{AW}/buckets/{bucket_id}")
except Exception:
log_warning(f"bucket lookup failed for host={host} bucket={bucket_id} aw_base={AW}")
return bounds, []
try:
events = get(f"{AW}/buckets/{bucket_id}/events?limit=50000")
except Exception:
log_warning(f"events fetch failed for host={host} bucket={bucket_id} aw_base={AW}")
return bounds, []
return bounds, events
def build_report_summary(rows):
if not rows:
return {
@@ -270,22 +346,8 @@ def build_report_summary(rows):
def report_for_date(host, report_date):
start_local = datetime(report_date.year, report_date.month, report_date.day, tzinfo=REPORT_TZ)
end_local = start_local + timedelta(days=1) - timedelta(seconds=1)
start = start_local.astimezone(timezone.utc)
end = end_local.astimezone(timezone.utc)
bucket_id = get_sessions_bucket_id(host)
try:
get(f"{AW}/buckets/{bucket_id}")
except Exception:
log_warning(f"bucket lookup failed for host={host} bucket={bucket_id} aw_base={AW}")
return []
try:
events = get(f"{AW}/buckets/{bucket_id}/events?limit=50000")
except Exception:
log_warning(f"events fetch failed for host={host} bucket={bucket_id} aw_base={AW}")
return []
return aggregate_rows(events, start, end, host)
bounds, events = fetch_events_for_date(host, report_date)
return aggregate_rows(events, bounds["start"], bounds["end"], host)
def report_today(host):
+158
View File
@@ -0,0 +1,158 @@
#!/usr/bin/env python3
import importlib.util
import os
import sys
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
MODULE_PATH = Path(__file__).with_name("aw-worktime-api.py")
SPEC = importlib.util.spec_from_file_location("aw_worktime_api", MODULE_PATH)
WORKTIME = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(WORKTIME)
INFLUX_URL = os.environ.get("AW_WORKTIME_INFLUX_URL", "").strip().rstrip("/")
INFLUX_ORG = os.environ.get("AW_WORKTIME_INFLUX_ORG", "proxmox").strip() or "proxmox"
INFLUX_BUCKET = os.environ.get("AW_WORKTIME_INFLUX_BUCKET", "aw_metrics").strip() or "aw_metrics"
INFLUX_TOKEN = os.environ.get("AW_WORKTIME_INFLUX_TOKEN", "").strip()
INFLUX_ENABLED = os.environ.get("AW_WORKTIME_INFLUX_ENABLED", "").strip().lower() in {"1", "true", "yes", "on"}
HOSTS = [item.strip() for item in os.environ.get("AW_WORKTIME_INFLUX_HOSTS", WORKTIME.DEFAULT_HOST).split(",") if item.strip()]
DAYS = [item.strip() for item in os.environ.get("AW_WORKTIME_INFLUX_DAYS", "today,yesterday").split(",") if item.strip()]
def _escape_tag(value):
return (
str(value or "")
.replace("\\", "\\\\")
.replace(" ", "\\ ")
.replace(",", "\\,")
.replace("=", "\\=")
)
def _line(measurement, tags, fields, timestamp_ns):
tag_part = ",".join(f"{key}={_escape_tag(value)}" for key, value in sorted(tags.items()) if value is not None and value != "")
field_parts = []
for key, value in fields.items():
if isinstance(value, bool):
field_parts.append(f"{key}={'true' if value else 'false'}")
elif isinstance(value, int):
field_parts.append(f"{key}={value}i")
elif isinstance(value, float):
field_parts.append(f"{key}={value}")
else:
text = str(value or "").replace("\\", "\\\\").replace('"', '\\"')
field_parts.append(f'{key}="{text}"')
if not field_parts:
return ""
if tag_part:
return f"{measurement},{tag_part} {','.join(field_parts)} {timestamp_ns}"
return f"{measurement} {','.join(field_parts)} {timestamp_ns}"
def _timestamp_ns(dt):
return int(dt.astimezone(timezone.utc).timestamp() * 1_000_000_000)
def build_lines_for_day(host, report_date):
bounds, events = WORKTIME.fetch_events_for_date(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)
lines = []
daily_ts = _timestamp_ns(bounds["start"])
for row in rows:
lines.append(
_line(
"aw_rdp_worktime_daily",
{
"host": host,
"user": row["user"],
"user_id": row["user_id"],
"report_date": report_date.isoformat(),
},
{
"active_seconds": int(row["active_seconds"]),
"idle_seconds": int(row["idle_seconds"]),
"sessions_count": int(row["sessions_count"]),
"samples_count": int(row["samples_count"]),
"active_samples": int(row["active_samples"]),
},
daily_ts,
)
)
for row in hourly_rows:
lines.append(
_line(
"aw_rdp_worktime_hourly",
{
"host": host,
"user": row["user"],
"user_id": row["user_id"],
"report_date": row["report_date"],
"hour_local": row["hour_local"],
},
{
"active_seconds": int(row["active_seconds"]),
},
_timestamp_ns(WORKTIME.pts(row["bucket_start_utc"])),
)
)
lines.append(
_line(
"aw_rdp_worktime_summary_daily",
{
"host": host,
"report_date": report_date.isoformat(),
},
{
"users_count": int(summary["users_count"]),
"total_active_seconds": int(summary["total_active_seconds"]),
"top_user": summary["top_user"],
},
daily_ts,
)
)
return [line for line in lines if line]
def write_lines(lines):
if not lines:
return 0
if not INFLUX_URL or not INFLUX_TOKEN:
raise RuntimeError("InfluxDB destination is not configured")
payload = ("\n".join(lines) + "\n").encode("utf-8")
req = urllib.request.Request(
f"{INFLUX_URL}/api/v2/write?org={INFLUX_ORG}&bucket={INFLUX_BUCKET}&precision=ns",
data=payload,
method="POST",
headers={"Authorization": f"Token {INFLUX_TOKEN}", "Content-Type": "text/plain; charset=utf-8"},
)
with urllib.request.urlopen(req, timeout=30) as response:
if response.status not in {204, 200}:
raise RuntimeError(f"InfluxDB write failed with status={response.status}")
return len(lines)
def main():
if not INFLUX_ENABLED:
print("[aw-worktime-influx-exporter] disabled by AW_WORKTIME_INFLUX_ENABLED", file=sys.stderr)
return 0
lines = []
for host in HOSTS:
for day in DAYS:
report_date = WORKTIME.resolve_report_date(day=day)
lines.extend(build_lines_for_day(host, report_date))
written = write_lines(lines)
print(f"[aw-worktime-influx-exporter] wrote {written} points to {INFLUX_BUCKET}", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,15 @@
[Unit]
Description=AW Worktime InfluxDB exporter
After=network-online.target aw-worktime-api.service
Wants=network-online.target aw-worktime-api.service
[Service]
Type=oneshot
EnvironmentFile=/etc/activitywatch/aw-server.env
ExecStart=/usr/bin/python3 /usr/local/bin/aw-worktime-influx-exporter.py
User=activitywatch
Group=activitywatch
StandardOutput=journal
StandardError=journal
SyslogIdentifier=aw-worktime-influx-exporter
@@ -0,0 +1,11 @@
[Unit]
Description=Run AW Worktime InfluxDB exporter every 10 minutes
[Timer]
OnBootSec=3min
OnUnitActiveSec=10min
AccuracySec=1min
Unit=aw-worktime-influx-exporter.service
[Install]
WantedBy=timers.target
+16
View File
@@ -71,3 +71,19 @@ def test_build_aw_api_base_accepts_root_and_api_urls():
assert MODULE.build_aw_api_base("http://127.0.0.1:5600") == "http://127.0.0.1:5600/api/0"
assert MODULE.build_aw_api_base("http://127.0.0.1:5600/") == "http://127.0.0.1:5600/api/0"
assert MODULE.build_aw_api_base("http://127.0.0.1:5600/api/0") == "http://127.0.0.1:5600/api/0"
def test_aggregate_hourly_rows_splits_interval_by_local_hour():
start = datetime(2026, 5, 14, 6, 0, 0, tzinfo=timezone.utc)
end = datetime(2026, 5, 14, 8, 59, 59, tzinfo=timezone.utc)
rows = MODULE.aggregate_hourly_rows(
[
_event("2026-05-14T06:50:00Z", "user5", 4, True, sampleSeconds=1800),
_event("2026-05-14T07:20:00Z", "user5", 4, True, sampleSeconds=1800),
],
start,
end,
"SHARKON2025",
)
assert [row["hour_local"] for row in rows] == ["09:00", "10:00"]
assert [row["active_seconds"] for row in rows] == [600, 3000]
@@ -0,0 +1,35 @@
#!/usr/bin/env python3
import importlib.util
from datetime import date
from pathlib import Path
MODULE_PATH = Path(__file__).with_name("aw-worktime-influx-exporter.py")
SPEC = importlib.util.spec_from_file_location("aw_worktime_influx_exporter", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
def test_build_lines_for_day_emits_daily_hourly_and_summary(monkeypatch):
bounds = MODULE.WORKTIME.get_report_bounds(date(2026, 5, 14))
events = [
{
"timestamp": "2026-05-14T06:00:00Z",
"duration": 0.0,
"data": {
"username": "user5",
"userId": "WORKGROUP\\user5",
"sessionId": 4,
"state": "Активно",
"active": True,
"sampleSeconds": 1800,
},
}
]
monkeypatch.setattr(MODULE.WORKTIME, "fetch_events_for_date", lambda host, report_date: (bounds, events))
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)
@@ -0,0 +1,350 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": {
"type": "influxdb",
"uid": "influxdb_aw"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"query": "from(bucket: \"aw_metrics\")\n |> range(start: -3d)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_summary_daily\" and r._field == \"users_count\" and r.host == \"${host}\")\n |> group()\n |> last()",
"refId": "A"
}
],
"title": "Пользователи в последнем отчёте",
"type": "stat"
},
{
"datasource": {
"type": "influxdb",
"uid": "influxdb_aw"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
}
]
},
"unit": "s"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 0
},
"id": 2,
"options": {
"colorMode": "value",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"targets": [
{
"query": "from(bucket: \"aw_metrics\")\n |> range(start: -3d)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_summary_daily\" and r._field == \"total_active_seconds\" and r.host == \"${host}\")\n |> group()\n |> last()",
"refId": "A"
}
],
"title": "Активное время в последнем отчёте",
"type": "stat"
},
{
"datasource": {
"type": "influxdb",
"uid": "influxdb_aw"
},
"fieldConfig": {
"defaults": {
"decimals": 2,
"unit": "h"
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 12,
"x": 12,
"y": 0
},
"id": 3,
"options": {
"colorMode": "value",
"displayMode": "basic",
"namePlacement": "left",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showUnfilled": true,
"sizing": "auto"
},
"targets": [
{
"query": "from(bucket: \"aw_metrics\")\n |> range(start: -3d)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_daily\" and r._field == \"active_seconds\" and r.host == \"${host}\")\n |> group(columns: [\"host\", \"user\"])\n |> last()\n |> map(fn: (r) => ({ r with _value: float(v: r._value) / 3600.0 }))",
"refId": "A"
}
],
"title": "Пользователи: последнее активное время, часы",
"type": "bargauge"
},
{
"datasource": {
"type": "influxdb",
"uid": "influxdb_aw"
},
"fieldConfig": {
"defaults": {
"decimals": 2,
"unit": "h"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 4
},
"id": 4,
"options": {
"colorMode": "value",
"displayMode": "basic",
"namePlacement": "left",
"orientation": "horizontal",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"showUnfilled": true,
"sizing": "auto"
},
"targets": [
{
"query": "from(bucket: \"aw_metrics\")\n |> range(start: -7d)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_daily\" and r._field == \"active_seconds\" and r.host == \"${host}\")\n |> group(columns: [\"host\", \"user\"])\n |> sort(columns: [\"_time\"])\n |> tail(n: 2)\n |> first()\n |> map(fn: (r) => ({ r with _value: float(v: r._value) / 3600.0 }))",
"refId": "A"
}
],
"title": "Пользователи: предыдущий отчёт, часы",
"type": "bargauge"
},
{
"datasource": {
"type": "influxdb",
"uid": "influxdb_aw"
},
"fieldConfig": {
"defaults": {
"custom": {
"drawStyle": "line",
"lineInterpolation": "smooth",
"lineWidth": 2,
"showPoints": "auto"
},
"unit": "h"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 4
},
"id": 5,
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "desc"
}
},
"targets": [
{
"query": "from(bucket: \"aw_metrics\")\n |> range(start: -48h)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_hourly\" and r._field == \"active_seconds\" and r.host == \"${host}\")\n |> map(fn: (r) => ({ r with _value: float(v: r._value) / 3600.0 }))\n |> group(columns: [\"user\"])",
"refId": "A"
}
],
"title": "Почасовая активность пользователей, последние 48 часов",
"type": "timeseries"
},
{
"datasource": {
"type": "influxdb",
"uid": "influxdb_aw"
},
"fieldConfig": {
"defaults": {
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 12
},
"id": 6,
"options": {
"cellHeight": "sm",
"footer": {
"show": false
},
"showHeader": true
},
"targets": [
{
"query": "from(bucket: \"aw_metrics\")\n |> range(start: -7d)\n |> filter(fn: (r) => r._measurement == \"aw_rdp_worktime_daily\" and r._field == \"active_seconds\" and r.host == \"${host}\")\n |> map(fn: (r) => ({ r with _value: float(v: r._value) / 3600.0 }))\n |> keep(columns: [\"_time\", \"report_date\", \"user\", \"user_id\", \"_value\"])\n |> sort(columns: [\"_time\", \"user\"], desc: true)",
"refId": "A"
}
],
"title": "Ежедневная сводка по пользователям, последние 7 дней",
"type": "table"
}
],
"refresh": "10m",
"schemaVersion": 41,
"tags": [
"detmir",
"activitywatch",
"rdp",
"worktime"
],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "SHARKON2025",
"value": "SHARKON2025"
},
"datasource": {
"type": "influxdb",
"uid": "influxdb_aw"
},
"definition": "import \"influxdata/influxdb/schema\"\nschema.tagValues(bucket: \"aw_metrics\", tag: \"host\", predicate: (r) => r._measurement == \"aw_rdp_worktime_daily\", start: -30d)",
"hide": 0,
"includeAll": false,
"label": "Хост",
"multi": false,
"name": "host",
"options": [],
"query": "import \"influxdata/influxdb/schema\"\nschema.tagValues(bucket: \"aw_metrics\", tag: \"host\", predicate: (r) => r._measurement == \"aw_rdp_worktime_daily\", start: -30d)",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
}
]
},
"time": {
"from": "now-48h",
"to": "now"
},
"timepicker": {},
"timezone": "browser",
"title": "DetMir RDP User Activity",
"uid": "detmir-rdp-user-activity",
"version": 1,
"weekStart": ""
}