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

+83 -4
View File
@@ -9,9 +9,15 @@ import {
} from '@tanstack/react-router';
import { AuditPage } from '../pages/admin/AuditPage';
import { LibraryPage } from '../pages/admin/LibraryPage';
import { PatternsPage } from '../pages/admin/PatternsPage';
import { SourcesPage } from '../pages/admin/SourcesPage';
import { UsersPage } from '../pages/admin/UsersPage';
import { AlbumPage } from '../pages/AlbumPage';
import { ArtistPage } from '../pages/ArtistPage';
import { HomePage } from '../pages/HomePage';
import { LoginPage } from '../pages/LoginPage';
import { SearchPage, type SearchType } from '../pages/SearchPage';
import { SettingsPage } from '../pages/SettingsPage';
import { SetupPage } from '../pages/SetupPage';
import { AppShell } from '../widgets/AppShell';
@@ -96,6 +102,10 @@ const appRoute = createRoute({
component: AppShell,
});
const adminOnly = ({ context }: { context: { user: AuthUser } }) => {
if (context.user.role !== 'admin') throw redirect({ to: '/' });
};
const homeRoute = createRoute({ getParentRoute: () => appRoute, path: '/', component: HomePage });
const settingsRoute = createRoute({ getParentRoute: () => appRoute, path: '/settings', component: SettingsPage });
@@ -112,17 +122,86 @@ const usersRoute = createRoute({
const auditRoute = createRoute({
getParentRoute: () => appRoute,
path: '/admin/audit',
beforeLoad: ({ context }) => {
if (context.user.role !== 'admin') throw redirect({ to: '/' });
},
beforeLoad: adminOnly,
component: AuditPage,
});
const searchRoute = createRoute({
getParentRoute: () => appRoute,
path: '/search',
validateSearch: (search: Record<string, unknown>): { q: string; type?: Exclude<SearchType, 'all'> } => {
const type = search.type;
return {
q: typeof search.q === 'string' ? search.q : '',
...(type === 'artists' || type === 'albums' || type === 'tracks' ? { type } : {}),
};
},
component: function SearchRoute() {
const { q, type } = searchRoute.useSearch();
return <SearchPage q={q} type={type ?? 'all'} />;
},
});
const artistRoute = createRoute({
getParentRoute: () => appRoute,
path: '/artist/$id',
component: function ArtistRoute() {
const { id } = artistRoute.useParams();
return <ArtistPage key={id} id={id} />;
},
});
const albumRoute = createRoute({
getParentRoute: () => appRoute,
path: '/album/$id',
component: function AlbumRoute() {
const { id } = albumRoute.useParams();
return <AlbumPage key={id} id={id} />;
},
});
const sourcesRoute = createRoute({
getParentRoute: () => appRoute,
path: '/admin/sources',
beforeLoad: adminOnly,
component: SourcesPage,
});
const patternsRoute = createRoute({
getParentRoute: () => appRoute,
path: '/admin/sources/$id/patterns',
beforeLoad: adminOnly,
component: function PatternsRoute() {
const { id } = patternsRoute.useParams();
return <PatternsPage id={id} />;
},
});
const libraryRoute = createRoute({
getParentRoute: () => appRoute,
path: '/admin/library',
beforeLoad: ({ context }) => {
if (context.user.role === 'user') throw redirect({ to: '/' });
},
component: LibraryPage,
});
const routeTree = rootRoute.addChildren([
loginRoute,
setupAdminRoute,
setupPasswordRoute,
appRoute.addChildren([homeRoute, settingsRoute, usersRoute, auditRoute]),
appRoute.addChildren([
homeRoute,
settingsRoute,
searchRoute,
artistRoute,
albumRoute,
usersRoute,
auditRoute,
sourcesRoute,
patternsRoute,
libraryRoute,
]),
]);
export function createAppRouter(session: AuthSession, opts: { history?: Parameters<typeof createRouter>[0]['history'] } = {}) {