mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 02:59:55 +00:00
Part C of #2128: an admin area with an overview, the user table and a global audit log. Off by default like the rest of user management: with `userManagement` off nothing changes, and non-admins get no new UI. This is the first of 4 stacked PRs (C, D, E, then a small export/backup follow-up). Each later one contains this branch; review them in order. ## The situation - An admin could manage users one by one in `#/admin/users`, but nothing showed whether the instance needs attention: accounts stuck in activation, bouncing mail, someone guessing a password, an MQTT source that dropped. - The audit log existed (`internal/users/audit.go`) but could only be read per user, and logins were not recorded. ## What this PR adds **Store (`internal/users`)** - Schema v3: an index on `audit_log(at)`. - `AuditList` with filters (action or group prefix like `user.login.*`, user as actor or target, period) and keyset pagination; `PruneAudit`; `Stats` for the user figures. **Server** - `GET /api/admin/stats` (typed struct): accounts by status, admins, registrations and active users over 7 and 30 days, logins and failed logins in 24 hours, mail by final status, and the attention items computed server-side. - `GET /api/admin/audit`: filtered, newest first, `next` cursor. - `GET /api/admin/users` gains `bouncing=1`. - Logins are audited as `user.login` and `user.login.failed` (reason `wrong_password`, `pending` or `disabled`). An unknown address writes no row. The writes are asynchronous, so the login response does not wait on `users.db`. Login rows are pruned after 90 days. **Frontend** - `#/admin?tab=overview|users|audit`: `admin.js` (tab shell), `admin-overview.js` ("Needs attention", Users card, System card from the existing health, MQTT and observer endpoints), `admin-audit.js` (filters in the URL, "Load more"). `admin-users.js` becomes the Users tab; the old `#/admin/users` link rewrites to it. - `/api/healthz` is read on open and on Refresh only, not on the 60-second timer, because it walks every packet under a read lock. Spec: [`docs/specs/2026-10-07-admin-dashboard-design.md`](https://github.com/efiten/CoreScope/blob/feat/admin-dashboard/docs/specs/2026-10-07-admin-dashboard-design.md). ## Verification - `internal/users` and `cmd/server`: `go vet` and `go test` pass locally, 26 new Go tests. One upstream test, `TestSaveGeoFilterPreservesFileMode`, also fails on Windows on plain `master` (file modes) and is unrelated. - `sh test-all.sh` exits 0; the XSS gate in diff mode passes. - User-management E2E with the `e2etest` build: 13 of 13 steps locally. - Running on our staging and production instance since 7 October 2026. ## Not in this PR - Restricting existing pages (perf, MQTT status) to logged-in users or admins. The admin area adds no access mechanism of its own, so that stays a later router rule plus endpoint check. - Server-enforced customizer tab restrictions (#1508). - The 24-hour, 5-attempt and 10-minute attention thresholds are constants for now (AGENTS.md rule 8: customizer later). --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
20 lines
1.6 KiB
CSS
20 lines
1.6 KiB
CSS
/* Admin area (#/admin, optional user management): tab shell, overview
|
|
cards, figures and the registrations bar chart. Colors only via variables. */
|
|
.admin-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-bottom: 12px; }
|
|
.admin-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr)); gap: 16px; }
|
|
.admin-card { background: var(--card-bg); border: 1px solid var(--border); border-radius: 10px; padding: 16px; min-width: 0; }
|
|
.admin-card h3 { margin: 0 0 8px; font-size: 1rem; }
|
|
.admin-card h4 { margin: 16px 0 6px; font-size: var(--fs-sm); color: var(--text-muted); }
|
|
.admin-attention { margin-bottom: 16px; border-color: var(--status-yellow); }
|
|
.admin-attention ul, .admin-list { margin: 0; padding-left: 18px; }
|
|
.admin-attention a { color: var(--link-color); }
|
|
.admin-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 8px; margin: 0; }
|
|
.admin-stat dt { font-size: var(--fs-sm); color: var(--text-muted); }
|
|
.admin-stat dd { margin: 0; font-size: var(--fs-lg); font-weight: var(--fw-semibold); overflow-wrap: anywhere; }
|
|
.admin-bars { display: flex; align-items: flex-end; gap: 2px; height: 48px; margin-top: 12px; }
|
|
.admin-bar { flex: 1 1 0; min-height: 1px; background: var(--accent-strong); border-radius: 2px 2px 0 0; }
|
|
.admin-tabs { display: flex; flex-wrap: wrap; gap: 6px; margin: 0 0 16px; }
|
|
.admin-tabs .tab-btn { display: inline-flex; align-items: center; min-height: 48px; text-decoration: none; box-sizing: border-box; }
|
|
.um-filters input[type="checkbox"] { min-height: 0; }
|
|
@media (pointer: fine) { .admin-tabs .tab-btn { min-height: 36px; } }
|