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:
@@ -12,6 +12,7 @@ import type {
|
||||
ResolvedDuplicatesResponse,
|
||||
AdminPlaybackError,
|
||||
AdminQuarantineRow,
|
||||
AdminStatus,
|
||||
AdminSummary,
|
||||
QuarantineActionRow,
|
||||
MusicRequest,
|
||||
@@ -36,6 +37,21 @@ export function createAdminSummaryQuery() {
|
||||
});
|
||||
}
|
||||
|
||||
// Admin status (M504): who is listening, downloads, the library and disk,
|
||||
// and the server, for the Overview below its verdict. Polled while open.
|
||||
|
||||
export async function getAdminStatus(): Promise<AdminStatus> {
|
||||
return api.get<AdminStatus>('/api/admin/status');
|
||||
}
|
||||
|
||||
export function createAdminStatusQuery() {
|
||||
return createQuery({
|
||||
queryKey: qk.adminStatus(),
|
||||
queryFn: getAdminStatus,
|
||||
refetchInterval: 15_000
|
||||
});
|
||||
}
|
||||
|
||||
// Admin request queue -----------------------------------------------------
|
||||
|
||||
export async function listAdminRequests(
|
||||
|
||||
@@ -60,6 +60,7 @@ export const qk = {
|
||||
['adminPlaybackErrors', { resolved: resolved ?? false }] as const,
|
||||
scanStatus: () => ['scanStatus'] as const,
|
||||
adminSummary: () => ['adminSummary'] as const,
|
||||
adminStatus: () => ['adminStatus'] as const,
|
||||
coverage: () => ['coverage'] as const,
|
||||
fingerprintCoverage: () => ['fingerprintCoverage'] as const,
|
||||
coverProviders: () => ['coverProviders'] as const,
|
||||
|
||||
@@ -879,3 +879,49 @@ export type AdminSummary = {
|
||||
suspect: AdminQueue;
|
||||
missing: AdminQueue;
|
||||
};
|
||||
|
||||
// What GET /api/admin/status returns (M504): the Overview's status sections.
|
||||
export type AdminListening = {
|
||||
username: string;
|
||||
track_id: string;
|
||||
title: string;
|
||||
artist_name: string;
|
||||
album_id: string;
|
||||
album_title: string;
|
||||
duration_ms: number;
|
||||
started_at: string;
|
||||
client: string;
|
||||
device_class: string;
|
||||
};
|
||||
// data marks Minstrel's own data folder, beside the library's folders.
|
||||
export type AdminDisk = { path: string; data?: boolean; total_bytes: number; free_bytes: number; error?: string };
|
||||
export type AdminServerError = { at: string; message: string; attrs: string };
|
||||
export type AdminStatus = {
|
||||
listening: {
|
||||
now: AdminListening[];
|
||||
plays_day: number;
|
||||
listeners_day: number;
|
||||
last_play_at: string | null;
|
||||
};
|
||||
downloads: {
|
||||
running: number;
|
||||
needs_you: number;
|
||||
needs_you_since: string | null;
|
||||
imported_day: number;
|
||||
given_up_day: number;
|
||||
live: Download[];
|
||||
clients_down: { client: string; detail: string; since: string }[];
|
||||
};
|
||||
library: { tracks: number; albums: number; artists: number; bytes: number; disks: AdminDisk[] };
|
||||
server: {
|
||||
version: string;
|
||||
channel: string;
|
||||
started_at: string;
|
||||
// The database schema's version, and whether its last migration stopped
|
||||
// part way.
|
||||
migration: number;
|
||||
dirty: boolean;
|
||||
errors: AdminServerError[];
|
||||
error_count: number;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { formatBytes } from './bytes';
|
||||
|
||||
describe('formatBytes', () => {
|
||||
test('picks the unit that keeps the figure above one', () => {
|
||||
expect(formatBytes(0)).toBe('0 B');
|
||||
expect(formatBytes(999)).toBe('999 B');
|
||||
expect(formatBytes(12_300)).toBe('12.3 KB');
|
||||
expect(formatBytes(820_000_000_000)).toBe('820 GB');
|
||||
expect(formatBytes(1_400_000_000_000)).toBe('1.4 TB');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
// A size in bytes, in the largest unit that keeps it above one: "1.4 TB",
|
||||
// "820 GB", "12 KB". Decimal units, as drives are sold and as `df -H` shows.
|
||||
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||
|
||||
export function formatBytes(n: number): string {
|
||||
let v = n;
|
||||
let u = 0;
|
||||
while (v >= 1000 && u < UNITS.length - 1) {
|
||||
v /= 1000;
|
||||
u++;
|
||||
}
|
||||
const digits = u === 0 || v >= 100 ? 0 : 1;
|
||||
return `${v.toFixed(digits)} ${UNITS[u]}`;
|
||||
}
|
||||
Reference in New Issue
Block a user