Files
minstrel/internal/db/dbq/playback_errors.sql.go
T
bvandeusen 15b59a214d
test-go / test (push) Successful in 29s
test-go / integration (push) Successful in 11m25s
feat(server): playback_errors table + admin inbox endpoints
New client-reported playback-error log. Surfaces zero-duration
tracks (and future load_failed / stalled kinds) into an admin
inbox so the operator can hide / delete / re-request the
offending track.

Schema (migration 0032):
- playback_errors table with CHECK constraints on the kind +
  resolution enums (per the standing rule that new enum values
  need a migration to add)
- Partial index on unresolved rows for fast inbox lookup
- ON DELETE CASCADE from tracks + users so cleanup is automatic

Endpoints:
- POST /api/playback-errors: any signed-in user reports. Body
  validates track existence + kind whitelist; client_id required
  so support can correlate reports from the same device.
- GET /api/admin/playback-errors?resolved=false&offset=&limit=:
  admin list with join to track/album/artist for table render
  without per-row round-trips. Pagination capped at 200/page.
- POST /api/admin/playback-errors/{id}/resolve: admin marks
  resolved with a resolution enum string.

Auto-resolve on Hide/Delete/Re-request from the inbox row is
driven from the web client (two sequential calls) — keeps the
existing track-action endpoints unchanged.
2026-06-02 11:25:50 -04:00

211 lines
5.6 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: playback_errors.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const insertPlaybackError = `-- name: InsertPlaybackError :one
INSERT INTO playback_errors (track_id, user_id, client_id, kind, detail)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, track_id, user_id, client_id, kind, detail, occurred_at,
resolved_at, resolved_by, resolution
`
type InsertPlaybackErrorParams struct {
TrackID pgtype.UUID
UserID pgtype.UUID
ClientID string
Kind string
Detail *string
}
// Records a client-reported playback failure. The handler validates
// the kind enum before this runs; the CHECK constraint is a
// belt-and-braces guard.
func (q *Queries) InsertPlaybackError(ctx context.Context, arg InsertPlaybackErrorParams) (PlaybackError, error) {
row := q.db.QueryRow(ctx, insertPlaybackError,
arg.TrackID,
arg.UserID,
arg.ClientID,
arg.Kind,
arg.Detail,
)
var i PlaybackError
err := row.Scan(
&i.ID,
&i.TrackID,
&i.UserID,
&i.ClientID,
&i.Kind,
&i.Detail,
&i.OccurredAt,
&i.ResolvedAt,
&i.ResolvedBy,
&i.Resolution,
)
return i, err
}
const listAdminPlaybackErrors = `-- name: ListAdminPlaybackErrors :many
SELECT
pe.id AS id,
pe.track_id AS track_id,
pe.user_id AS user_id,
u.username AS username,
pe.client_id AS client_id,
pe.kind AS kind,
pe.detail AS detail,
pe.occurred_at AS occurred_at,
pe.resolved_at AS resolved_at,
pe.resolution AS resolution,
t.title AS track_title,
t.file_path AS track_file_path,
ar.name AS artist_name,
al.title AS album_title,
al.id AS album_id
FROM playback_errors pe
JOIN users u ON u.id = pe.user_id
JOIN tracks t ON t.id = pe.track_id
JOIN albums al ON al.id = t.album_id
JOIN artists ar ON ar.id = t.artist_id
WHERE ($1::bool AND pe.resolved_at IS NOT NULL)
OR (NOT $1::bool AND pe.resolved_at IS NULL)
ORDER BY pe.occurred_at DESC
LIMIT $3::int OFFSET $2::int
`
type ListAdminPlaybackErrorsParams struct {
ResolvedFilter bool
Off int32
Lim int32
}
type ListAdminPlaybackErrorsRow struct {
ID pgtype.UUID
TrackID pgtype.UUID
UserID pgtype.UUID
Username string
ClientID string
Kind string
Detail *string
OccurredAt pgtype.Timestamptz
ResolvedAt pgtype.Timestamptz
Resolution *string
TrackTitle string
TrackFilePath string
ArtistName string
AlbumTitle string
AlbumID pgtype.UUID
}
// Admin inbox query. Returns one row per playback_errors row joined
// with track / album / artist metadata so the SPA can render without
// a second round-trip. Filter by resolved status via the sqlc.arg —
// pass true for the Resolved tab, false for Unresolved (the default).
func (q *Queries) ListAdminPlaybackErrors(ctx context.Context, arg ListAdminPlaybackErrorsParams) ([]ListAdminPlaybackErrorsRow, error) {
rows, err := q.db.Query(ctx, listAdminPlaybackErrors, arg.ResolvedFilter, arg.Off, arg.Lim)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAdminPlaybackErrorsRow
for rows.Next() {
var i ListAdminPlaybackErrorsRow
if err := rows.Scan(
&i.ID,
&i.TrackID,
&i.UserID,
&i.Username,
&i.ClientID,
&i.Kind,
&i.Detail,
&i.OccurredAt,
&i.ResolvedAt,
&i.Resolution,
&i.TrackTitle,
&i.TrackFilePath,
&i.ArtistName,
&i.AlbumTitle,
&i.AlbumID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const resolveAllPlaybackErrorsForTrack = `-- name: ResolveAllPlaybackErrorsForTrack :execrows
UPDATE playback_errors
SET resolved_at = now(),
resolved_by = $2,
resolution = $3
WHERE track_id = $1 AND resolved_at IS NULL
`
type ResolveAllPlaybackErrorsForTrackParams struct {
TrackID pgtype.UUID
ResolvedBy pgtype.UUID
Resolution *string
}
// Bulk-resolve every unresolved error for a track when an admin acts
// on it from a non-inbox surface (existing quarantine flow, etc.).
// Returns the affected row count so the caller can surface "N reports
// auto-resolved" in the response if useful.
func (q *Queries) ResolveAllPlaybackErrorsForTrack(ctx context.Context, arg ResolveAllPlaybackErrorsForTrackParams) (int64, error) {
result, err := q.db.Exec(ctx, resolveAllPlaybackErrorsForTrack, arg.TrackID, arg.ResolvedBy, arg.Resolution)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const resolvePlaybackError = `-- name: ResolvePlaybackError :one
UPDATE playback_errors
SET resolved_at = now(),
resolved_by = $2,
resolution = $3
WHERE id = $1
RETURNING id, track_id, user_id, client_id, kind, detail, occurred_at,
resolved_at, resolved_by, resolution
`
type ResolvePlaybackErrorParams struct {
ID pgtype.UUID
ResolvedBy pgtype.UUID
Resolution *string
}
// Marks a single error resolved. Idempotent over (id, resolution) —
// a second resolve with the same resolution is a no-op rewrite. Returns
// the row so the handler can echo it. The CHECK constraint enforces
// the resolution enum.
func (q *Queries) ResolvePlaybackError(ctx context.Context, arg ResolvePlaybackErrorParams) (PlaybackError, error) {
row := q.db.QueryRow(ctx, resolvePlaybackError, arg.ID, arg.ResolvedBy, arg.Resolution)
var i PlaybackError
err := row.Scan(
&i.ID,
&i.TrackID,
&i.UserID,
&i.ClientID,
&i.Kind,
&i.Detail,
&i.OccurredAt,
&i.ResolvedAt,
&i.ResolvedBy,
&i.Resolution,
)
return i, err
}