Files
minstrel/internal/db/queries/diagnostics.sql
T
bvandeusen 4ed831d9c3
test-go / test (push) Successful in 37s
test-go / integration (push) Successful in 4m44s
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
2026-06-29 18:38:56 -04:00

67 lines
3.0 KiB
SQL

-- name: InsertDiagnosticEvent :one
-- Records one client-reported diagnostic event. The writer validates the
-- `kind` category against its whitelist before this runs; the CHECK
-- constraint is the belt-and-braces guard. payload carries the finer
-- event discriminator + all event-specific fields. Returns received_at
-- so the caller can confirm ingest.
INSERT INTO diagnostic_events (
user_id, client_id, app_version, os_version, kind, payload, occurred_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, received_at;
-- name: ListAdminDiagnostics :many
-- Admin timeline query. All filters are optional (NULL = no filter):
-- account (user_id), device (client_id), category (kind), and the
-- occurred_at window (from_ts/to_ts). Newest-first; the SPA reverses for
-- a chronological timeline / export. Joined with users for the username
-- so the view renders without a second round-trip.
SELECT
de.id AS id,
de.user_id AS user_id,
u.username AS username,
de.client_id AS client_id,
de.app_version AS app_version,
de.os_version AS os_version,
de.kind AS kind,
de.payload AS payload,
de.occurred_at AS occurred_at,
de.received_at AS received_at
FROM diagnostic_events de
JOIN users u ON u.id = de.user_id
WHERE (sqlc.narg(user_id)::uuid IS NULL OR de.user_id = sqlc.narg(user_id)::uuid)
AND (sqlc.narg(client_id)::text IS NULL OR de.client_id = sqlc.narg(client_id)::text)
AND (sqlc.narg(kind)::text IS NULL OR de.kind = sqlc.narg(kind)::text)
AND (sqlc.narg(from_ts)::timestamptz IS NULL
OR de.occurred_at >= sqlc.narg(from_ts)::timestamptz)
AND (sqlc.narg(to_ts)::timestamptz IS NULL
OR de.occurred_at <= sqlc.narg(to_ts)::timestamptz)
ORDER BY de.occurred_at DESC
LIMIT sqlc.arg(lim)::int OFFSET sqlc.arg(off)::int;
-- name: ListDiagnosticDevices :many
-- Distinct devices that have reported, for the admin device filter +
-- "who is currently reporting" overview. Optionally scoped to one account.
SELECT
de.client_id AS client_id,
de.user_id AS user_id,
u.username AS username,
max(de.app_version)::text AS app_version,
max(de.os_version)::text AS os_version,
max(de.occurred_at)::timestamptz AS last_seen,
count(*) AS event_count
FROM diagnostic_events de
JOIN users u ON u.id = de.user_id
WHERE (sqlc.narg(user_id)::uuid IS NULL OR de.user_id = sqlc.narg(user_id)::uuid)
GROUP BY de.client_id, de.user_id, u.username
ORDER BY last_seen DESC;
-- name: GcPruneDiagnostics :execrows
-- Retention sweep for the gc worker. Deletes diagnostic_events older
-- than 30 days by server clock (received_at) so a skewed client clock
-- can't keep rows alive. Matches the other gc lifecycle sweeps'
-- hardcoded-threshold style; diagnostics are debug telemetry, not user
-- data, and 30 days is ample to investigate a live incident after the
-- fact. Returns the affected row count for the sweep log line.
DELETE FROM diagnostic_events WHERE received_at < now() - INTERVAL '30 days';