getbased-dashboard 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. getbased_dashboard-0.5.0/LICENSE +22 -0
  2. getbased_dashboard-0.5.0/PKG-INFO +125 -0
  3. getbased_dashboard-0.5.0/README.md +104 -0
  4. getbased_dashboard-0.5.0/pyproject.toml +36 -0
  5. getbased_dashboard-0.5.0/setup.cfg +4 -0
  6. getbased_dashboard-0.5.0/src/getbased_dashboard/__init__.py +9 -0
  7. getbased_dashboard-0.5.0/src/getbased_dashboard/api/__init__.py +6 -0
  8. getbased_dashboard-0.5.0/src/getbased_dashboard/api/activity.py +153 -0
  9. getbased_dashboard-0.5.0/src/getbased_dashboard/api/knowledge.py +367 -0
  10. getbased_dashboard-0.5.0/src/getbased_dashboard/api/mcp.py +325 -0
  11. getbased_dashboard-0.5.0/src/getbased_dashboard/cli.py +66 -0
  12. getbased_dashboard-0.5.0/src/getbased_dashboard/config.py +64 -0
  13. getbased_dashboard-0.5.0/src/getbased_dashboard/server.py +155 -0
  14. getbased_dashboard-0.5.0/src/getbased_dashboard/web/app.js +117 -0
  15. getbased_dashboard-0.5.0/src/getbased_dashboard/web/index.html +35 -0
  16. getbased_dashboard-0.5.0/src/getbased_dashboard/web/modals.js +172 -0
  17. getbased_dashboard-0.5.0/src/getbased_dashboard/web/styles.css +706 -0
  18. getbased_dashboard-0.5.0/src/getbased_dashboard/web/tabs/activity.js +151 -0
  19. getbased_dashboard-0.5.0/src/getbased_dashboard/web/tabs/knowledge.js +666 -0
  20. getbased_dashboard-0.5.0/src/getbased_dashboard/web/tabs/mcp.js +179 -0
  21. getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/PKG-INFO +125 -0
  22. getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/SOURCES.txt +28 -0
  23. getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/dependency_links.txt +1 -0
  24. getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/entry_points.txt +2 -0
  25. getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/requires.txt +12 -0
  26. getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/top_level.txt +1 -0
  27. getbased_dashboard-0.5.0/tests/test_activity_api.py +132 -0
  28. getbased_dashboard-0.5.0/tests/test_knowledge_api.py +460 -0
  29. getbased_dashboard-0.5.0/tests/test_mcp_api.py +329 -0
  30. getbased_dashboard-0.5.0/tests/test_server.py +136 -0
