feat(web): storage admin, search and catalog pages (M2)
ci / ts / ts (packages + web) (push) Successful in 1m56s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m59s

- Storage admin: local folders with a folder picker and NFS sources,
  connection test, scan and full scan with live progress, enable,
  disable, delete
- path pattern editor with reorder, examples and a preview table that
  shows which pattern matched and what came from tags; compile errors
  land under the broken pattern
- Library page with counters, scans and unreadable files; search
  reindex for admins
- search box in the top bar: combobox with keyboard navigation, 150 ms
  debounce, cancelled stale requests, "/" and Ctrl+K, layout fix hint
- search page with tabs and paging, artist and album pages, album
  cards and track lists; unavailable tracks are dimmed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 07:01:52 +03:00
1 parent c1160cccd3
commit a685b1b7ca
27 files changed
+2505 -9

No files matched your search

+29
View File
@@ -17,6 +17,17 @@ const problem = (status: number, code: string, extra: object = {}) => json({ sta
let seq = 0;
const kino = { id: '00000000-0000-4000-8000-0000000000aa', name: 'Кино' };
const albumSummary = {
id: '00000000-0000-4000-8000-0000000000bb',
title: 'Группа крови',
year: 1988,
cover_url: null,
cover_colors: [],
artist: kino,
track_count: 11,
};
export function makeUser(username: string, role: Role, extra: Partial<FakeUser> = {}): FakeUser {
seq++;
return {
@@ -146,6 +157,24 @@ export function fakeApi(initial: FakeUser[]) {
}
case 'GET /api/v1/admin/audit':
return json({ items: [], next_cursor: null });
case 'GET /api/v1/search/suggest':
case 'GET /api/v1/search': {
// «rbyj» — «кино» в английской раскладке: сервер находит и сообщает исправление.
const q = url.searchParams.get('q') ?? '';
const hit = q === 'кино' || q === 'rbyj';
return json({
artists: hit ? [kino] : [],
albums: hit ? [albumSummary] : [],
tracks: [],
totals: { artists: hit ? 1 : 0, albums: hit ? 1 : 0, tracks: 0 },
corrected_query: q === 'rbyj' ? 'кино' : null,
next_cursor: null,
});
}
case `GET /api/v1/artists/${kino.id}`:
return json({ ...kino, album_count: 1, track_count: 0 });
case `GET /api/v1/artists/${kino.id}/albums`:
return json({ items: [albumSummary] });
}
return problem(404, 'common.not_found');
};