---
category: spec
tags: [monitoring, observability, plan]
last_updated: 2026-03-18
confidence: high
---

# Monitoring Dashboard Plan

## Current State

Infrastructure already in place:
- **healthcheck role**: cron every minute, checks all 4 services (systemctl + HTTP), emails on failure/recovery via msmtp. Binary up/down only.
- **diskmon role**: cron checks `/` and `/srv` at 80%/90% thresholds, emails alerts.
- **smoke-test.sh**: post-deploy liveness + content checks for all 4 services + each wiki slug.
- **No health endpoints** in any app code (api_server.py, auth_server.py, mcp_entry.py have no `/health` or `/status` routes).
- **No metrics collection** (no Prometheus, no structured logs beyond journald).

## Recommendation: Option C — `/app/admin/stats` page in the management UI

Rationale: The management UI (port 8002, `/app/*`) already exists with auth, templates, and DB access. A stats page requires zero new infrastructure and is immediately accessible in-browser. Option A (enhanced health endpoint) gives machine-readable data but no dashboard. Option B (Prometheus) is new infra.

## What to Build

One new route in `app/api_server.py`: `GET /app/admin/stats`

**Access control**: Require a valid platform JWT cookie AND `is_admin` flag on the user record (or, simpler, hardcode a set of admin DIDs from env var `ROBOT_ADMIN_DIDS`).

**Page content** (all point-in-time, no history):

| Section | Source |
|---------|--------|
| Service status | `systemctl is-active` for each of the 4 units |
| HTTP liveness | `curl -s -o /dev/null -w "%{http_code}" http://localhost:{port}/` for each |
| Disk usage | `df -h /srv` |
| Wiki count | `SELECT COUNT(*) FROM wikis` |
| User count | `SELECT COUNT(*) FROM users` |
| Recent wikis | `SELECT slug, created_at FROM wikis ORDER BY created_at DESC LIMIT 10` |
| Journal tail | `journalctl -u robot-otterwiki -u robot-api -u robot-auth -u robot-mcp -n 50 --no-pager` |

**Implementation notes**:
- Call `subprocess.run` with `capture_output=True` for systemctl/df/journalctl — same pattern already used in `_init_wiki_repo`.
- Use existing Jinja2 template system (Bootstrap already pulled in via otterwiki static assets).
- No JS required. Plain HTML table.
- Rate-limit the route (already have `flask_limiter` wired up).

## What NOT to Build

- No time-series data, no graphs, no retention.
- No `/health` JSON endpoint (the healthcheck cron already handles alerting; a JSON endpoint adds no dashboard value).
- No Prometheus (new infra, not justified yet).

## Ansible Changes

Add `healthcheck` and `diskmon` roles to `deploy.yml` (they are currently absent — only `database` and `deploy` roles run on deploy). This ensures the cron jobs are always present and current after a deploy.

```yaml
# deploy.yml — add to roles list:
    - healthcheck
    - diskmon
```

The stats page itself requires no Ansible changes (it's app code deployed via the existing `deploy` role).

## Files to Touch

- `app/api_server.py` — add `/app/admin/stats` route
- `app/management/templates/admin_stats.html` — new template
- `ansible/deploy.yml` — add `healthcheck` + `diskmon` to roles
- `tests/` — one test for the route (auth required, returns 200 with admin creds)

## Out of Scope

- Per-wiki metrics (page count, edit frequency) — future work
- Historical data / trending — future work
- Alerting changes — healthcheck cron already handles this
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9