Render unknown URLs inside the app shell and move ListeningStats to the domain types
CI/CD / BackEnd (push) Successful in 1m19s
CI/CD / WebPlayer (push) Successful in 46s
CI/CD / Deploy (push) Skipped

Add catch-all routes so unknown pages 404 inside the (app) layout and unknown
/api URLs return a JSON 404 instead of redirecting to the login page. Restore
the full root error page.
This commit is contained in:
2026-09-24 14:29:45 +02:00
parent 2d23eb87c6
commit 8e85af2b3b
6 changed files with 28 additions and 12 deletions
+2 -2
View File
@@ -33,7 +33,7 @@ npm run lint:tokens # scripts/check-tokens.mjs, the design system guardrail
## Folder structure ## Folder structure
- `src/lib/types.ts` holds the domain types (`Track`, `Album`, `Playlist`, `Mix`, `Paged<T>`). Pages and the player only ever see these, never raw API DTOs. - `src/lib/types.ts` holds the domain types (`Track`, `Album`, `Playlist`, `Mix`, `ListeningStats`, `Paged<T>`). Pages and the player only ever see these, never raw API DTOs.
- `src/lib/player/` is the player: `player.svelte.ts` (state and orchestration), `queue.ts` (pure queue operations), `stream.ts`, `mediaSession.ts`, `progressClock.ts`, `listenTracker.ts`, `recentlyPlayed.ts` (merges `player.recentlyPlayed` with the server history), and `actions.ts` (`playAllOrToggle`, `playShuffled`, `isQueueCurrent`). - `src/lib/player/` is the player: `player.svelte.ts` (state and orchestration), `queue.ts` (pure queue operations), `stream.ts`, `mediaSession.ts`, `progressClock.ts`, `listenTracker.ts`, `recentlyPlayed.ts` (merges `player.recentlyPlayed` with the server history), and `actions.ts` (`playAllOrToggle`, `playShuffled`, `isQueueCurrent`).
- `src/lib/state/` holds reactive rune state: `panels.svelte.ts` (queue panel, create playlist modal), `history.svelte.ts` (previous page for `BackLink`), `menu.svelte.ts` (`createMenu<T>()`), `pagedList.svelte.ts` (`createPagedList`), `dialog.svelte.ts`, `liked.svelte.ts` (the user's liked tracks), `toggle.svelte.ts` (`createToggle`), and `scroll.ts` (`restoreScroll`). Nothing in `state/` imports from `components/`. - `src/lib/state/` holds reactive rune state: `panels.svelte.ts` (queue panel, create playlist modal), `history.svelte.ts` (previous page for `BackLink`), `menu.svelte.ts` (`createMenu<T>()`), `pagedList.svelte.ts` (`createPagedList`), `dialog.svelte.ts`, `liked.svelte.ts` (the user's liked tracks), `toggle.svelte.ts` (`createToggle`), and `scroll.ts` (`restoreScroll`). Nothing in `state/` imports from `components/`.
- `src/lib/theme/` holds the theme: `accent.svelte.ts` (accent hue per track and the hue fade), `mode.svelte.ts`, `palette.ts`, `tokens.ts`, and `color.ts` (`ACCENT_HUE`). The player never imports from `theme/`: `(app)/+layout.svelte` follows `player.currentId` and calls `accent.follow`. - `src/lib/theme/` holds the theme: `accent.svelte.ts` (accent hue per track and the hue fade), `mode.svelte.ts`, `palette.ts`, `tokens.ts`, and `color.ts` (`ACCENT_HUE`). The player never imports from `theme/`: `(app)/+layout.svelte` follows `player.currentId` and calls `accent.follow`.
@@ -51,7 +51,7 @@ npm run lint:tokens # scripts/check-tokens.mjs, the design system guardrail
- `media/` for stateless music display components that receive props (Artwork, MediaCard, ListRow, PlayButton, and so on). - `media/` for stateless music display components that receive props (Artwork, MediaCard, ListRow, PlayButton, and so on).
- `forms/` for composite entity forms (AlbumForm, PlaylistForm, CoverForm, ImageDropzone). - `forms/` for composite entity forms (AlbumForm, PlaylistForm, CoverForm, ImageDropzone).
- `layout/` for page level scaffolding (Page, PageHeader, SectionHeading). - `layout/` for page level scaffolding (Page, PageHeader, SectionHeading).
- The root `+layout.svelte` only holds the globals (fonts, theme, `DialogHost`). Everything the user sees after signing in lives under `src/routes/(app)/`, including Home (`(app)/+page.svelte`); `(app)/+layout.server.ts` guards the group and loads the shell data, and `(app)/+error.svelte` renders inside the shell. Auth routes live under `src/routes/auth/`: `+page.svelte` is the login screen, and `login/`, `logout/`, `callback/` are the OIDC endpoints. The public user profile route is `src/routes/(app)/user/[id]/`, and its followers/following lists share one route, `user/[id]/[list=followList]/` (matcher in `src/params/followList.ts`). - The root `+layout.svelte` only holds the globals (fonts, theme, `DialogHost`). Everything the user sees after signing in lives under `src/routes/(app)/`, including Home (`(app)/+page.svelte`); `(app)/+layout.server.ts` guards the group and loads the shell data, and `(app)/+error.svelte` renders inside the shell. Unknown URLs hit the catch-all `(app)/[...rest]/` (it throws a 404) so they also get the shell (unknown `/api/...` URLs hit `api/[...rest]/`, a JSON 404); the root `+error.svelte` only covers errors outside the group or in the group layout load. Auth routes live under `src/routes/auth/`: `+page.svelte` is the login screen, and `login/`, `logout/`, `callback/` are the OIDC endpoints. The public user profile route is `src/routes/(app)/user/[id]/`, and its followers/following lists share one route, `user/[id]/[list=followList]/` (matcher in `src/params/followList.ts`).
## Naming conventions and glossary ## Naming conventions and glossary
+1 -7
View File
@@ -1,5 +1,5 @@
import type { components } from '$lib/api/schema'; import type { components } from '$lib/api/schema';
import type { Album, LikedTrack, Mix, Paged, Playlist, Track } from '$lib/types'; import type { Album, LikedTrack, ListeningStats, Mix, Paged, Playlist, Track } from '$lib/types';
type TrackDto = components['schemas']['TrackApplicationResponse']; type TrackDto = components['schemas']['TrackApplicationResponse'];
type AlbumDto = components['schemas']['AlbumApplicationResponse']; type AlbumDto = components['schemas']['AlbumApplicationResponse'];
@@ -84,12 +84,6 @@ export function toPlaylist(dto: PlaylistDto): Playlist {
type ListeningStatsDto = components['schemas']['ListeningStatsResponse']; type ListeningStatsDto = components['schemas']['ListeningStatsResponse'];
export interface ListeningStats {
tracksThisWeek: number;
secondsThisWeek: number;
streakDays: number;
}
export function toCount(value: number | string | null | undefined): number { export function toCount(value: number | string | null | undefined): number {
return Number(value ?? 0) || 0; return Number(value ?? 0) || 0;
} }
+6
View File
@@ -51,6 +51,12 @@ export interface Mix {
tracks: Track[]; tracks: Track[];
} }
export interface ListeningStats {
tracksThisWeek: number;
secondsThisWeek: number;
streakDays: number;
}
export interface Paged<T> { export interface Paged<T> {
items: T[]; items: T[];
pageNumber: number; pageNumber: number;
@@ -0,0 +1,6 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = () => {
error(404, 'Página no encontrada');
};
+9 -3
View File
@@ -1,12 +1,18 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state';
import Button from '$lib/components/ui/primitives/Button.svelte';
</script> </script>
<div class="min-h-screen bg-bg text-fg antialiased"> <div class="min-h-screen bg-bg px-6 text-fg antialiased">
<div class="mx-auto my-[14vh] max-w-prose-sm text-center"> <div class="animate-enter mx-auto my-[14vh] max-w-prose-sm text-center">
<h1 class="mb-2.5 text-display-3"> <h1 class="mb-2.5 text-display-3">
{page.status === 404 ? 'Página no encontrada' : 'Algo salió mal'} {page.status === 404 ? 'Página no encontrada' : 'Algo salió mal'}
</h1> </h1>
<a href="/" class="text-body text-fg-2 underline">Volver al inicio</a> <p class="mb-6.5 text-body text-fg-2">
{page.status === 404
? 'Comprueba el enlace o vuelve al inicio.'
: 'No se pudo cargar la página. Inténtalo de nuevo en unos momentos.'}
</p>
<Button href="/" variant="secondary">Volver al inicio</Button>
</div> </div>
</div> </div>
@@ -0,0 +1,4 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const fallback: RequestHandler = () => json({ message: 'No encontrado.' }, { status: 404 });