From 10d50dccbde5a5da7926d7a9d70160dcf30795b7 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 23 Oct 2026 12:42:12 -0400 Subject: [PATCH] docs: web comments say what the app does, not how it came to be (#6277) Rewrites the web app's comments to rule 32. Task, milestone, rule and spec ids are gone; regression stories are restated as the trap they guard (public auth routes, the menu's stopPropagation, universal loads under adapter-static, metric deltas with their margin). Flutter and Tauri mentions now name the Android app or a native shell, operator quotes and dates are dropped, and one library's counts no longer appear outside test fixtures. Verified with a TypeScript-scanner token comparison (template literals and regex literals re-scanned): only comment text changed. Co-Authored-By: Claude Opus 5.5 --- web/csp.go | 2 +- web/csp_test.go | 2 +- web/scripts/check-tint-contrast.js | 4 +- web/scripts/tokens-to-css.js | 10 +- web/src/lib/admin/queues.ts | 5 +- web/src/lib/admin/reports.ts | 2 +- web/src/lib/api/admin.ts | 66 +++++------ web/src/lib/api/catalogue.ts | 4 +- web/src/lib/api/errors.ts | 10 +- web/src/lib/api/me.ts | 6 +- web/src/lib/api/metrics.ts | 6 +- web/src/lib/api/normalization.ts | 4 +- web/src/lib/api/notifications.ts | 4 +- web/src/lib/api/playlists.ts | 9 +- web/src/lib/api/queries.ts | 12 +- web/src/lib/api/requests.ts | 7 +- web/src/lib/api/suggestions.ts | 8 +- web/src/lib/api/tuning.ts | 8 +- web/src/lib/api/types.ts | 108 +++++++++--------- web/src/lib/api/wanted.ts | 18 +-- web/src/lib/auth/publicRoutes.test.ts | 16 ++- web/src/lib/auth/publicRoutes.ts | 11 +- web/src/lib/auth/store.svelte.ts | 2 +- web/src/lib/components/AcoustIDCard.svelte | 7 +- web/src/lib/components/AcquisitionCard.svelte | 10 +- .../components/AcquisitionServiceRow.svelte | 2 +- web/src/lib/components/ActiveSessions.svelte | 2 +- .../lib/components/CollectionHeader.svelte | 2 +- web/src/lib/components/Cover.svelte | 3 +- .../lib/components/DiscoverResultCard.svelte | 6 +- .../lib/components/DiscoverResultCard.test.ts | 18 +-- .../components/FingerprintSettingsCard.svelte | 10 +- .../FingerprintSettingsCard.test.ts | 4 +- web/src/lib/components/FollowControl.svelte | 2 +- .../components/LibraryFoldersSetting.svelte | 2 +- .../lib/components/ListContinuation.svelte | 6 +- .../components/LoudnessSettingsCard.svelte | 4 +- .../lib/components/MobileAppDownload.svelte | 6 +- .../lib/components/NetworkSettingsCard.svelte | 2 +- .../components/NetworkSettingsCard.test.ts | 2 +- .../components/NormalizationSetting.svelte | 4 +- .../components/NotificationEmailCard.svelte | 6 +- .../components/NotificationSettings.svelte | 8 +- web/src/lib/components/PageColumn.svelte | 9 +- web/src/lib/components/PlayActions.svelte | 7 +- web/src/lib/components/PlayerBar.svelte | 2 +- web/src/lib/components/PlayerBar.test.ts | 6 +- web/src/lib/components/PlaylistCard.svelte | 8 +- web/src/lib/components/PlaylistCard.test.ts | 2 +- .../lib/components/PlaylistTrackRow.svelte | 2 +- .../lib/components/PlaylistTrackRow.test.ts | 2 +- .../lib/components/PublicAddressCard.svelte | 4 +- web/src/lib/components/QueueTrackRow.svelte | 14 +-- web/src/lib/components/QueueTrackRow.test.ts | 4 +- .../ReacquisitionSettingsCard.svelte | 6 +- .../ReacquisitionSettingsCard.test.ts | 10 +- .../components/RecommendationMetrics.svelte | 18 +-- .../components/RecommendationMetrics.test.ts | 9 +- .../lib/components/ReleaseProfilesCard.svelte | 2 +- web/src/lib/components/RequestRow.test.ts | 2 +- .../lib/components/SearchSettingsCard.svelte | 8 +- web/src/lib/components/SectionRail.svelte | 4 +- web/src/lib/components/ServerVersion.svelte | 15 ++- web/src/lib/components/Shell.svelte | 33 +++--- web/src/lib/components/StatusPill.svelte | 2 +- web/src/lib/components/StatusPill.test.ts | 2 +- web/src/lib/components/SuggestionFeed.svelte | 14 +-- web/src/lib/components/SuggestionFeed.test.ts | 2 +- web/src/lib/components/TabStrip.svelte | 4 +- web/src/lib/components/TrackList.svelte | 2 +- web/src/lib/components/TrackListRow.svelte | 4 +- web/src/lib/components/TrackRow.svelte | 2 +- web/src/lib/components/TrackRow.test.ts | 2 +- web/src/lib/components/WantCandidates.svelte | 2 +- .../lib/components/admin/ProviderGroup.svelte | 6 +- .../lib/components/admin/SmtpSettings.svelte | 4 +- web/src/lib/components/reorder-drag.ts | 2 +- .../lib/components/settings/Segmented.svelte | 2 +- .../lib/components/settings/SettingRow.svelte | 2 +- .../settings/SettingsColumns.svelte | 2 +- .../components/settings/SettingsGroup.svelte | 6 +- .../lib/components/wanted/BlockList.svelte | 2 +- .../wanted/CatalogueArtistCard.svelte | 2 +- .../components/wanted/CatalogueGrid.svelte | 6 +- .../components/wanted/CollectionRail.svelte | 8 +- .../lib/components/wanted/DownloadList.svelte | 4 +- .../lib/components/wanted/FollowList.svelte | 2 +- .../components/wanted/UnmatchedList.svelte | 2 +- .../components/wanted/UnmatchedMatch.svelte | 2 +- web/src/lib/components/wanted/WantList.svelte | 8 +- .../lib/components/wanted/WantTracks.svelte | 2 +- web/src/lib/media/covers.ts | 2 +- web/src/lib/media/dominantColor.ts | 2 +- web/src/lib/player/audioLoader.ts | 9 +- web/src/lib/player/events.svelte.ts | 25 ++-- web/src/lib/player/gain.ts | 2 +- web/src/lib/player/gainStage.svelte.ts | 8 +- web/src/lib/player/store.svelte.ts | 45 ++++---- web/src/lib/player/store.test.ts | 30 ++--- web/src/lib/playlists/playlistTrackToRef.ts | 2 +- web/src/lib/playlists/systemRefetch.ts | 2 +- web/src/lib/serverEvents.svelte.ts | 12 +- web/src/lib/utils/plural.test.ts | 5 +- web/src/lib/utils/plural.ts | 2 +- web/src/lib/utils/relativeTime.test.ts | 6 +- web/src/lib/utils/relativeTime.ts | 4 +- web/src/lib/wanted/downloadState.ts | 2 +- web/src/lib/wanted/wantStatus.ts | 4 +- web/src/routes/+layout.svelte | 4 +- web/src/routes/+page.svelte | 13 +-- web/src/routes/admin/+layout.svelte | 4 +- web/src/routes/admin/+page.svelte | 10 +- web/src/routes/admin/acquisition/+page.svelte | 2 +- web/src/routes/admin/admin.test.ts | 6 +- web/src/routes/admin/diagnostics/+page.svelte | 6 +- web/src/routes/admin/email/+page.svelte | 2 +- web/src/routes/admin/library/+page.svelte | 6 +- web/src/routes/admin/metadata/+page.svelte | 2 +- web/src/routes/admin/requests/+page.svelte | 4 +- web/src/routes/admin/tuning/+page.svelte | 21 ++-- web/src/routes/admin/tuning/tuning.test.ts | 22 ++-- web/src/routes/admin/users/+page.svelte | 2 +- web/src/routes/albums/[id]/+page.svelte | 2 +- web/src/routes/albums/[id]/album.test.ts | 6 +- web/src/routes/artists/[id]/+page.svelte | 2 +- web/src/routes/artists/[id]/artist.test.ts | 7 +- web/src/routes/collection/+layout.svelte | 6 +- web/src/routes/collection/+layout.ts | 2 +- .../collection/artist/[mbid]/+page.svelte | 2 +- web/src/routes/collection/collection.test.ts | 2 +- .../routes/collection/duplicates/+page.svelte | 43 +++---- .../collection/duplicates/duplicates.test.ts | 16 +-- .../routes/collection/missing/+page.svelte | 7 +- .../routes/collection/missing/missing.test.ts | 4 +- .../routes/collection/reports/+page.svelte | 6 +- .../collection/suspect-sources/+page.svelte | 11 +- .../suspect-sources/suspect-sources.test.ts | 4 +- web/src/routes/discover/+page.svelte | 2 +- web/src/routes/discover/discover.test.ts | 4 +- web/src/routes/library/+layout.svelte | 13 +-- web/src/routes/library/+page.ts | 7 +- web/src/routes/library/albums/+page.svelte | 2 +- web/src/routes/library/albums/empty.test.ts | 4 +- web/src/routes/library/artists/+page.svelte | 2 +- web/src/routes/library/genres/+page.svelte | 4 +- web/src/routes/library/genres/genres.test.ts | 6 +- web/src/routes/library/liked/+page.svelte | 2 +- web/src/routes/library/liked/liked.test.ts | 2 +- web/src/routes/library/years/years.test.ts | 2 +- web/src/routes/now-playing/+page.svelte | 4 +- web/src/routes/page.test.ts | 7 +- web/src/routes/playlists/+page.ts | 10 +- web/src/routes/playlists/[id]/+page.svelte | 12 +- .../routes/playlists/[id]/playlist.test.ts | 9 +- web/src/routes/search/albums/albums.test.ts | 2 +- web/src/routes/search/artists/artists.test.ts | 2 +- web/src/routes/search/tracks/tracks.test.ts | 2 +- web/src/routes/settings/+layout.svelte | 2 +- web/src/routes/settings/+page.ts | 4 +- web/src/routes/settings/account/+page.svelte | 6 +- .../routes/settings/account/account.test.ts | 4 +- .../routes/settings/connections/+page.svelte | 6 +- web/src/test-utils/appModuleMocks.test.ts | 4 +- web/vitest.setup.ts | 21 ++-- 164 files changed, 602 insertions(+), 624 deletions(-) diff --git a/web/csp.go b/web/csp.go index 7ac77736..054f2cea 100644 --- a/web/csp.go +++ b/web/csp.go @@ -20,7 +20,7 @@ var srcAttrRe = regexp.MustCompile(`(?i)\bsrc\s*=`) // page carries: the theme bootstrap in app.html, the branding global the Vite // plugin injects, and SvelteKit's start-up block. Their hashes are taken from // the page AFTER the branding template has run, so they match the bytes the -// browser actually receives, whatever the operator's app name. Any script +// browser actually receives, whatever the app is named. Any script // that differs, including one injected through an XSS, is refused. // // The rest: diff --git a/web/csp_test.go b/web/csp_test.go index b603597a..52e2d725 100644 --- a/web/csp_test.go +++ b/web/csp_test.go @@ -49,7 +49,7 @@ func TestContentSecurityPolicy_HashesInlineScriptsOnly(t *testing.T) { } // The hash must be of the page as served, after the branding template has -// substituted the operator's app name, or a renamed instance would refuse +// substituted the app's name, or a renamed instance would refuse // its own bootstrap script. func TestContentSecurityPolicy_TracksTemplatedContent(t *testing.T) { a := contentSecurityPolicy([]byte(``)) diff --git a/web/scripts/check-tint-contrast.js b/web/scripts/check-tint-contrast.js index 5d6af080..60d99493 100644 --- a/web/scripts/check-tint-contrast.js +++ b/web/scripts/check-tint-contrast.js @@ -1,4 +1,4 @@ -// Finds a hue painted as TEXT on a tint of itself (#3150). The tint sits close +// Finds a hue painted as TEXT on a tint of itself. The tint sits close // to the surface under it, so the raw hue on it measures 1.6–2.4:1 against // AA's 4.5. The fix is the hue's -fg partner (tokens.json colors.fg), mixed // toward parchment: accent-fg on an accent tint measures 5.03. @@ -67,7 +67,7 @@ export function findCss(source) { // Hues that fail AA as text even on a plain surface, so the raw hue is never // text at all. Dark mode, against 4.5: // error 2.64 on slate, 3.23 on iron → error-fg 5.30 at worst -// accent 2.21 on slate, 3.04 on the page → accent-fg 5.62 at worst (#5318) +// accent 2.21 on slate, 3.04 on the page → accent-fg 5.62 at worst // Accent icons (the liked heart, active shuffle/repeat) follow too: as // graphics they only need 3:1, and the raw accent misses even that on iron. // A border or outline in the hue is a graphic and stays allowed. diff --git a/web/scripts/tokens-to-css.js b/web/scripts/tokens-to-css.js index 42495359..527ed6a1 100644 --- a/web/scripts/tokens-to-css.js +++ b/web/scripts/tokens-to-css.js @@ -1,8 +1,8 @@ #!/usr/bin/env node -// Reads ../src/lib/styles/tokens.json and emits tokens.generated.css. -// Single source of truth for FabledSword tokens shared with the Flutter -// client. Keep output deterministic — Tailwind reads tokens.json directly, -// the .css emission is for raw CSS consumers and theme switching. +// Reads ../src/lib/styles/tokens.json and emits tokens.generated.css. Single +// source of truth for the FabledSword tokens, which the Android app mirrors in +// FabledSwordTokens.kt. Keep output deterministic — Tailwind reads tokens.json +// directly, the .css emission is for raw CSS consumers and theme switching. import { readFileSync, writeFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; @@ -19,7 +19,7 @@ export function emit(tokens) { for (const [name, value] of Object.entries(tokens.colors.flat)) { lines.push(` --fs-${name}: ${value};`); } - // The house -fg tokens (FabledSword, 2026-08-27; #3150): a hue as TEXT on a + // The house -fg tokens: a hue as TEXT on a // tint of itself, mixed toward parchment. Parchment inverts in light mode, // so one declaration lightens the text on dark surfaces and darkens it on // light ones. Declared on :root, the element data-theme is set on, so the diff --git a/web/src/lib/admin/queues.ts b/web/src/lib/admin/queues.ts index 767831a7..7232b02d 100644 --- a/web/src/lib/admin/queues.ts +++ b/web/src/lib/admin/queues.ts @@ -1,11 +1,10 @@ import { plural } from '#lib/utils/plural.js'; import type { AdminQueue, AdminSummary } from '#lib/api/types.js'; -// The queues that wait on an admin (#5605), in the order the Overview lists +// The queues that wait on an admin, in the order the Overview lists // them. The rails and the Overview read the same list, so a queue that // needs you is counted the same in every place. Requests live under Admin; -// the library's health queues, Missing files among them, under Collection -// (M504). +// the library's health queues, Missing files among them, under Collection. export type QueueKey = keyof AdminSummary; export type QueueDef = { diff --git a/web/src/lib/admin/reports.ts b/web/src/lib/admin/reports.ts index 3720c80e..7332b7ff 100644 --- a/web/src/lib/admin/reports.ts +++ b/web/src/lib/admin/reports.ts @@ -7,7 +7,7 @@ import type { PlaybackErrorKind } from '#lib/api/types.js'; -// Admin › Reports (#5606): a broken track is one thing to decide about, +// Admin › Reports: a broken track is one thing to decide about, // whether a listener flagged it or it failed to play, so the two sources // fold into one row per track. The rail's count (the summary's `reports`) // counts the same way, by track. diff --git a/web/src/lib/api/admin.ts b/web/src/lib/api/admin.ts index 8e71dfb1..058ee980 100644 --- a/web/src/lib/api/admin.ts +++ b/web/src/lib/api/admin.ts @@ -21,7 +21,7 @@ import type { PlaybackErrorResolution } from './types'; -// Admin summary (#5605) --------------------------------------------------- +// Admin summary --------------------------------------------------- export async function getAdminSummary(): Promise { return api.get('/api/admin/summary'); @@ -38,7 +38,7 @@ export function createAdminSummaryQuery() { }); } -// Admin status (M504): who is listening, downloads, the library and disk, +// Admin status: 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 { @@ -68,7 +68,7 @@ export async function listAdminRequests( ); } -// Approving puts the request on the wanted list (M502 #5520). An artist +// Approving puts the request on the wanted list. An artist // request is followed on profileId, or the default profile when omitted. export async function approveRequest(id: string, profileId?: string): Promise { return api.post( @@ -93,7 +93,7 @@ export function createAdminRequestsQuery(status?: RequestStatus) { queryFn: () => listAdminRequests(status), // Only the 'approved' tab needs polling — that's where in-flight // ingests live. Other tabs (pending/rejected/completed) are static - // until the operator acts on them. + // until an admin acts on them. refetchInterval: (query) => { if (status !== 'approved') return false; const rows = query.state.data as MusicRequest[] | undefined; @@ -280,14 +280,14 @@ export function createCoverageQuery() { }); } -// Fingerprint backfill (#3908) -------------------------------------------- +// Fingerprint backfill -------------------------------------------- export type FingerprintCoverage = { total: number; fingerprinted: number; rejected: number; pending: number; - // False when the operator has switched fingerprinting off (#3913): pending + // False when an admin has switched fingerprinting off: pending // then never shrinks, and nothing should read as progress. enabled: boolean; }; @@ -307,7 +307,7 @@ export function createFingerprintCoverageQuery() { }); } -// Fingerprinting settings (#3913) ------------------------------------------ +// Fingerprinting settings ------------------------------------------ export type FingerprintSettings = { enabled: boolean; @@ -317,7 +317,7 @@ export type FingerprintSettings = { acoustic_max_bit_error_rate: number; backfill_concurrency: number; sweep_interval_hours: number; - // Lets the duplicate resolver act on its own (M498): merge copies no + // Lets the duplicate resolver act on its own: merge copies no // release needs. auto_resolve: boolean; }; @@ -332,7 +332,7 @@ export async function updateFingerprintSettings( return api.put('/api/admin/library/fingerprint-settings', s); } -// Loudness analysis (#4995) ------------------------------------------------- +// Loudness analysis ------------------------------------------------- export type LoudnessCoverage = { total: number; @@ -342,7 +342,7 @@ export type LoudnessCoverage = { silent: number; unreadable: number; pending: number; - // False when the operator has switched analysis off: pending then never + // False when an admin has switched analysis off: pending then never // shrinks, and nothing should read as progress. enabled: boolean; }; @@ -354,7 +354,7 @@ export async function getLoudnessCoverage(): Promise { export type LoudnessSettings = { enabled: boolean; backfill_concurrency: number; - /** Disk for the leveled copies rendered for Sonos/UPnP speakers (#5001). */ + /** Disk for the leveled copies rendered for Sonos/UPnP speakers. */ leveled_cache_mb: number; }; @@ -366,7 +366,7 @@ export async function updateLoudnessSettings(s: LoudnessSettings): Promise('/api/admin/library/loudness-settings', s); } -// The library's folders (#6129) ----------------------------------------------- +// The library's folders ----------------------------------------------- export type LibraryFolder = { path: string; @@ -395,7 +395,7 @@ export async function updateLibraryFolders(folders: LibraryFolderChange[]): Prom return api.put('/api/admin/library/folders', { folders }); } -// AcoustID recording-id lookup (M401) --------------------------------------- +// AcoustID recording-id lookup --------------------------------------- export type AcoustIDSettings = { enabled: boolean; @@ -529,7 +529,7 @@ export function createCoverProvidersQuery() { // Tag-enrichment providers ------------------------------------------------- // Parallel to the cover-art providers surface, over /api/admin/tag-sources. // Independent settings so a new folksonomy source is added without touching -// art config (#1490). +// art config. export type TagProviderCapability = 'track_tags'; @@ -656,7 +656,7 @@ export async function updateUserAutoApprove(id: string, autoApprove: boolean): P return api.put(`/api/admin/users/${id}/auto-approve`, { auto_approve: autoApprove }); } -// Flip an account's diagnostics/debug-reporting opt-in (M9). Admin-set; +// Flip an account's diagnostics/debug-reporting opt-in. Admin-set; // the client obeys it (with a local per-device OFF switch). export async function updateUserDebugMode(id: string, enabled: boolean): Promise { return api.put(`/api/admin/users/${id}/debug-mode`, { enabled }); @@ -709,7 +709,7 @@ export function createSMTPConfigQuery() { }); } -// Device diagnostics (M9) --------------------------------------------------- +// Device diagnostics --------------------------------------------------- // Coarse event category. The finer event sub-type lives inside `payload`. export type DiagnosticKind = @@ -773,7 +773,7 @@ export function createAdminDiagnosticsQuery(f: DiagnosticsFilter) { return createQuery({ queryKey: qk.adminDiagnostics(f as Record), queryFn: () => listAdminDiagnostics(f), - // The operator enables debug then watches events stream in; a short + // An admin enables debug then watches events stream in; a short // poll keeps the timeline live without manual refresh. refetchInterval: 10_000, staleTime: 5_000 @@ -788,7 +788,7 @@ export function createDiagnosticDevicesQuery(userId?: string) { }); } -// Trusted-proxy depth (#2453) --------------------------------------------- +// Trusted-proxy depth --------------------------------------------- // detected_client_ip / forwarded_chain / remote_addr describe THIS request // under the current setting, so the admin card can be verified rather than @@ -823,7 +823,7 @@ export async function updatePublicUrl(publicUrl: string): Promise= last.total ? undefined : loaded; } -// Loads as the page scrolls (rule 172), one tab at a time. Polls so a sweep -// or resolver pass finishing shows up while the operator is on the page. +// Loads as the page scrolls, one tab at a time. Polls so a sweep +// or resolver pass finishing shows up while an admin is on the page. export function createDuplicatesQuery(view: DuplicatesView = 'review') { return createInfiniteQuery({ queryKey: qk.adminDuplicates(view), @@ -866,7 +866,7 @@ export function resolvedDuplicatesNextOffset(last: ResolvedDuplicatesResponse): return last.items.length === 0 || loaded >= last.total ? undefined : loaded; } -// What the resolver did by itself (M498), newest first. +// What the resolver did by itself, newest first. export function createResolvedDuplicatesQuery() { return createInfiniteQuery({ queryKey: qk.adminResolvedDuplicates(), @@ -881,9 +881,9 @@ export async function runDuplicateSweep(): Promise<{ started: boolean }> { return api.post<{ started: boolean }>('/api/admin/library/duplicates/sweep', {}); } -// Merges a group into the copy chosen to keep, removing the other copies' files. -// survivor_track_id is the operator's choice; the server checks it belongs to -// the group. +// Merges a group into the copy chosen to keep, removing the other copies' +// files. survivor_track_id is an admin's choice; the server checks it belongs +// to the group. export async function mergeDuplicateGroup( id: string, body: { survivor_track_id: string } @@ -894,7 +894,7 @@ export async function mergeDuplicateGroup( ); } -// One recording in several of an album's slots (#6184): keep the copy whose +// One recording in several of an album's slots: keep the copy whose // title it is, or none (empty keep_track_id), and fetch the album again. export async function refetchDuplicateGroup( id: string, @@ -910,7 +910,7 @@ export async function dismissDuplicateGroup(id: string): Promise { await api.post(`/api/admin/library/duplicates/${encodeURIComponent(id)}/dismiss`, {}); } -// Missing files (#2527) ----------------------------------------------------- +// Missing files ----------------------------------------------------- export async function listMissingFiles( offset: number = 0, @@ -936,7 +936,7 @@ export function createMissingFilesQuery(offset: number = 0, limit: number = 50) }); } -// Suspect sources (#5410) -------------------------------------------------- +// Suspect sources -------------------------------------------------- export const SUSPECT_SOURCES_PAGE_SIZE = 50; @@ -952,7 +952,7 @@ export function suspectSourcesNextOffset(last: AdminSuspectResponse): number | u return loaded >= last.total ? undefined : loaded; } -// Loads as the page scrolls (rule 172). Changes only when files are added or +// Loads as the page scrolls. Changes only when files are added or // removed, so it is not re-fetched on every focus. export function createSuspectSourcesQuery() { return createInfiniteQuery({ @@ -965,12 +965,12 @@ export function createSuspectSourcesQuery() { } // 'fine' lets a flagged track back into radio and the mixes; 'suspect' holds -// it back again (M498 #5439). +// it back again. export async function setSuspectVerdict(trackId: string, verdict: SuspectVerdict): Promise { await api.put(`/api/admin/library/suspect-sources/${encodeURIComponent(trackId)}`, { verdict }); } -// Missing-file re-acquisition (#2527 / milestone #290) ---------------------- +// Missing-file re-acquisition ---------------------- export type ReacquisitionSettings = { enabled: boolean; @@ -992,7 +992,7 @@ export async function updateReacquisitionSettings( return api.put('/api/admin/library/reacquisition', s); } -// Notification emails (M489 #5346): when the grouped emails go out. +// Notification emails: when the grouped emails go out. export type NotificationEmailSettings = { summary_hour: number; batch_window_minutes: number; @@ -1008,7 +1008,7 @@ export async function updateNotificationEmailSettings( return api.put('/api/admin/notification-email', s); } -// Indexers and download clients (M502 #5518) ------------------------------- +// Indexers and download clients ------------------------------- export type AcquisitionProtocol = 'prowlarr' | 'torznab' | 'qbittorrent' | 'exchange'; diff --git a/web/src/lib/api/catalogue.ts b/web/src/lib/api/catalogue.ts index 8729a85a..09d66f43 100644 --- a/web/src/lib/api/catalogue.ts +++ b/web/src/lib/api/catalogue.ts @@ -3,8 +3,8 @@ import { api } from './client'; import { qk } from './queries'; import type { CatalogueSearchResult, RequestKind } from './types'; -// searchCatalogue asks MusicBrainz through Minstrel's release catalogue -// (M502 #5519). URLSearchParams handles encoding (spaces, quotes, +// searchCatalogue asks MusicBrainz through Minstrel's release catalogue. +// URLSearchParams handles encoding (spaces, quotes, // ampersands) so callers can pass q raw. export async function searchCatalogue( q: string, diff --git a/web/src/lib/api/errors.ts b/web/src/lib/api/errors.ts index 3a2f5858..6b77147e 100644 --- a/web/src/lib/api/errors.ts +++ b/web/src/lib/api/errors.ts @@ -9,20 +9,20 @@ export function errCode(err: unknown): string { } /** - * Codes whose server message carries specifics the operator needs in order to + * Codes whose server message carries specifics an admin needs in order to * act — which directory, which uid — that fixed copy cannot say. For these the - * message follows the copy (#3918). Kept to a named list on purpose: most + * message follows the copy. Kept to a named list on purpose: most * server messages are internal detail and must never reach a toast. Mirrored * in Android's ErrorCopy. */ const DETAIL_CODES: ReadonlySet = new Set([ 'library_not_writable', 'file_delete_failed', - // The server names the field and its range (#3913). + // The server names the field and its range. 'invalid_setting', - // The server says what shape of address it wants (#4981). + // The server says what shape of address it wants. 'invalid_public_url', - // The server names the folder and what is wrong with it (#6129). + // The server names the folder and what is wrong with it. 'bad_folders' ]); diff --git a/web/src/lib/api/me.ts b/web/src/lib/api/me.ts index d2217c2c..2a9fdb96 100644 --- a/web/src/lib/api/me.ts +++ b/web/src/lib/api/me.ts @@ -43,7 +43,7 @@ export async function changePassword(currentPassword: string, newPassword: strin } // The signed-in user's profile, to fill the Account form with what is -// stored (#5596): a form that starts blank sends blanks, and a blank clears. +// stored: a form that starts blank sends blanks, and a blank clears. export async function getMyProfile(): Promise { return api.get('/api/me'); } @@ -61,7 +61,7 @@ export async function regenerateAPIToken(): Promise { return api.post('/api/me/api-token', {}); } -// Subsonic password (#5026) ------------------------------------------------ +// Subsonic password ------------------------------------------------ // For Subsonic clients that only sign in with a username and password (the // t/s scheme). The server generates it, so it is never the login password; // like the API key it is shown once, when generated. @@ -89,7 +89,7 @@ export async function putMyTimezone(timezone: string): Promise { await api.put('/api/me/timezone', { timezone }); } -// Active sessions (#370) --------------------------------------------------- +// Active sessions --------------------------------------------------- // created_ip is frozen at issue time; last_ip moves with the session. The // pair is the signal — the same device string arriving from an address you diff --git a/web/src/lib/api/metrics.ts b/web/src/lib/api/metrics.ts index 776ee52b..c8e465ea 100644 --- a/web/src/lib/api/metrics.ts +++ b/web/src/lib/api/metrics.ts @@ -3,8 +3,8 @@ import { api } from './client'; // Mirrors internal/api/me_recommendation_metrics.go: raw play sources are // bucketed server-side into stable surface families, grouped by intent, and -// anchored by the manual-plays baseline (milestone #127). -// A difference from the baseline, with its uncertainty (#2495). Both figures +// anchored by the manual-plays baseline. +// A difference from the baseline, with its uncertainty. Both figures // are already in percentage points — the server does the arithmetic so both // clients read the same numbers. // @@ -31,7 +31,7 @@ export type SurfaceMetric = { skip_delta?: MetricDelta; completion_delta?: MetricDelta; // Present when the surface's builder stamps pick-kind provenance and - // the window holds attributed plays (#1249, generalized #1270): For + // the window holds attributed plays: For // You's taste/fresh split, Discover's candidate buckets, the tiered // mixes' tier1-3 — plus pre-attribution plays. breakdown?: SurfaceMetric[]; diff --git a/web/src/lib/api/normalization.ts b/web/src/lib/api/normalization.ts index 9c7db924..e9786b16 100644 --- a/web/src/lib/api/normalization.ts +++ b/web/src/lib/api/normalization.ts @@ -1,6 +1,6 @@ import { api, apiFetch } from './client'; -// Loudness normalization preference (M464 #4998). One per user, stored on +// Loudness normalization preference. One per user, stored on // the server so the web player, the Android app and casts agree. export type NormalizationMode = 'off' | 'auto' | 'track' | 'album'; export type NormalizationTarget = -18 | -16 | -14; @@ -41,7 +41,7 @@ export function putNormalization(p: NormalizationPrefs): Promise('/api/me/normalization', p); } -// Per-track gains (#4997): dB to the -18 LUFS ReplayGain reference and linear +// Per-track gains: dB to the -18 LUFS ReplayGain reference and linear // peaks. A field is absent until the server has measured it. export type ReplayGain = { track_gain?: number; diff --git a/web/src/lib/api/notifications.ts b/web/src/lib/api/notifications.ts index 1b283086..b2990852 100644 --- a/web/src/lib/api/notifications.ts +++ b/web/src/lib/api/notifications.ts @@ -2,7 +2,7 @@ import { createQuery } from '@tanstack/svelte-query'; import { api } from './client'; import { qk } from './queries'; -// The notifications inbox (M489). The server renders title, body and link per +// The notifications inbox. The server renders title, body and link per // kind, so every client says the same thing and this module never has to know // what a kind means. @@ -123,7 +123,7 @@ export function createNotificationSettingsQuery() { }); } -/** Short row labels, menu-style (rule 188). Unknown kinds show their key. */ +/** Short row labels, menu-style. Unknown kinds show their key. */ export const NOTIFICATION_KIND_LABELS: Record = { request_approved: 'Request approved', request_rejected: 'Request declined', diff --git a/web/src/lib/api/playlists.ts b/web/src/lib/api/playlists.ts index bd340d74..c3dddfd6 100644 --- a/web/src/lib/api/playlists.ts +++ b/web/src/lib/api/playlists.ts @@ -19,20 +19,19 @@ export async function getPlaylist(id: string): Promise { return api.get(`/api/playlists/${encodeURIComponent(id)}`); } -// #415 stage 2/3: rotation-aware play order for a system playlist. +// Rotation-aware play order for a system playlist. // Same shape as getPlaylist but tracks are server-ordered (unplayed // this rotation first, then heard; resets when exhausted). // Intentionally uncached — it varies per play, unlike getPlaylist. -// {variant} is the raw system_variant (#411 R2: generic by-kind). +// {variant} is the raw system_variant. export async function systemShuffle(variant: string): Promise { return api.get( `/api/playlists/system/${encodeURIComponent(variant)}/shuffle`, ); } -// #411 R2: generic by-kind refresh. Rebuilds the caller's system -// playlists and returns the named kind's fresh row. Replaces the -// former per-kind refreshForYou/refreshDiscover. +// Generic by-kind refresh. Rebuilds the caller's system playlists and +// returns the named kind's fresh row. export type RefreshSystemResponse = { playlist_id: string | null; track_count: number; diff --git a/web/src/lib/api/queries.ts b/web/src/lib/api/queries.ts index 09706f4a..06a49eb4 100644 --- a/web/src/lib/api/queries.ts +++ b/web/src/lib/api/queries.ts @@ -35,21 +35,21 @@ export const qk = { myRequests: () => ['myRequests'] as const, adminRequests: (status?: string) => ['adminRequests', { status: status ?? 'all' }] as const, - // Wanted list (M502 #5520). Wants sit under ['adminWants'] so one prefix + // Wanted list. Wants sit under ['adminWants'] so one prefix // invalidation refreshes every tab and its counts. adminWants: (state: string, q: string = '', artist: string = '') => ['adminWants', { state, q, artist }] as const, wantArtists: (q: string = '') => ['adminWants', 'artists', { q }] as const, wantTracks: (id: string) => ['wantTracks', id] as const, - // The Collection's Unmatched section (#5579). + // The Collection's Unmatched section. unmatched: () => ['adminWants', 'unmatched'] as const, adminFollows: () => ['adminFollows'] as const, adminFollow: (artistMbid: string) => ['adminFollow', artistMbid] as const, releaseProfiles: () => ['releaseProfiles'] as const, - // Search and grab (M502 #5521). + // Search and grab. wantCandidates: (wantId: string) => ['wantCandidates', wantId] as const, searchSettings: () => ['searchSettings'] as const, - // The Wanted view's Activity and Blocked tabs (#5522). + // The Wanted view's Activity and Blocked tabs. downloads: () => ['adminWants', 'downloads'] as const, releaseBlocks: () => ['adminWants', 'releaseBlocks'] as const, myQuarantine: () => ['myQuarantine'] as const, @@ -78,7 +78,7 @@ export const qk = { adminDiagnosticDevices: (userId?: string) => ['adminDiagnosticDevices', { userId: userId ?? 'all' }] as const, smtpConfig: () => ['smtpConfig'] as const, - // Notifications inbox (M489). Both keys sit under ['notifications'] so one + // Notifications inbox. Both keys sit under ['notifications'] so one // prefix invalidation refreshes the list and the badge together. notifications: () => ['notifications', 'list'] as const, notificationsUnread: () => ['notifications', 'unread'] as const, @@ -95,7 +95,7 @@ export const qk = { ['playlists', { kind: kind ?? 'user' }] as const, playlist: (id: string) => ['playlist', id] as const, systemPlaylistsStatus: () => ['systemPlaylistsStatus'] as const, - // Browse indexes (#367). Keys carry no arguments — both are whole-library + // Browse indexes. Keys carry no arguments — both are whole-library // indexes, and the per-genre / per-year album lists are fetched outside // svelte-query because their selection comes from the URL. genres: () => ['genres'] as const, diff --git a/web/src/lib/api/requests.ts b/web/src/lib/api/requests.ts index baeb5225..233914e0 100644 --- a/web/src/lib/api/requests.ts +++ b/web/src/lib/api/requests.ts @@ -3,9 +3,8 @@ import { api, apiFetch } from './client'; import { qk } from './queries'; import type { MusicRequest, RequestKind } from './types'; -/** Cadence for in-flight request polling. Tuned to match the operator's - "fast enough to feel live, slow enough to not hammer the server" - threshold from #369. */ +/** Cadence for in-flight request polling: fast enough to feel live, slow + enough not to hammer the server. */ const POLL_INTERVAL_MS = 12_000; /** True if any row is mid-ingest (status === 'approved'). The status flips @@ -18,7 +17,7 @@ export function hasInFlightRequest(rows: readonly MusicRequest[] | undefined): b // CreateRequestParams is the shape callers pass; unused MBID fields are // optional, and we omit them from the wire body rather than sending empty // strings (the backend tolerates either, but this keeps test expectations -// crisp and matches the spec §5 wire shape). +// crisp and matches the server's wire shape). export type CreateRequestParams = { kind: RequestKind; artist_mbid: string; diff --git a/web/src/lib/api/suggestions.ts b/web/src/lib/api/suggestions.ts index 2abcd30a..e782e5fe 100644 --- a/web/src/lib/api/suggestions.ts +++ b/web/src/lib/api/suggestions.ts @@ -11,7 +11,7 @@ export function createSuggestionsQuery(limit = 12) { return createQuery({ queryKey: qk.suggestions(limit), queryFn: () => listSuggestions(limit), - staleTime: 5 * 60_000 // 5 minutes — see M5c spec §5 + staleTime: 5 * 60_000 // 5 minutes }); } @@ -20,9 +20,9 @@ export function createSuggestionsQuery(limit = 12) { // out-of-library, so there is no artists row to resolve a display name from // and the snooze list would have nothing to render. Omitting it is a 400. // -// No `days` is sent. There is deliberately no duration UI yet — that knob is -// slice 6 (#2377) — and hardcoding a value here would pin the default to the -// client instead of the server that owns it. +// No `days` is sent: the default is an admin setting on the server (Discover +// tuning), and hardcoding a value here would pin it to the client instead of +// the server that owns it. export async function snoozeSuggestion(mbid: string, name: string): Promise { await api.post(`/api/discover/suggestions/${encodeURIComponent(mbid)}/snooze`, { name }); } diff --git a/web/src/lib/api/tuning.ts b/web/src/lib/api/tuning.ts index c5c01b88..13f0840b 100644 --- a/web/src/lib/api/tuning.ts +++ b/web/src/lib/api/tuning.ts @@ -1,6 +1,6 @@ import { api } from './client'; -// Mirrors internal/api/admin_recommendation_tuning.go (#1250): the +// Mirrors internal/api/admin_recommendation_tuning.go: the // recommendation tuning lab. Field names are the server's snake_case // keys, used verbatim in PATCH bodies. @@ -26,9 +26,9 @@ export type TasteTuning = { mood_scale: number; }; -// Discover request-surface knobs (#2377). Its own scope rather than part of +// Discover request-surface knobs. Its own scope rather than part of // taste: snooze_days belongs here, and a snooze must never be read as taste -// signal (#2374). +// signal. export type DiscoverTuning = { tag_overlap_weight: number; snooze_days: number; @@ -65,7 +65,7 @@ export function resetTuning(scope: TuningScope): Promise { return api.post(`/api/admin/recommendation-tuning/${scope}/reset`, {}); } -// Trends (#1251): weekly per-surface outcome series with knob-turn +// Trends: weekly per-surface outcome series with knob-turn // markers. Mirrors internal/api/admin_recommendation_trends.go. export type TrendPoint = { diff --git a/web/src/lib/api/types.ts b/web/src/lib/api/types.ts index 7e66e9a2..051371a4 100644 --- a/web/src/lib/api/types.ts +++ b/web/src/lib/api/types.ts @@ -41,7 +41,7 @@ export type TrackRef = { disc_number?: number; duration_sec: number; stream_url: string; - // #5418: the file is gone from the library (missing_since is set). Sent on + // The file is gone from the library (missing_since is set). Sent on // direct lookups such as album detail and search; lists that pick tracks // for playback leave missing files out instead. unavailable?: boolean; @@ -49,15 +49,15 @@ export type TrackRef = { export type ArtistDetail = ArtistRef & { albums: AlbumRef[]; - // Genres across this artist's tracks (#367). Server guarantees an array. + // Genres across this artist's tracks. Server guarantees an array. genres: string[]; - // MusicBrainz id, which following is keyed by (M502 #5520); "" when none. + // MusicBrainz id, which following is keyed by; "" when none. mbid: string; }; export type AlbumDetail = AlbumRef & { tracks: TrackRef[]; - // Genres across this album's tracks (#367). Server guarantees an array. + // Genres across this album's tracks. Server guarantees an array. genres: string[]; }; @@ -93,8 +93,8 @@ export type PlaylistTrack = { duration_sec: number; stream_url: string | null; added_at: string; - // True when the track still exists but its file is missing from disk - // (#2527). Distinct from track_id === null: that one is gone for good, + // True when the track still exists but its file is missing from disk. + // Distinct from track_id === null: that one is gone for good, // this one keeps its play history and likes and may come back — the // scanner un-marks it, or adopts it if it returns renamed. Both are // unplayable; only this one is worth telling the user about. @@ -125,9 +125,7 @@ export type EventRequest = // `source` is the system-playlist variant the play came from // (for_you, discover, deep_cuts, …); empty/undefined for library // / user-playlist / radio / Subsonic. Server stores it on - // play_events.source and uses it to advance the rotation. Drift - // #555: the type was missing `source` so callers had no typed slot - // for it and a "clean up extra properties" refactor could drop it. + // play_events.source and uses it to advance the rotation. | { type: 'play_started'; track_id: string; client_id?: string; source?: string } | { type: 'play_ended'; play_event_id: string; duration_played_ms: number } | { type: 'play_skipped'; play_event_id: string; position_ms: number }; @@ -148,7 +146,7 @@ export type RequestStatus = export type RequestKind = 'artist' | 'album' | 'track'; -// One row of the request search (GET /api/catalogue/search, M502 #5519). +// One row of the request search (GET /api/catalogue/search). // An album is a MusicBrainz release group; image_url is Minstrel's own art // endpoint, which answers 404 when no source has an image. export type CatalogueSearchResult = { @@ -189,7 +187,7 @@ export type MusicRequest = { requested_at: string; updated_at: string; // Live ingest counters. Server fills these from the matched entity's - // children. Useful for in-flight requests so the operator can see + // children. Useful for in-flight requests so an admin can see // "5 albums · 47 tracks" while the rest is still being fetched. imported_album_count: number; imported_track_count: number; @@ -276,7 +274,7 @@ export type AdminPlaybackError = { }; // deleted_album is history: an album removed whole, before Minstrel filed its -// own music (M502). +// own music. export type QuarantineAction = 'resolved' | 'deleted_file' | 'deleted_album'; export type QuarantineActionRow = { @@ -311,9 +309,9 @@ export type ArtistSuggestion = { score: number; attribution: SeedContribution[]; // up to 3 entries, ordered by contribution DESC image_url?: string; // absent → card placeholder - // Tags this candidate shares with your taste profile, strongest first (max 3, - // #2377). Absent for most candidates — tag coverage for artists you don't own - // is partial by nature (#2376) — so the card falls back to seed attribution. + // Tags this candidate shares with your taste profile, strongest first (max + // 3). Absent for most candidates — tag coverage for artists you don't own + // is partial by nature — so the card falls back to seed attribution. matched_tags?: string[]; }; @@ -339,7 +337,7 @@ export type HomePayload = { you_might_like_artists: ArtistRef[]; }; -// Missing files (#2527) ----------------------------------------------------- +// Missing files ----------------------------------------------------- // One track whose file the scan could not find. The text fields come from the // tracks row rather than the filesystem — the recording is still a known thing @@ -359,13 +357,13 @@ export type AdminMissingTrack = { last_played_at: string | null; }; -// What the wanted list says about this group's album (#290, M502 #5520): +// What the wanted list says about this group's album: // when it was wanted back, and when the library held it again. null while the // album is inside the grace window, or when MusicBrainz can't name it. export type AdminReacquisitionState = { wanted_at: string | null; held_at: string | null; - // The want itself, as the wanted list sends it (M504), so the Missing page + // The want itself, as the wanted list sends it, so the Missing page // shows its search and download and acts on it. want: Want | null; }; @@ -387,7 +385,7 @@ export type AdminMissingResponse = { groups: AdminMissingGroup[]; }; -// Suspect sources (#5410) -------------------------------------------------- +// Suspect sources -------------------------------------------------- // A present track whose filename carries a video-rip marker. markers are the // reasons, read from the file's basename: "music video", "[Audio]", "MV"... @@ -403,12 +401,12 @@ export type AdminSuspectTrack = { disc_number: number | null; track_number: number | null; markers: string[]; - // 'suspect': held back from radio and the mixes. 'fine': the operator let it - // back. null: the resolver hasn't looked yet (M498 #5439). + // 'suspect': held back from radio and the mixes. 'fine': an admin let it + // back. null: the resolver hasn't looked yet. verdict: SuspectVerdict | null; verdict_at: string | null; // The album is on the wanted list for a clean copy, and when one was last - // searched for (#5524). + // searched for. replacing: boolean; searched_at: string | null; }; @@ -428,10 +426,10 @@ export type AdminSuspectResponse = { groups: AdminSuspectGroup[]; }; -// Duplicates report (#3912) ------------------------------------------------- +// Duplicates report ------------------------------------------------- // One copy in a proposed duplicate group. like_count and play_count cover every -// user: they are what the operator weighs when choosing which copy to keep. +// user: they are what an admin weighs when choosing which copy to keep. export type AdminDuplicateMember = { track_id: string; title: string; @@ -448,11 +446,11 @@ export type AdminDuplicateMember = { play_count: number; disc_number: number | null; track_number: number | null; - // To play each copy from the review (#6168). + // To play each copy from the review. stream_url: string; }; -// The resolver's verdict on a group (M498). null until its first pass. +// The resolver's verdict on a group. null until its first pass. export type DuplicateClass = 'same_release' | 'cross_release' | 'distinct' | 'mismatch' | 'review'; // exact: identical encoded audio. acoustic: the same recording, differently @@ -465,7 +463,7 @@ export type AdminDuplicateGroup = { survivor_track_id: string; survivor_reason: string; class: DuplicateClass | null; - // Why the resolver left the group for the operator, when it had something to say. + // Why the resolver left the group for an admin, when it had something to say. resolve_note: string | null; members: AdminDuplicateMember[]; }; @@ -480,14 +478,14 @@ export type AdminDuplicateSweep = { error_message: string | null; }; -// What a merge did (#3911). removed_paths are the files that were deleted from +// What a merge did. removed_paths are the files that were deleted from // disk. export type MergeDuplicateResult = { survivor_track_id: string; removed_paths: string[]; }; -// What settling one recording in several slots did (#6184). kept_track_id is +// What settling one recording in several slots did. kept_track_id is // null when every copy went; blocked names the releases set aside, empty when // the album came from before Minstrel downloaded anything. export type RefetchDuplicateResult = { @@ -517,7 +515,7 @@ export type AdminDuplicatesResponse = { // The report's group tabs. 'resolved' is the third tab, served separately. export type DuplicatesView = 'review' | 'cross_release'; -// One merge the resolver made on its own (M498), from the audit log. +// One merge the resolver made on its own, from the audit log. // What the resolver did by itself: merged a group, dismissed two versions of a // song as not duplicates, or removed copies that filled another song's track // slot (their tracks stay, marked missing, and the album is fetched again). @@ -542,15 +540,15 @@ export type ResolvedDuplicatesResponse = { items: ResolvedDuplicateActivity[]; }; -// Wanted list (M502 #5520) ------------------------------------------------ +// Wanted list ------------------------------------------------ -// library: in the library when the list began (#5567). +// library: in the library when the list began. export type WantReason = 'request' | 'follow' | 'missing' | 'library'; -// all: one artist's page, every album of theirs on the list (#5572). -// upgrade: held albums wanted for a better copy (#5524). +// all: one artist's page, every album of theirs on the list. +// upgrade: held albums wanted for a better copy. export type WantState = 'wanted' | 'missing' | 'upgrade' | 'held' | 'all'; // Why a held album is wanted again: its copy is below the quality settled -// for, or one of its tracks is a video rip (#5524). +// for, or one of its tracks is a video rip. export type WantUpgrade = 'quality' | 'rip'; // One release group on the wanted list. held_at is set once the library @@ -572,10 +570,10 @@ export type Want = { held_at: string | null; image_url: string; // A held album wanted for a better copy, and what the copy it has is - // ("MP3, about 192 kbps") (#5524). + // ("MP3, about 192 kbps"). upgrade?: WantUpgrade; held_quality?: string; - // The search schedule's last look, and the download running (#5521). + // The search schedule's last look, and the download running. search?: WantSearch | null; grab?: WantGrab | null; }; @@ -591,7 +589,7 @@ export type WantSearch = { error: string; }; -// What an import made of a finished download (M502 #5522). +// What an import made of a finished download. export type ImportState = | '' | 'downloading' @@ -615,13 +613,13 @@ export type WantGrab = { progress: number; import_state: ImportState; import_reason: string; - // Why an ended download ended (#5523). + // Why an ended download ended. end_detail: string; imported_at: string | null; imported_files: number; }; -// Search and grab (M502 #5521) ---------------------------------------------- +// Search and grab ---------------------------------------------- export type CandidateVerdict = 'grab' | 'review' | 'refused' | 'rejected' | 'blocked'; @@ -694,26 +692,26 @@ export type SearchSettings = { last_pass_at: string | null; last_pass: SearchPass; // The library folder new music is filed into ("" = the first of - // scan_paths), and the library's folders (#5522). + // scan_paths), and the library's folders. import_root: string; scan_paths: string[]; - // The download lifecycle (#5523): an imported download seeds to this + // The download lifecycle: an imported download seeds to this // ratio or for this many days (0: removed once imported; no time limit), // and one nobody shares is given up after this many hours (0: never). seed_ratio: number; seed_days: number; stall_hours: number; - // The tier a held album is upgraded to; "" is no quality upgrades (#5524). + // The tier a held album is upgraded to; "" is no quality upgrades. upgrade_until: string; }; export type WantCounts = { - // Artists in the Collection's catalogue (#5572). + // Artists in the Collection's catalogue. artists: number; wanted: number; missing: number; held: number; - // Held albums wanted for a better copy (#5524). + // Held albums wanted for a better copy. upgrades: number; following: number; // Downloads running, those of them waiting on a person, releases set aside. @@ -721,11 +719,11 @@ export type WantCounts = { attention: number; blocked: number; // Library albums no MusicBrainz album matched, waiting to be matched by - // hand or left off (#5579). + // hand or left off. unmatched: number; }; -// What an import's check counted (#5522). +// What an import's check counted. export type ImportEvidence = { files: number; tracks: number; @@ -763,7 +761,7 @@ export type Download = { files: string[]; skipped: number; rehomed_from?: string; - // Why an ended download ended; for an imported one, its seeding (#5523). + // Why an ended download ended; for an imported one, its seeding. end_detail: string; seeding: boolean; ratio: number; @@ -772,7 +770,7 @@ export type Download = { condition: DownloadCondition | null; }; -// An open condition on a download, from the lifecycle's ledger (#5523). +// An open condition on a download, from the lifecycle's ledger. export type DownloadCondition = { trigger: 'no_peers' | 'no_metadata' | 'client_error' | 'client_unreachable' | 'gone' | 'not_removed'; detail: string; @@ -805,10 +803,10 @@ export type ReleaseBlock = { export type ReleaseBlocksPage = { items: ReleaseBlock[]; limit: number; offset: number }; -// The one-time listing of the library's albums as held (#5567): when it +// The one-time listing of the library's albums as held: when it // finished (null while it runs), and how many albums nothing names to // MusicBrainz, so they could not be listed. -// One album in the Collection's Unmatched section (#5579): why no +// One album in the Collection's Unmatched section: why no // MusicBrainz album matched it, or that a person left it off. export type UnmatchedAlbum = { album_id: string; @@ -829,7 +827,7 @@ export type LibraryListing = { listed_at: string | null; unlisted: number }; // artist heads the list when it is narrowed to one artist's page. export type WantsPage = Page & { counts: WantCounts; library: LibraryListing; artist?: WantArtist }; -// One artist in the Collection's catalogue (#5572): how many of their albums +// One artist in the Collection's catalogue: how many of their albums // on the list are held, of how many. export type WantArtist = { artist_mbid: string; @@ -847,7 +845,7 @@ export type WantArtist = { export type WantArtistsPage = Page & { counts: WantCounts; library: LibraryListing }; -// An album's tracks in the Wanted view (#5572): the library's own for an +// An album's tracks in the Wanted view: the library's own for an // album it holds (a lost file marked missing), else MusicBrainz's standard // edition, named by edition; none when MusicBrainz lists no edition. export type WantTrack = { disc: number; number: number; title: string; length_ms: number; missing: boolean }; @@ -888,7 +886,7 @@ export type ReleaseProfilesResponse = { secondary_types: string[]; }; -// What GET /api/admin/summary returns (#5605): each queue that waits on an +// What GET /api/admin/summary returns: each queue that waits on an // admin, with how many and since when the oldest has waited (null when empty). export type AdminQueue = { count: number; since: string | null }; export type AdminSummary = { @@ -900,7 +898,7 @@ export type AdminSummary = { missing: AdminQueue; }; -// What GET /api/admin/status returns (M504): the Overview's status sections. +// What GET /api/admin/status returns: the Overview's status sections. export type AdminListening = { username: string; track_id: string; diff --git a/web/src/lib/api/wanted.ts b/web/src/lib/api/wanted.ts index ee6ca812..a98200e4 100644 --- a/web/src/lib/api/wanted.ts +++ b/web/src/lib/api/wanted.ts @@ -21,12 +21,12 @@ import type { WantsPage } from './types'; -// The wanted list (M502 #5520): release groups Minstrel is to get, the +// The wanted list: release groups Minstrel is to get, the // artists it follows for new releases, and the release-type profiles that // say which kinds of release a follow wants. // q narrows the list to an artist or album whose name holds it; artist to -// one artist's page, which the response then heads (#5572). +// one artist's page, which the response then heads. export async function listWants( state: WantState, offset: number = 0, @@ -44,7 +44,7 @@ export function wantsNextOffset(last: WantsPage): number | undefined { return last.items.length === 0 || loaded >= last.total ? undefined : loaded; } -// Loads as the page scrolls (rule 172), one tab at a time. +// Loads as the page scrolls, one tab at a time. export function createWantsQuery(state: WantState, q: string = '', artist: string = '') { return createInfiniteQuery({ queryKey: qk.adminWants(state, q, artist), @@ -57,7 +57,7 @@ export function createWantsQuery(state: WantState, q: string = '', artist: strin }); } -// The Collection's catalogue (#5572): every artist on the list, A–Z, with +// The Collection's catalogue: every artist on the list, A–Z, with // how many of their albums are held. Its first page carries every section's // count. export async function listWantArtists(offset: number = 0, q: string = ''): Promise { @@ -80,7 +80,7 @@ export function createWantArtistsQuery(q: string = '') { }); } -// An album's tracks (#5572): the library's own for an album it holds, +// An album's tracks: the library's own for an album it holds, // otherwise MusicBrainz's standard edition. export async function getWantTracks(id: string): Promise { return api.get(`/api/admin/wanted/${id}/tracks`); @@ -94,7 +94,7 @@ export function createWantTracksQuery(id: string) { }); } -// The Collection's Unmatched section (#5579): library albums no MusicBrainz +// The Collection's Unmatched section: library albums no MusicBrainz // album matched, and after them the ones a person left off. export async function listUnmatched(): Promise { const out = await api.get<{ items: UnmatchedAlbum[] }>('/api/admin/wanted/unmatched'); @@ -191,7 +191,7 @@ export async function deleteReleaseProfile(id: string): Promise { await api.del(`/api/admin/release-profiles/${id}`); } -// Search and grab (M502 #5521): each want's last search, a person's Search, +// Search and grab: each want's last search, a person's Search, // Grab and "never this one", and the schedule's settings. export async function listCandidates(wantId: string): Promise { @@ -252,7 +252,7 @@ export async function putSearchSettings(body: { return api.put('/api/admin/acquisition/settings', body); } -// Downloads and their imports (M502 #5522) ---------------------------------- +// Downloads and their imports ---------------------------------- // A page holding fewer than it asked for is the last. function nextOffset(last: { items: unknown[]; limit: number; offset: number }): number | undefined { @@ -288,7 +288,7 @@ export async function refuseDownload(id: string): Promise { } // A running download given up: the release is set aside for good, the -// download removed and the album searched for again (#5523). +// download removed and the album searched for again. export async function abandonDownload(id: string): Promise { await api.post(`/api/admin/downloads/${id}/abandon`, {}); } diff --git a/web/src/lib/auth/publicRoutes.test.ts b/web/src/lib/auth/publicRoutes.test.ts index 71e875e3..4a14ad03 100644 --- a/web/src/lib/auth/publicRoutes.test.ts +++ b/web/src/lib/auth/publicRoutes.test.ts @@ -6,10 +6,10 @@ describe('isPublicRoute', () => { expect(isPublicRoute('/login')).toBe(true); }); - // Regression guard for #376: /register was previously bounced to /login - // by the layout guard, breaking bootstrap-admin self-registration on - // fresh deployments. Do NOT remove /register from the public set unless - // you have a deliberate replacement (e.g. invite-only landing page). + // /register must stay public: bounced to /login by the layout guard, the + // bootstrap admin of a fresh deployment could not register. Do NOT remove + // /register from the public set unless you have a deliberate replacement + // (e.g. invite-only landing page). test('/register is public — bootstrap admin must reach the form', () => { expect(isPublicRoute('/register')).toBe(true); }); @@ -31,11 +31,9 @@ describe('isPublicRoute', () => { expect(isPublicRoute('/loginx')).toBe(false); }); - // Regression guard for drift #558: /forgot-password and the - // /reset-password/ deep links must be reachable without a - // session — the reset flow is entered by clicking an email link - // while signed out, so the auth gate redirecting to /login broke - // account recovery. + // /forgot-password and the /reset-password/ deep links must be + // reachable without a session — the reset flow is entered by clicking + // an email link while signed out. test('/forgot-password is public', () => { expect(isPublicRoute('/forgot-password')).toBe(true); }); diff --git a/web/src/lib/auth/publicRoutes.ts b/web/src/lib/auth/publicRoutes.ts index aaecf98f..b3afab4e 100644 --- a/web/src/lib/auth/publicRoutes.ts +++ b/web/src/lib/auth/publicRoutes.ts @@ -1,12 +1,9 @@ // Routes that the +layout.svelte auth guard must NOT redirect away from // when the visitor is unauthenticated. Bootstrap-admin self-registration -// (#376) requires /register to be reachable without a session — without -// /register here, the login page's "Register" link bounces back to /login. -// -// Drift #558: /forgot-password and /reset-password/ were missing -// from this set. The email-link reset flow is by definition entered -// without a session — a signed-out user clicking their reset link was -// being bounced to /login, breaking account recovery. +// requires /register to be reachable without a session — without /register +// here, the login page's "Register" link bounces back to /login. The +// email-link reset flow (/forgot-password, /reset-password/) is by +// definition entered without a session. const PUBLIC_ROUTES = new Set(['/login', '/register', '/forgot-password']); const PUBLIC_PREFIXES = ['/reset-password/']; diff --git a/web/src/lib/auth/store.svelte.ts b/web/src/lib/auth/store.svelte.ts index c84c2f03..777f4129 100644 --- a/web/src/lib/auth/store.svelte.ts +++ b/web/src/lib/auth/store.svelte.ts @@ -9,7 +9,7 @@ import { user, setUser } from './user.svelte'; export { user }; // Weekly client-driven cadence for sending the browser's current IANA -// timezone to PUT /api/me/timezone (#392 Half B). Tracked in +// timezone to PUT /api/me/timezone. Tracked in // localStorage so the cadence survives tab restarts; bumped after each // successful PUT. Failures are swallowed — server keeps its previous // value (or 'UTC' default). diff --git a/web/src/lib/components/AcoustIDCard.svelte b/web/src/lib/components/AcoustIDCard.svelte index 2871725e..88fbbf1d 100644 --- a/web/src/lib/components/AcoustIDCard.svelte +++ b/web/src/lib/components/AcoustIDCard.svelte @@ -13,9 +13,10 @@ import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte'; import SettingRow from '#lib/components/settings/SettingRow.svelte'; - // AcoustID recording-id lookup (M401): fills the MusicBrainz ids that - // untagged tracks lack, so the similarity arms can see them. Off until the - // operator adds their own key (rule 26), and it never gates anything else: + // AcoustID recording-id lookup: fills the MusicBrainz ids that untagged + // tracks lack, so the similarity arms can see them. Off until an admin adds + // their own key — a third-party account is never on by default — and it + // never gates anything else: // when AcoustID is unreachable the card says so and nothing else notices. let overview = $state(null); diff --git a/web/src/lib/components/AcquisitionCard.svelte b/web/src/lib/components/AcquisitionCard.svelte index 631ad512..bb1b4557 100644 --- a/web/src/lib/components/AcquisitionCard.svelte +++ b/web/src/lib/components/AcquisitionCard.svelte @@ -11,10 +11,10 @@ import AcquisitionServiceRow from './AcquisitionServiceRow.svelte'; import SettingsGroup from '#lib/components/settings/SettingsGroup.svelte'; - // Where Minstrel looks for music and what downloads it (M502 #5518), - // Fabled Librarian's model: two lists of connections the operator adds and - // configures. Nothing is seeded or copied. A FabledExchange is - // one entry under download clients, with a switch for each of its jobs. + // Where Minstrel looks for music and what downloads it: two lists of + // connections an admin adds and configures. Nothing is seeded or copied. A + // FabledExchange is one entry under download clients, with a switch for each + // of its jobs. let services = $state(null); let loadError = $state(false); @@ -125,7 +125,7 @@ {:else if services === null}

Loading indexers and download clients…

{:else} - +

{#if !canFetch} No download client is switched on, so nothing can be downloaded. diff --git a/web/src/lib/components/AcquisitionServiceRow.svelte b/web/src/lib/components/AcquisitionServiceRow.svelte index 4c765368..48f7df91 100644 --- a/web/src/lib/components/AcquisitionServiceRow.svelte +++ b/web/src/lib/components/AcquisitionServiceRow.svelte @@ -13,7 +13,7 @@ import { errCode, errMessage } from '#lib/api/errors.js'; import { pushToast } from '#lib/stores/toast.svelte.js'; - // One indexer or download client (M502 #5518). The form is a copy of the + // One indexer or download client. The form is a copy of the // saved row, so Save is only offered when something changed. Secrets are // write-only: the server says whether one is set and never what it is. diff --git a/web/src/lib/components/ActiveSessions.svelte b/web/src/lib/components/ActiveSessions.svelte index 82ee5cd2..1b033f0f 100644 --- a/web/src/lib/components/ActiveSessions.svelte +++ b/web/src/lib/components/ActiveSessions.svelte @@ -120,7 +120,7 @@ } - +

Signed-in devices

diff --git a/web/src/lib/components/CollectionHeader.svelte b/web/src/lib/components/CollectionHeader.svelte index 3c6c20c5..a347bba6 100644 --- a/web/src/lib/components/CollectionHeader.svelte +++ b/web/src/lib/components/CollectionHeader.svelte @@ -1,6 +1,6 @@ @@ -37,7 +37,7 @@ } = $props(); // Art comes from Minstrel's catalogue endpoint, which answers 404 when no - // source has an image (M502 #5519): show the icon then, not a broken image. + // source has an image: show the icon then, not a broken image. // Reset when the card is reused for another result. let imageFailed = $state(false); $effect(() => { @@ -118,7 +118,7 @@ Icon-only to keep Request unambiguously the primary action, with the intent carried by the accessible name. "Not right now" is the whole point of the wording: this parks a suggestion, it does not - record an opinion about the artist (rule #101). + record an opinion about the artist. -->