4ed831d9c3
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
55 lines
2.5 KiB
SQL
55 lines
2.5 KiB
SQL
-- 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;
|