feat: read-only web dashboard for monitoring sessions, messages, and cron - #2
feat: read-only web dashboard for monitoring sessions, messages, and cron#2heyalchang wants to merge 1 commit into
Conversation
prbot Review (Claude)Now I have a thorough understanding. Here's my review: Code Review: PR #2 —
|
| Aspect | Rating |
|---|---|
| Dashboard code quality | ⭐⭐⭐⭐ Good |
| Test coverage (dashboard) | ⭐⭐⭐⭐ Good (34 tests) |
| Security | ⭐⭐⭐ Needs auth + XSS audit |
| PR scope / reviewability | ⭐ Way too large |
| Upstream readiness | ⭐⭐ Needs splitting |
Verdict: The dashboard feature itself is well-built and well-tested. However, this PR needs to be split into manageable, reviewable chunks before it can be properly evaluated for upstream submission. The dashboard-specific files (dashboard/, tests/test_dashboard_data.py) could be extracted into a clean, focused PR of ~3,700 lines that would be straightforward to review and merge.
…and cron Adds `hermes dashboard` — an aiohttp-based web UI for observing gateway activity without modifying state. 10 tabs: Overview, Messages, Sessions, Usage, Cron Jobs, Logs, Search, Memory, Plugins, Checkpoints. Key design choices: - Read-only: all data access opens fresh SQLite connections in ro mode - Schema-adaptive: detects available columns via PRAGMA table_info, works across DB schema versions - Single-file SPA: vanilla HTML/JS, no build step, no framework deps - Separate process: `hermes dashboard [--port PORT]`, never embedded in the gateway - XSS-safe: esc() for HTML content, escAttr() for JS-in-attribute contexts - /api/health endpoint for monitoring integration Includes 34 tests covering data layer, schema detection, search, cron output path traversal blocking, and secret redaction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
fd38ee1 to
07d061c
Compare
prbot Review (Claude)Now I have a thorough understanding of this PR. Here's my review: Code Review: PR #2 —
|
| File | Lines | Purpose |
|---|---|---|
dashboard/data.py |
720 | Read-only data access layer (SQLite, filesystem) |
dashboard/server.py |
186 | aiohttp route handlers + app factory |
dashboard/static/index.html |
2,413 | Single-file SPA (HTML + CSS + JS) |
tests/test_dashboard_data.py |
408 | 34 tests for the data layer |
| Integration | ~25 | CLI subcommand + pyproject.toml extras |
Note: The PR shows 600 files / 135K additions because the upstream/dashboard branch has diverged significantly from main. Only the 5 files above are the actual dashboard contribution.
👍 Strengths
- Clean architecture — Strict separation between data access (
data.py), HTTP layer (server.py), and UI (index.html). No state leaks between layers. - Read-only by design — All SQLite connections use
?mode=ro, no write endpoints exist, and cron controls are intentionallydisabled. - Schema-adaptive —
PRAGMA table_infodetects available columns so it works across DB versions. This is thoughtful for upstream compatibility. - Good test coverage on the data layer — path traversal blocking, secret redaction, schema degradation, and cron parsing are all tested.
- Zero build step — No npm/webpack/framework dependencies. Just a single HTML file with vanilla JS.
- Optional dependency —
pip install hermes-agent[dashboard]via pyproject extras.
🔴 Security Issues
1. XSS via onclick inline handler (Medium Severity)
// In loadFeed():
onclick="jumpToSession('${esc(m.session_id)}')"The esc() function escapes <, >, &, " but does NOT escape single quotes ('). Since the JS string is delimited by single quotes inside the onclick attribute, a malicious session_id containing ');alert(1);// would break out of the string context.
Fix: Use escAttr() (which already exists and escapes quotes), or better yet, use data-session-id attributes with event delegation (which is already the pattern used for cron job rows).
2. Secret redaction leaks short secrets
if len(v) > 4:
v = v[:2] + "****" + v[-2:]
elif v:
v = "****"A 5-character secret like abcde becomes ab****de — revealing 4 of 5 characters (80%). Consider using a higher threshold (e.g., len(v) > 8) or always masking all but the last 2 characters.
3. No API error handling in api() fetch
async function api(path) {
const r = await fetch('/api/' + path);
return r.json();
}Non-200 responses (404, 500) will attempt r.json() which may throw on non-JSON error pages. This silently fails. Add a status check:
async function api(path) {
const r = await fetch('/api/' + path);
if (!r.ok) throw new Error(`API error: ${r.status}`);
return r.json();
}4. _safe_id() is incomplete sanitization
def _safe_id(raw: str) -> str:
return raw.replace("/", "").replace("..", "").replace("\\", "")This strips characters but doesn't validate the result is a legitimate job ID format. Consider allowlisting: re.sub(r'[^a-zA-Z0-9_-]', '', raw).
🟡 Code Quality & Maintainability
5. Global mutable state for schema cache
_sessions_columns: Optional[set] = NoneOnce cached, this is never invalidated. If the DB schema is altered (e.g., migration during hermes upgrade), the dashboard will serve stale column metadata until restart. Consider a TTL-based cache or re-check every N minutes.
6. get_checkpoints() has O(n×m) subprocess calls
For each checkpoint directory, it runs git log (1 subprocess), then git diff --shortstat per commit (up to 50 subprocesses). With 10 checkpoint dirs × 50 snapshots = 500 subprocess calls per request. This could easily take 30+ seconds.
Fix: Use git log --stat --format=... in a single call per checkpoint, or add pagination and lazy-load stats.
7. Hardcoded platform list in get_feed()
sources = [
"telegram", "whatsapp", "discord", "slack", "signal",
"homeassistant", "email", "webhook", "matrix", "mattermost",
"dingtalk", "sms",
]This will miss any new platforms added upstream. Consider SELECT DISTINCT source FROM sessions WHERE source != 'cli' AND source != 'cron' instead.
8. 2,400-line single HTML file
While intentional (no build step), this makes the frontend very difficult to review and maintain. At minimum, consider splitting the <script> block into a separate dashboard.js file served from the same static directory.
9. json.dumps(obj, default=str) silently converts errors
In json_response(), default=str silently converts any non-serializable object to its string representation. This can mask bugs where unexpected types slip through. Consider being explicit about which types get converted.
🟡 Test Coverage Gaps
| What's tested ✅ | What's missing ❌ |
|---|---|
| Data layer (34 tests) | Server route handlers (0 tests) |
| Schema detection | get_checkpoints() with actual git data |
| Path traversal blocking | get_insights() with real data |
| Secret redaction | Short secret edge cases |
| Cron parsing | Error response codes (404, malformed params) |
| FTS search | Frontend rendering (even basic smoke tests) |
Adding a few aiohttp test client tests for server.py would significantly increase confidence, especially for the input sanitization in route handlers.
🔵 Minor Nits
tail_log()importsdequeinside the function — move to module level- No CORS headers — fine for same-origin use, but worth a comment explaining this is intentional
- Missing
__all__indashboard/data.py— would clarify the public API get_cron_status()countsdisabled = total - enabled - pausedwhich could be negative if a job is bothenabled: FalseANDstate: 'paused'(should that be possible?)
Summary
This is a well-structured, thoughtfully designed dashboard with good separation of concerns and defensive read-only patterns. The main areas needing attention before merge:
| Priority | Issue | Effort |
|---|---|---|
| 🔴 High | XSS in onclick handler — use escAttr() or data attributes |
5 min |
| 🔴 Medium | Secret redaction threshold too low | 5 min |
| 🟡 Medium | get_checkpoints() performance (subprocess per commit) |
30 min |
| 🟡 Medium | Add server route tests | 1-2 hr |
| 🟡 Low | Hardcoded platform list in get_feed() |
10 min |
| 🟡 Low | API error handling in JS api() |
5 min |
The XSS fix is the only blocker — everything else can be addressed in follow-ups.
prbot Review (Codex)SummaryThis PR adds a new local aiohttp dashboard, wires it into FindingsP0 — Must fix before merge
P1 — Strong recommendation
P2 — Worth raising
Tool Notes
The repo’s default pytest |
Summary
Adds
hermes dashboard— an aiohttp web UI for observing gateway activity. 10 tabs: Overview, Messages, Sessions, Usage, Cron Jobs, Logs, Search, Memory, Plugins, Checkpoints.hermes dashboard [--port PORT], never embedded in gatewayIntended for upstream submission to NousResearch/hermes-agent after review here.
Test plan
python -m pytest tests/test_dashboard_data.py -v(34 tests)hermes dashboard --port 18808and verify all 10 tabs render🤖 Generated with Claude Code