feat(storage): wire Storage Maintenance screen (A6)

Duplicates / failed / missing-metadata tabs (paginated, reusing TrackRow) plus
admin orphan-file cleanup. Adds getDuplicates/getBrokenFiles/getMissingMetadata/
runCleanup endpoints and a DuplicateGroup type; drops the dead scanStorage/
deleteTrackFile stubs that had no backend route.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Цвылев Александр Вадимович
2026-07-28 20:42:05 +03:00
parent 28d9e59e34
commit 3eba3701d0
5 changed files with 345 additions and 17 deletions
+54 -14
View File
@@ -1,10 +1,32 @@
import { api } from '../index';
import { toStorageStats, type RawStorageStats } from '../mappers';
import type { StorageStats } from '../types';
import {
toStorageStats,
toPage,
toTrack,
type RawStorageStats,
type RawPaged,
type RawTrack,
} from '../mappers';
import type {
StorageStats,
DuplicateGroup,
PaginatedResponse,
Track,
} from '../types';
// `GET /storage` returns library + disk statistics (§A6). The maintenance
// routes (`/storage/duplicates`, `/storage/broken`, `/storage/missing-metadata`,
// `POST /storage/cleanup`) are still backend stubs and unused by the UI.
// routes back the Storage Maintenance screen: `/duplicates`, `/broken`,
// `/missing-metadata` (read) and `POST /cleanup` (admin, enqueues the worker).
interface RawDuplicateGroup {
fingerprint: string;
tracks: RawTrack[];
}
interface Paging {
limit?: number;
offset?: number;
}
export const storageApi = api.injectEndpoints({
endpoints: (build) => ({
@@ -13,16 +35,32 @@ export const storageApi = api.injectEndpoints({
transformResponse: (raw: RawStorageStats) => toStorageStats(raw),
providesTags: ['Storage'],
}),
scanStorage: build.mutation<{ jobId: string }, void>({
query: () => ({ url: '/storage/scan', method: 'POST' }),
invalidatesTags: ['Storage', 'Track', 'Album', 'Artist'],
getDuplicates: build.query<DuplicateGroup[], void>({
query: () => '/storage/duplicates',
transformResponse: (raw: RawDuplicateGroup[]) =>
raw.map((g) => ({
fingerprint: g.fingerprint,
tracks: g.tracks.map(toTrack),
})),
providesTags: ['Storage', 'Track'],
}),
deleteTrackFile: build.mutation<void, string>({
query: (trackId) => ({
url: `/storage/tracks/${trackId}`,
method: 'DELETE',
getBrokenFiles: build.query<PaginatedResponse<Track>, Paging | void>({
query: (p) => ({ url: '/storage/broken', params: p ?? {} }),
transformResponse: (raw: RawPaged<RawTrack>) => toPage(raw, toTrack),
providesTags: ['Storage', 'Track'],
}),
getMissingMetadata: build.query<PaginatedResponse<Track>, Paging | void>({
query: (p) => ({ url: '/storage/missing-metadata', params: p ?? {} }),
transformResponse: (raw: RawPaged<RawTrack>) => toPage(raw, toTrack),
providesTags: ['Storage', 'Track'],
}),
runCleanup: build.mutation<{ status: string; jobId: string }, void>({
query: () => ({ url: '/storage/cleanup', method: 'POST' }),
transformResponse: (raw: { status: string; job_id: string }) => ({
status: raw.status,
jobId: raw.job_id,
}),
invalidatesTags: ['Storage', { type: 'Track', id: undefined }],
invalidatesTags: ['Storage', 'Track'],
}),
}),
overrideExisting: false,
@@ -30,6 +68,8 @@ export const storageApi = api.injectEndpoints({
export const {
useGetStorageStatsQuery,
useScanStorageMutation,
useDeleteTrackFileMutation,
useGetDuplicatesQuery,
useGetBrokenFilesQuery,
useGetMissingMetadataQuery,
useRunCleanupMutation,
} = storageApi;
+6
View File
@@ -176,6 +176,12 @@ export interface StorageStats {
disk?: StorageDiskUsage;
}
/** A set of tracks sharing one acoustic fingerprint — de-dup candidates (§A6). */
export interface DuplicateGroup {
fingerprint: string;
tracks: Track[];
}
export interface User {
id: string;
username: string;