feat(server/diagnostics): device debug-reporting ingest + admin timeline + retention (M9)
New diagnostic_events table + per-account users.debug_mode_enabled flag.
When an account's flag is on, its client(s) POST a batch timeseries of
connectivity / UPnP-sync / power / lifecycle events to /api/diagnostics
(no-op 204 when off, kind whitelist mirrors the CHECK constraint).
Admin surface: GET /api/admin/diagnostics (optional account/device/kind/
time-window filters, RFC3339-or-epoch-ms, export-sized paging) + a
/diagnostics/devices overview + PUT /api/admin/users/{id}/debug-mode to
flip an account remotely while a bug is live. debug_mode_enabled is now
exposed on /api/me (client gate) and the admin user views.
Retention: a 30-day gc-worker sweep (GcPruneDiagnostics), keyed on the
server clock so a skewed device clock can't keep rows alive.
Refs Scribe M9 (#119), tasks #1172 #1173.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K55iTxn95BtshocgdE1shW
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE users DROP COLUMN IF EXISTS debug_mode_enabled;
|
||||
DROP TABLE IF EXISTS diagnostic_events;
|
||||
@@ -0,0 +1,54 @@
|
||||
-- Device diagnostics / debug-reporting subsystem (M9).
|
||||
--
|
||||
-- When an account has users.debug_mode_enabled = true, that account's
|
||||
-- client(s) stream a timeseries of device state here: connectivity +
|
||||
-- server-health transitions (roaming / poor-data recovery), UPnP/Sonos
|
||||
-- sync events (locked-phone desync), and power/lifecycle state (Doze /
|
||||
-- battery-optimization, the prime suspect for the desync). The admin
|
||||
-- /admin/diagnostics view filters by account + device + time window and
|
||||
-- exports the slice as JSON for offline analysis.
|
||||
--
|
||||
-- `kind` is a COARSE category gated by a CHECK whitelist; the finer
|
||||
-- event discriminator + all event-specific fields live in the `payload`
|
||||
-- jsonb. This keeps the enum small and stable so new event variants
|
||||
-- don't require a migration (per the enum-CHECK-whitelist rule, only a
|
||||
-- new *category* would). If you add a category here, mirror it in the
|
||||
-- handler whitelist in internal/api/diagnostics.go.
|
||||
--
|
||||
-- Two timestamps on purpose: occurred_at is the client device clock
|
||||
-- (when the event happened on the phone — may be skewed/wrong, e.g. in
|
||||
-- a dead zone), received_at is the server clock at ingest. Retention
|
||||
-- pruning is keyed on received_at so a bad client clock can't keep rows
|
||||
-- alive forever.
|
||||
|
||||
CREATE TABLE diagnostic_events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
client_id text NOT NULL,
|
||||
app_version text,
|
||||
os_version text,
|
||||
kind text NOT NULL,
|
||||
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
occurred_at timestamptz NOT NULL,
|
||||
received_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT diagnostic_events_kind_check
|
||||
CHECK (kind IN (
|
||||
'connectivity', 'upnp_sync', 'power', 'lifecycle',
|
||||
'heartbeat', 'http'
|
||||
))
|
||||
);
|
||||
|
||||
-- Admin reads filter by account then device, ordered newest-first.
|
||||
CREATE INDEX idx_diagnostic_events_user
|
||||
ON diagnostic_events (user_id, occurred_at DESC);
|
||||
CREATE INDEX idx_diagnostic_events_client
|
||||
ON diagnostic_events (client_id, occurred_at DESC);
|
||||
-- Retention pruner deletes by server-clock age.
|
||||
CREATE INDEX idx_diagnostic_events_received
|
||||
ON diagnostic_events (received_at);
|
||||
|
||||
-- Per-account opt-in for diagnostics reporting. Admin-set primarily
|
||||
-- (flip remotely while a bug is live); the client also exposes a local
|
||||
-- OFF switch. Default false: no account reports until explicitly enabled.
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS debug_mode_enabled boolean NOT NULL DEFAULT false;
|
||||
Reference in New Issue
Block a user