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.
- getbased_dashboard-0.5.0/LICENSE +22 -0
- getbased_dashboard-0.5.0/PKG-INFO +125 -0
- getbased_dashboard-0.5.0/README.md +104 -0
- getbased_dashboard-0.5.0/pyproject.toml +36 -0
- getbased_dashboard-0.5.0/setup.cfg +4 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/__init__.py +9 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/api/__init__.py +6 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/api/activity.py +153 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/api/knowledge.py +367 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/api/mcp.py +325 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/cli.py +66 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/config.py +64 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/server.py +155 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/web/app.js +117 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/web/index.html +35 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/web/modals.js +172 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/web/styles.css +706 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/web/tabs/activity.js +151 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/web/tabs/knowledge.js +666 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard/web/tabs/mcp.js +179 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/PKG-INFO +125 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/SOURCES.txt +28 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/dependency_links.txt +1 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/entry_points.txt +2 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/requires.txt +12 -0
- getbased_dashboard-0.5.0/src/getbased_dashboard.egg-info/top_level.txt +1 -0
- getbased_dashboard-0.5.0/tests/test_activity_api.py +132 -0
- getbased_dashboard-0.5.0/tests/test_knowledge_api.py +460 -0
- getbased_dashboard-0.5.0/tests/test_mcp_api.py +329 -0
- 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,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,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)
|