15b59a214d
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.
211 lines
5.6 KiB
Go
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
|
|
}
|