@@ -0,0 +1,22 @@
1
+ GNU GENERAL PUBLIC LICENSE
2
+ Version 3, 29 June 2007
3
+
4
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
5
+ Everyone is permitted to copy and distribute verbatim copies
6
+ of this license document, but changing it is not allowed.
7
+
8
+ Preamble
9
+
10
+ The GNU General Public License is a free, copyleft license for
11
+ software and other kinds of works.
12
+
13
+ The licenses for most software and other practical works are designed
14
+ to take away your freedom to share and change the works. By contrast,
15
+ the GNU General Public License is intended to guarantee your freedom to
16
+ share and change all versions of a program--to make sure it remains free
17
+ software for all its users. We, the Free Software Foundation, use the
18
+ GNU General Public License for most of our software; it applies also to
19
+ any other work released this way by its authors. You can apply it to
20
+ your programs, too.
21
+
22
+ For the full license text, see <https://www.gnu.org/licenses/gpl-3.0.txt>
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.4
2
+ Name: getbased-dashboard
3
+ Version: 0.5.0
4
+ Summary: Web dashboard for getbased-agents — manage knowledge libraries, generate MCP client configs, inspect agent activity
5
+ License-Expression: GPL-3.0-only
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: fastapi>=0.110
10
+ Requires-Dist: uvicorn[standard]>=0.29
11
+ Requires-Dist: httpx>=0.27
12
+ Requires-Dist: typer>=0.12
13
+ Requires-Dist: python-multipart>=0.0.6
14
+ Requires-Dist: getbased-mcp>=0.2.2
15
+ Provides-Extra: test
16
+ Requires-Dist: pytest>=8.0; extra == "test"
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == "test"
18
+ Requires-Dist: respx>=0.21; extra == "test"
19
+ Requires-Dist: httpx>=0.27; extra == "test"
20
+ Dynamic: license-file
21
+
22
+ # getbased-dashboard
23
+
24
+ Web dashboard for [getbased-agents](https://github.com/elkimek/getbased-agents) — one page that covers knowledge library management, MCP client setup, and agent-activity inspection. Matches the getbased PWA's browser-local lens UX for users running the external-server backend.
25
+
26
+ ---
27
+
28
+ ## What it looks like
29
+
30
+ Three tabs, single auth gate, one pill for ingest progress that lives outside the tab DOM so it survives navigation.
31
+
32
+ ### Knowledge tab
33
+ - Engine badge strip at the top: `ONNX · CPU · MiniLM-L6-v2 · 384d · floor 0.55 · ready`
34
+ - Library list with per-row model chip, live chunk count (`12,758 chunks`), relative last-ingested (`indexed 2h ago`), activate/rename/delete
35
+ - Create-library form with a model dropdown (MiniLM-L6-v2 · BGE-small/base/large-en · BGE-M3) — dimension + download size shown per option
36
+ - Drag-drop ingest with a bottom-right pill: HTML5 `<progress>`, `12,500 / 16,000 · 3.2/s` chunks/sec rate, Cancel (partial commit) + Dismiss (×). 3s auto-dismiss on completion.
37
+ - Search preview with score per result
38
+ - Sources panel sorted by chunk count desc, Delete-all + per-source delete
39
+
40
+ ### MCP tab
41
+ - Env viewer showing what a spawned MCP would see (`LENS_URL`, `LENS_API_KEY_FILE` + present/missing, `GETBASED_TOKEN` set/not set, module path). Tooltips explain the difference between "dashboard's env" and "client's MCP env block"
42
+ - Config generator — emits paste-ready blocks for **Claude Desktop**, **Claude Code**, **Cursor**, **Cline**, **Hermes**. JSON for the first four, YAML with `enabled_tools` allowlist for Hermes. Copy-to-clipboard button.
43
+ - "Test MCP" — spawns the real `getbased-mcp` binary via stdio, runs `initialize` + `tools/list`, returns elapsed ms + tool names
44
+
45
+ ### Activity tab
46
+ - Top-line stat cards (total calls, errors, error rate, tools in use)
47
+ - Per-tool table with P50/P95 latency
48
+ - Newest-first feed of recent calls, polls every 10s
49
+ - Clear log button
50
+
51
+ ---
52
+
53
+ ## Install and run
54
+
55
+ ```bash
56
+ pipx install getbased-dashboard
57
+ getbased-dashboard serve # http://127.0.0.1:8323
58
+ ```
59
+
60
+ The dashboard expects a [getbased-rag](https://github.com/elkimek/getbased-agents/tree/main/packages/rag) server at `http://127.0.0.1:8322` and reuses rag's API key. On first visit the UI prompts for the bearer key; it's stored in `localStorage` on your machine.
61
+
62
+ Or as part of the full stack:
63
+
64
+ ```bash
65
+ pipx install "getbased-agent-stack[full]"
66
+ lens serve # in one terminal — the RAG backend
67
+ getbased-dashboard serve # in another — the UI
68
+ ```
69
+
70
+ ---
71
+
72
+ ## Architecture
73
+
74
+ ```
75
+ Browser Dashboard Rag server MCP subprocess
76
+ localhost ↔ localhost ↔ localhost on-demand stdio
77
+ /api/* proxy /query, /ingest, tools/list
78
+ + MCP test spawn /libraries, /info, (for Test button)
79
+ + activity tail /models, /stats
80
+ ```
81
+
82
+ The dashboard holds no data. Delete it and your knowledge base is untouched.
83
+
84
+ - All `/api/*` routes bearer-auth'd with the same key rag + MCP use (`secrets.compare_digest`, constant-time)
85
+ - Error envelope normalised to `{"error": "<string>"}` for both HTTPException and Pydantic validation errors — frontend has one shape to parse
86
+ - Upload path streams chunk-by-chunk to a temp file with a byte cap enforced before buffering (no OOM-via-multi-GB-upload)
87
+ - Client disconnect propagates: browser aborts fetch → dashboard drops upstream → rag sees disconnect → ingest stops at next batch boundary
88
+
89
+ ---
90
+
91
+ ## Config
92
+
93
+ | Variable | Default | Description |
94
+ |---|---|---|
95
+ | `DASHBOARD_HOST` | `127.0.0.1` | Bind host. Loopback-only by default — expose to LAN at your own risk |
96
+ | `DASHBOARD_PORT` | `8323` | Bind port |
97
+ | `LENS_URL` | `http://127.0.0.1:8322` | Where the rag server lives |
98
+ | `LENS_API_KEY_FILE` | `$XDG_DATA_HOME/getbased/lens/api_key` (with legacy fallback to `~/.hermes/rag/lens_api_key`) | Shared bearer token — same one MCP reads |
99
+ | `DASHBOARD_ACTIVITY_LOG` | `$XDG_STATE_HOME/getbased/mcp/activity.jsonl` | JSONL path the MCP writes to; dashboard tails it |
100
+ | `DASHBOARD_MAX_INGEST_BYTES` | `268435456` (256 MB) | Cap on a single upload's total size |
101
+ | `GETBASED_TOKEN` | (from env) | Optional. When set, the MCP tab's env viewer reads "set" and the config generator bakes it into the env blocks. Typically you leave it unset locally and set it in your AI client's MCP config |
102
+
103
+ ---
104
+
105
+ ## CLI
106
+
107
+ ```
108
+ getbased-dashboard serve Start the web server
109
+ getbased-dashboard info Show resolved config + whether the rag key is on disk
110
+ ```
111
+
112
+ ---
113
+
114
+ ## Security notes
115
+
116
+ - Dashboard binds loopback by default. Exposing via `DASHBOARD_HOST=0.0.0.0` means anyone on the LAN can drive your rag with the bearer key
117
+ - The bearer key is read fresh from disk on every authed request — rotating the key (rewrite the file) takes effect without a dashboard restart
118
+ - Multipart upload filenames are basename-sanitised before forwarding to rag (defence in depth; rag also sanitises)
119
+ - Subprocess spawn for the MCP test button reaps the child on timeout, exception, or cancellation — no orphaned processes
120
+
121
+ ---
122
+
123
+ ## License
124
+
125
+ GPL-3.0-only, matching the rest of the monorepo.
@@ -0,0 +1,104 @@
1
+ # getbased-dashboard
2
+
3
+ Web dashboard for [getbased-agents](https://github.com/elkimek/getbased-agents) — one page that covers knowledge library management, MCP client setup, and agent-activity inspection. Matches the getbased PWA's browser-local lens UX for users running the external-server backend.
4
+
5
+ ---
6
+
7
+ ## What it looks like
8
+
9
+ Three tabs, single auth gate, one pill for ingest progress that lives outside the tab DOM so it survives navigation.
10
+
11
+ ### Knowledge tab
12
+ - Engine badge strip at the top: `ONNX · CPU · MiniLM-L6-v2 · 384d · floor 0.55 · ready`
13
+ - Library list with per-row model chip, live chunk count (`12,758 chunks`), relative last-ingested (`indexed 2h ago`), activate/rename/delete
14
+ - Create-library form with a model dropdown (MiniLM-L6-v2 · BGE-small/base/large-en · BGE-M3) — dimension + download size shown per option
15
+ - Drag-drop ingest with a bottom-right pill: HTML5 `<progress>`, `12,500 / 16,000 · 3.2/s` chunks/sec rate, Cancel (partial commit) + Dismiss (×). 3s auto-dismiss on completion.
16
+ - Search preview with score per result
17
+ - Sources panel sorted by chunk count desc, Delete-all + per-source delete
18
+
19
+ ### MCP tab
20
+ - Env viewer showing what a spawned MCP would see (`LENS_URL`, `LENS_API_KEY_FILE` + present/missing, `GETBASED_TOKEN` set/not set, module path). Tooltips explain the difference between "dashboard's env" and "client's MCP env block"
21
+ - Config generator — emits paste-ready blocks for **Claude Desktop**, **Claude Code**, **Cursor**, **Cline**, **Hermes**. JSON for the first four, YAML with `enabled_tools` allowlist for Hermes. Copy-to-clipboard button.
22
+ - "Test MCP" — spawns the real `getbased-mcp` binary via stdio, runs `initialize` + `tools/list`, returns elapsed ms + tool names
23
+
24
+ ### Activity tab
25
+ - Top-line stat cards (total calls, errors, error rate, tools in use)
26
+ - Per-tool table with P50/P95 latency
27
+ - Newest-first feed of recent calls, polls every 10s
28
+ - Clear log button
29
+
30
+ ---
31
+
32
+ ## Install and run
33
+
34
+ ```bash
35
+ pipx install getbased-dashboard
36
+ getbased-dashboard serve # http://127.0.0.1:8323
37
+ ```
38
+
39
+ The dashboard expects a [getbased-rag](https://github.com/elkimek/getbased-agents/tree/main/packages/rag) server at `http://127.0.0.1:8322` and reuses rag's API key. On first visit the UI prompts for the bearer key; it's stored in `localStorage` on your machine.
40
+
41
+ Or as part of the full stack:
42
+
43
+ ```bash
44
+ pipx install "getbased-agent-stack[full]"
45
+ lens serve # in one terminal — the RAG backend
46
+ getbased-dashboard serve # in another — the UI
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Architecture
52
+
53
+ ```
54
+ Browser Dashboard Rag server MCP subprocess
55
+ localhost ↔ localhost ↔ localhost on-demand stdio
56
+ /api/* proxy /query, /ingest, tools/list
57
+ + MCP test spawn /libraries, /info, (for Test button)
58
+ + activity tail /models, /stats
59
+ ```
60
+
61
+ The dashboard holds no data. Delete it and your knowledge base is untouched.
62
+
63
+ - All `/api/*` routes bearer-auth'd with the same key rag + MCP use (`secrets.compare_digest`, constant-time)
64
+ - Error envelope normalised to `{"error": "<string>"}` for both HTTPException and Pydantic validation errors — frontend has one shape to parse
65
+ - Upload path streams chunk-by-chunk to a temp file with a byte cap enforced before buffering (no OOM-via-multi-GB-upload)
66
+ - Client disconnect propagates: browser aborts fetch → dashboard drops upstream → rag sees disconnect → ingest stops at next batch boundary
67
+
68
+ ---
69
+
70
+ ## Config
71
+
72
+ | Variable | Default | Description |
73
+ |---|---|---|
74
+ | `DASHBOARD_HOST` | `127.0.0.1` | Bind host. Loopback-only by default — expose to LAN at your own risk |
75
+ | `DASHBOARD_PORT` | `8323` | Bind port |
76
+ | `LENS_URL` | `http://127.0.0.1:8322` | Where the rag server lives |
77
+ | `LENS_API_KEY_FILE` | `$XDG_DATA_HOME/getbased/lens/api_key` (with legacy fallback to `~/.hermes/rag/lens_api_key`) | Shared bearer token — same one MCP reads |
78
+ | `DASHBOARD_ACTIVITY_LOG` | `$XDG_STATE_HOME/getbased/mcp/activity.jsonl` | JSONL path the MCP writes to; dashboard tails it |
79
+ | `DASHBOARD_MAX_INGEST_BYTES` | `268435456` (256 MB) | Cap on a single upload's total size |
80
+ | `GETBASED_TOKEN` | (from env) | Optional. When set, the MCP tab's env viewer reads "set" and the config generator bakes it into the env blocks. Typically you leave it unset locally and set it in your AI client's MCP config |
81
+
82
+ ---
83
+
84
+ ## CLI
85
+
86
+ ```
87
+ getbased-dashboard serve Start the web server
88
+ getbased-dashboard info Show resolved config + whether the rag key is on disk
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Security notes
94
+
95
+ - Dashboard binds loopback by default. Exposing via `DASHBOARD_HOST=0.0.0.0` means anyone on the LAN can drive your rag with the bearer key
96
+ - The bearer key is read fresh from disk on every authed request — rotating the key (rewrite the file) takes effect without a dashboard restart
97
+ - Multipart upload filenames are basename-sanitised before forwarding to rag (defence in depth; rag also sanitises)
98
+ - Subprocess spawn for the MCP test button reaps the child on timeout, exception, or cancellation — no orphaned processes
99
+
100
+ ---
101
+
102
+ ## License
103
+
104
+ GPL-3.0-only, matching the rest of the monorepo.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "getbased-dashboard"
7
+ version = "0.5.0"
8
+ description = "Web dashboard for getbased-agents — manage knowledge libraries, generate MCP client configs, inspect agent activity"
9
+ readme = "README.md"
10
+ license = "GPL-3.0-only"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "fastapi>=0.110",
14
+ "uvicorn[standard]>=0.29",
15
+ "httpx>=0.27",
16
+ "typer>=0.12",
17
+ "python-multipart>=0.0.6",
18
+ "getbased-mcp>=0.2.2",
19
+ ]
20
+
21
+ [project.optional-dependencies]
22
+ test = ["pytest>=8.0", "pytest-asyncio>=0.23", "respx>=0.21", "httpx>=0.27"]
23
+
24
+ [project.scripts]
25
+ getbased-dashboard = "getbased_dashboard.cli:app"
26
+
27
+ [tool.setuptools.packages.find]
28
+ where = ["src"]
29
+
30
+ [tool.setuptools.package-data]
31
+ getbased_dashboard = ["web/**/*"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ addopts = "-ra -q"
36
+ asyncio_mode = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """getbased-dashboard — web UI for getbased-agents.
2
+
3
+ Orchestration layer that sits between the browser and the rag + mcp
4
+ packages. Holds no data of its own: proxies knowledge-base operations
5
+ to rag, spawns the mcp stdio process on demand for tool discovery and
6
+ config generation, reads the mcp's activity log for the dashboard feed.
7
+ """
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1,6 @@
1
+ """Dashboard HTTP API.
2
+
3
+ Each submodule registers its routes onto a FastAPI app via a `register()`
4
+ function. Keeps the server.py entry point readable and lets each concern
5
+ (knowledge, mcp, activity) own its own dependencies.
6
+ """
@@ -0,0 +1,153 @@
1
+ """Activity API — tail the MCP's JSONL activity log and surface the
2
+ recent records + simple aggregations.
3
+
4
+ The log is written by getbased-mcp at `$XDG_STATE_HOME/getbased/mcp/
5
+ activity.jsonl` (configurable via LENS_MCP_ACTIVITY_LOG). One record
6
+ per tool call: tool name, timestamp, duration, ok flag, error class on
7
+ failure. Args are never logged upstream so we don't have to strip them.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ from collections import defaultdict
15
+ from pathlib import Path
16
+
17
+ from fastapi import APIRouter, FastAPI, Request
18
+
19
+ from ..config import DashboardConfig
20
+ from ..server import _require_auth
21
+
22
+
23
+ def _cfg(request: Request) -> DashboardConfig:
24
+ return request.app.state.config
25
+
26
+
27
+ # Cap on how much of the log we read per request. Users with heavy agent
28
+ # usage can accumulate megabytes quickly — loading the entire file on
29
+ # every poll is wasteful. Tailing from the end keeps the endpoint O(cap)
30
+ # regardless of how long the log has been running.
31
+ _TAIL_BYTES = 512 * 1024
32
+
33
+
34
+ def _read_records(path: Path, limit: int) -> list[dict]:
35
+ """Return up to `limit` most-recent records. If the file is under
36
+ _TAIL_BYTES read the whole thing; otherwise seek from the end. Malformed
37
+ lines (partial last write, corrupt records) are silently skipped so
38
+ one bad line can't hide the rest."""
39
+ if not path.exists():
40
+ return []
41
+ size = path.stat().st_size
42
+ try:
43
+ if size <= _TAIL_BYTES:
44
+ text = path.read_text(encoding="utf-8", errors="replace")
45
+ else:
46
+ with path.open("rb") as f:
47
+ f.seek(size - _TAIL_BYTES)
48
+ # Drop the first (likely partial) line so we don't parse
49
+ # garbage. There will always be a complete line after the
50
+ # first newline we find, assuming writers use line-atomic
51
+ # append — which Python's text-mode write does.
52
+ f.readline()
53
+ text = f.read().decode("utf-8", errors="replace")
54
+ except OSError:
55
+ return []
56
+
57
+ records: list[dict] = []
58
+ for line in text.splitlines():
59
+ line = line.strip()
60
+ if not line:
61
+ continue
62
+ try:
63
+ rec = json.loads(line)
64
+ if isinstance(rec, dict):
65
+ records.append(rec)
66
+ except json.JSONDecodeError:
67
+ continue
68
+
69
+ return records[-limit:]
70
+
71
+
72
+ def _aggregate(records: list[dict]) -> dict:
73
+ """Per-tool counts, success rate, and P50/P95 latency. O(N log N) —
74
+ fine up to the ~thousand records our tail window holds."""
75
+ by_tool: dict[str, list[dict]] = defaultdict(list)
76
+ for r in records:
77
+ t = r.get("tool")
78
+ if isinstance(t, str):
79
+ by_tool[t].append(r)
80
+
81
+ def _percentile(sorted_vals: list[int], p: float) -> int | None:
82
+ if not sorted_vals:
83
+ return None
84
+ idx = int(p * (len(sorted_vals) - 1))
85
+ return sorted_vals[idx]
86
+
87
+ tools: list[dict] = []
88
+ for name, group in sorted(by_tool.items()):
89
+ durations = sorted(
90
+ int(r.get("duration_ms", 0)) for r in group if isinstance(r.get("duration_ms"), (int, float))
91
+ )
92
+ errors = sum(1 for r in group if not r.get("ok", True))
93
+ tools.append(
94
+ {
95
+ "tool": name,
96
+ "calls": len(group),
97
+ "errors": errors,
98
+ "error_rate": (errors / len(group)) if group else 0.0,
99
+ "p50_ms": _percentile(durations, 0.5),
100
+ "p95_ms": _percentile(durations, 0.95),
101
+ }
102
+ )
103
+
104
+ total_errors = sum(1 for r in records if not r.get("ok", True))
105
+ return {
106
+ "total_calls": len(records),
107
+ "total_errors": total_errors,
108
+ "overall_error_rate": (total_errors / len(records)) if records else 0.0,
109
+ "tools": tools,
110
+ }
111
+
112
+
113
+ def register(app: FastAPI) -> None:
114
+ router = APIRouter(prefix="/api/activity", tags=["activity"])
115
+
116
+ @router.get("")
117
+ async def activity_feed(request: Request, limit: int = 200):
118
+ cfg = _cfg(request)
119
+ _require_auth(request, cfg)
120
+ # Bound limit so a client can't ask us to return 10 million records
121
+ # in one payload. 1000 is plenty for a dashboard tick.
122
+ limit = max(1, min(1000, int(limit)))
123
+ records = _read_records(cfg.activity_log, limit)
124
+ stats = _aggregate(records)
125
+ return {
126
+ "log_path": str(cfg.activity_log),
127
+ "log_exists": cfg.activity_log.exists(),
128
+ "records": records,
129
+ "stats": stats,
130
+ }
131
+
132
+ @router.delete("")
133
+ async def clear_activity(request: Request):
134
+ """Wipe the log. Useful for resetting the dashboard's view after
135
+ a period of testing. Returns the new (empty) state so the UI can
136
+ refresh in one round-trip."""
137
+ cfg = _cfg(request)
138
+ _require_auth(request, cfg)
139
+ try:
140
+ if cfg.activity_log.exists():
141
+ os.unlink(cfg.activity_log)
142
+ except OSError:
143
+ # File may have been created by another process or removed in
144
+ # a race; either way we want to return "nothing here" state.
145
+ pass
146
+ return {
147
+ "log_path": str(cfg.activity_log),
148
+ "log_exists": False,
149
+ "records": [],
150
+ "stats": _aggregate([]),
151
+ }
152
+
153
+ app.include_router(router)