feat: the admin Overview shows who is listening, downloads, the library and its disk, and the server's version, uptime and recent errors; a download waiting on you, a client not answering or a full disk count in its verdict (M504 #5627 #5628 #5629 #5630)
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m35s
release / go (push) Successful in 2m0s
release / integration (push) Successful in 5m40s
release / android (push) Successful in 6m15s
release / Build signed APK (releases and dev) (push) Successful in 6m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 17s
release / web (push) Successful in 1m35s
release / go (push) Successful in 2m0s
release / integration (push) Successful in 5m40s
release / android (push) Successful in 6m15s
release / Build signed APK (releases and dev) (push) Successful in 6m30s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 25s
release / Verify release artifacts (tag releases only) (push) Skipped
The version moves to internal/buildinfo, so /rest/ping reports the build instead of "dev". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: admin_status.sql
|
||||
|
||||
package dbq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const downloadsStatus = `-- name: DownloadsStatus :one
|
||||
SELECT COUNT(*) FILTER (WHERE ended_at IS NULL AND import_state IN ('', 'downloading', 'waiting', 'importing'))::bigint AS running,
|
||||
COUNT(*) FILTER (WHERE ended_at IS NULL AND import_state IN ('review', 'failed'))::bigint AS needs_you,
|
||||
MIN(COALESCE(import_updated_at, grabbed_at)) FILTER (WHERE ended_at IS NULL AND import_state IN ('review', 'failed'))::timestamptz AS needs_you_since,
|
||||
COUNT(*) FILTER (WHERE imported_at > now() - INTERVAL '24 hours')::bigint AS imported_day,
|
||||
COUNT(*) FILTER (WHERE ended_at > now() - INTERVAL '24 hours'
|
||||
AND end_reason IN ('stalled', 'failed', 'gone', 'abandoned'))::bigint AS given_up_day
|
||||
FROM grabs
|
||||
WHERE ended_at IS NULL
|
||||
OR ended_at > now() - INTERVAL '24 hours'
|
||||
OR imported_at > now() - INTERVAL '24 hours'
|
||||
`
|
||||
|
||||
type DownloadsStatusRow struct {
|
||||
Running int64
|
||||
NeedsYou int64
|
||||
NeedsYouSince pgtype.Timestamptz
|
||||
ImportedDay int64
|
||||
GivenUpDay int64
|
||||
}
|
||||
|
||||
// Downloads at a glance: running, waiting on a person (and since when), and
|
||||
// the last day's imports and give-ups.
|
||||
func (q *Queries) DownloadsStatus(ctx context.Context) (DownloadsStatusRow, error) {
|
||||
row := q.db.QueryRow(ctx, downloadsStatus)
|
||||
var i DownloadsStatusRow
|
||||
err := row.Scan(
|
||||
&i.Running,
|
||||
&i.NeedsYou,
|
||||
&i.NeedsYouSince,
|
||||
&i.ImportedDay,
|
||||
&i.GivenUpDay,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const libraryStatus = `-- name: LibraryStatus :one
|
||||
SELECT COUNT(*)::bigint AS tracks,
|
||||
COUNT(DISTINCT album_id)::bigint AS albums,
|
||||
COUNT(DISTINCT artist_id)::bigint AS artists,
|
||||
COALESCE(SUM(file_size), 0)::bigint AS bytes
|
||||
FROM tracks
|
||||
WHERE missing_since IS NULL
|
||||
`
|
||||
|
||||
type LibraryStatusRow struct {
|
||||
Tracks int64
|
||||
Albums int64
|
||||
Artists int64
|
||||
Bytes int64
|
||||
}
|
||||
|
||||
// What the library holds on disk: tracks whose files are present, the
|
||||
// albums and artists they make, and their size.
|
||||
func (q *Queries) LibraryStatus(ctx context.Context) (LibraryStatusRow, error) {
|
||||
row := q.db.QueryRow(ctx, libraryStatus)
|
||||
var i LibraryStatusRow
|
||||
err := row.Scan(
|
||||
&i.Tracks,
|
||||
&i.Albums,
|
||||
&i.Artists,
|
||||
&i.Bytes,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listListeningNow = `-- name: ListListeningNow :many
|
||||
|
||||
SELECT pe.started_at, pe.client_id, pe.device_class,
|
||||
u.username,
|
||||
t.id AS track_id, t.title AS track_title, t.duration_ms,
|
||||
ar.name AS artist_name,
|
||||
al.id AS album_id, al.title AS album_title
|
||||
FROM play_events 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 pe.ended_at IS NULL
|
||||
AND pe.started_at > now() - INTERVAL '24 hours'
|
||||
AND pe.started_at + make_interval(secs => t.duration_ms / 1000.0) + INTERVAL '5 minutes' > now()
|
||||
ORDER BY pe.started_at DESC
|
||||
LIMIT 20
|
||||
`
|
||||
|
||||
type ListListeningNowRow struct {
|
||||
StartedAt pgtype.Timestamptz
|
||||
ClientID *string
|
||||
DeviceClass *string
|
||||
Username string
|
||||
TrackID pgtype.UUID
|
||||
TrackTitle string
|
||||
DurationMs int32
|
||||
ArtistName string
|
||||
AlbumID pgtype.UUID
|
||||
AlbumTitle string
|
||||
}
|
||||
|
||||
// The admin Overview's status sections (M504): who is listening, what is
|
||||
// downloading, what the library holds. Each is one cheap read.
|
||||
// Plays in progress: open play events still inside their track's length,
|
||||
// with a few minutes' grace for a pause. A client that never says a play
|
||||
// ended would otherwise read as listening until the 24-hour sweep closes it.
|
||||
func (q *Queries) ListListeningNow(ctx context.Context) ([]ListListeningNowRow, error) {
|
||||
rows, err := q.db.Query(ctx, listListeningNow)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListListeningNowRow
|
||||
for rows.Next() {
|
||||
var i ListListeningNowRow
|
||||
if err := rows.Scan(
|
||||
&i.StartedAt,
|
||||
&i.ClientID,
|
||||
&i.DeviceClass,
|
||||
&i.Username,
|
||||
&i.TrackID,
|
||||
&i.TrackTitle,
|
||||
&i.DurationMs,
|
||||
&i.ArtistName,
|
||||
&i.AlbumID,
|
||||
&i.AlbumTitle,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const playsLastDay = `-- name: PlaysLastDay :one
|
||||
SELECT COUNT(*)::bigint AS plays,
|
||||
COUNT(DISTINCT user_id)::bigint AS listeners,
|
||||
MAX(started_at)::timestamptz AS last_at
|
||||
FROM play_events
|
||||
WHERE started_at > now() - INTERVAL '24 hours'
|
||||
`
|
||||
|
||||
type PlaysLastDayRow struct {
|
||||
Plays int64
|
||||
Listeners int64
|
||||
LastAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
// The last day's listening, for the line under Listening now.
|
||||
func (q *Queries) PlaysLastDay(ctx context.Context) (PlaysLastDayRow, error) {
|
||||
row := q.db.QueryRow(ctx, playsLastDay)
|
||||
var i PlaysLastDayRow
|
||||
err := row.Scan(&i.Plays, &i.Listeners, &i.LastAt)
|
||||
return i, err
|
||||
}
|
||||
Reference in New Issue
Block a user