Files
LaDOSE/LaDOSE.Src/LaDOSE.WebApp/README.md
T
2026-08-06 09:58:06 +02:00

203 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LaDOSE.WebApp
Svelte 5 + SvelteKit front-end for `LaDOSE.Api`, styled with Tailwind CSS v4.
It ships as a static SPA (`@sveltejs/adapter-static`) and talks to the API over
JWT bearer auth, so it can be served from any static host.
## Requirements
Node ≥ 22.12 (the toolchain uses Vite 8). An `.nvmrc` is checked in:
```bash
nvm use # resolves lts/*
npm install
```
## Running
The API must be up — it serves on `http://localhost:5000` in development:
```bash
cd ../LaDOSE.Api && dotnet run # terminal 1
npm run dev # terminal 2 -> http://localhost:5173
```
Point the app at a different API with `VITE_API_BASE_URL` (see `.env.example`).
`LaDOSE.Api` already allows any origin with credentials, so no dev proxy is needed.
## Typed API access
`src/lib/api/schema.d.ts` is **generated** from the API's OpenAPI document — never
edit it by hand. Regenerate whenever a C# controller or DTO changes:
```bash
npm run api:sync # fetch openapi.json from the running API, then re-emit types
```
That is `api:fetch` (curl `/openapi/v1.json`, override the host with `LADOSE_API_URL`)
followed by `api:types` (`openapi-typescript`). `openapi.json` is committed so the
types can be rebuilt without a running API.
Because paths and DTOs come from the generated types, a renamed route or a changed
DTO field surfaces as a TypeScript error rather than a runtime 404.
| Module | Purpose |
| --- | --- |
| `src/lib/api/schema.d.ts` | Generated types — all 23 API paths and every DTO |
| `src/lib/api/schema-helpers.ts` | Friendly aliases (`ApplicationUserDTO`, `LoginRequest`, …) |
| `src/lib/api/client.ts` | `apiRequest` — bearer auth, JSON, `ApiError`; paths constrained to real routes |
| `src/lib/api/users.ts` | `login` / `register` against `/Users/auth` and `/Users/register` |
| `src/lib/api/errors.ts` | `toErrorMessage` — message to show, or redirect to `/login` on a 401 |
| `src/lib/api/tournaments.ts` | `listEvents` / `importSmashTournament` / `getResults`, authenticated from the session |
| `src/lib/api/games.ts` | `listGames` / `saveGame` / `deleteGame` / `searchSmashGames` |
| `src/lib/api/admin-users.ts` | `listUsers` / `listRoles` / `addUser` / `deleteUser` — all Admin-only |
| `src/lib/api/statistics.ts` | `getMatchStats` against `POST /api/Statistics/Matches` — set-level win/loss and head to head |
| `src/lib/tournaments/results.ts` | Pure reshaping of `TournamentsResultDTO`: ranking grid, per-game placements, WordPress HTML, CSV |
| `src/lib/statistics/aggregate.ts` | Pure aggregation for `/statistiques`: standings, per-game and per-event summaries, CSV |
| `src/lib/statistics/load.ts` | `loadEventResults` — per-event `GetResults` fan-out with progress, partial failure and abort |
| `src/lib/games/draft.ts` | `GameDTO` ⇄ editor form, including the blank-to-NULL rules |
| `src/lib/ui/classes.ts` | The Tailwind class strings shared by the pages |
| `src/lib/stores/session.svelte.ts` | Signed-in user, persisted to `localStorage`, drops expired JWTs |
Calling another endpoint takes one line, and the path is checked at compile time:
```ts
import { apiRequest, buildPath, session } from '$lib';
import type { GameDTO } from '$lib';
const games = await apiRequest<GameDTO[]>('/api/Game', { token: session.token });
const one = await apiRequest<GameDTO>(buildPath('/api/Game/{id}', { id: 3 }));
```
## Routes
- `/login` — username + password, posts to `POST /Users/auth`, stores the returned JWT
- `/` — guarded; greets the signed-in user with **Hello, \<name\>.** and offers sign-out
- `/tournaments` — guarded; the start.gg half of the old Avalonia `TournamentResultView`
- `/statistiques` — guarded; standings, attendance and match statistics over a chosen scope
- `/games` — guarded; the game catalogue editor, from the Avalonia `GamesView`
- `/users`**Admin only**; add and remove accounts
`session.displayName` prefers `firstName lastName` and falls back to `username`.
### `/tournaments`
Ports the Smash.gg (start.gg) column of `LaDOSE.DesktopApp.Avalonia`:
1. **Import** — a slug (`start.gg/tournament/<slug>`) is sent to
`GET /api/Tournament/ParseSmash/{slug}`, which pulls the brackets, placements and
sets into the database. Every bracket must be `COMPLETED` or the API throws.
2. **Events**`GET /api/Event`, newest first. Tick one event for a single
tournament, or several to aggregate a ranking season. The regex box replaces the
selection with every matching event name (e.g. `Ranking #13\d{2}`).
3. **Generate results**`POST /api/Tournament/GetResults` with the selected ids;
the API applies the point rules in `ExternalProviderService`. Three views:
- *Ranking* — players × games with totals, highest first, plus CSV export
- *By game* — placements and points for one game
- *HTML* — the podium table for the WordPress recap, with copy-to-clipboard
The Challonge half of the Avalonia view (date range, Challonge tournament list,
`ParseChallonge`) is deliberately not ported.
### `/statistiques`
Pick a scope — everything, the last 6/12 events, or a regex over event names — then
**Compute statistics**. Two independent sources feed the page, and they are kept apart
on purpose:
- **Points, placements, attendance** come from `POST /api/Tournament/GetResults`, called
**one event at a time**. The endpoint merges everything it is given and never says
which event a row came from, so per-event calls are the only way to get a time series
— and they contain the damage, because it throws a 500 on any bracket missing a rank-1
or rank-2 row. One broken import is reported as a skipped event instead of taking the
whole scope down with it.
- **Set-level win rates and head to head** come from `POST /api/Statistics/Matches`, in
a single call for the whole scope.
The **Matches** tab leads with its coverage line, and it matters: brackets imported
before set rows were persisted contribute placements but no matches, so those figures
can describe a fraction of the scope while the standings above cover all of it. Win
rates count **decided sets only**, so a player with no resolvable set is left out rather
than shown at 0%.
Other things worth knowing:
- Rank `999` is the service's "unplaced" sentinel (the participation bucket in
`ExternalProviderService`), so it never counts as a placement or a podium, and
**Best** shows `—`.
- Player names are merged case-insensitively, as everywhere else in the app.
- Brackets are counted per event, so a bracket name reused every month counts once
per event rather than once overall.
- Undated events sort last in the chart and the Events tab — `GET /api/Event` returns
newest first, which is the least misleading place to put them.
- `aggregate.ts` is pure, so it can be exercised under plain Node with fixtures, the
same way `src/lib/tournaments/results.ts` is.
### `/games`
Ports `GamesView`: the list on the left (ordered by `Order`), an editor on the right.
- **Save** — `POST /api/Game`. `AddOrUpdate` inserts when `id` is 0 and otherwise
**replaces every column**, so the form always sends a complete `GameDTO`; blank
text fields are sent as `null`.
- **New game** — starts an empty draft with `id` 0 and the next free `Order`. The
desktop app instead posted a blank row immediately and let you fill it in after.
- **Delete** — `DELETE /api/Game/{id}`, behind a confirm.
- **Find on start.gg** — `GET /api/Game/smash/{name}` searches start.gg's videogame
catalogue using the long name (falling back to the name); picking a match fills
`smashId`. The ids listed are **start.gg** videogame ids, not LaDOSE game ids.
`smashId` is what bracket imports match on: a game without one collects its results
under a synthetic "GAME NOT FOUND" entry.
Unlike the desktop form, `imgUrl` is editable here — it is part of `GameDTO` and was
otherwise only reachable through the database.
### `/users` (Admin only)
Lists every account with its roles, creates accounts, and deletes them.
`POST /Users/register` used to be `[AllowAnonymous]` so that the first account could be
created. It is now `POST /Users/AddUser` and requires the **Admin** role, so before this
page is reachable at all, one account has to be promoted directly in the database:
```bash
# edit the username in the file first, then:
psql "$LADOSE_DB" -v ON_ERROR_STOP=1 -f ../../Sql/2026-08-05_roles.sql
```
How roles work:
- They live in the pre-existing `applicationrole` / `applicationuserrole` tables. The
script seeds `Admin` and `User`; no schema change was needed.
- Only user management checks a role. **Every other endpoint is unchanged** — a plain
or role-less account can still use tournaments, games and the rest.
- The JWT carries only the user id. Roles are read from the database on every request
(`OnTokenValidated` in `Startup.cs`), so granting or revoking Admin applies to the
caller's next request instead of whenever their 16-minute token expires.
- `session.isAdmin` hides the link and the page, but that is cosmetic — the API is
what enforces access, and a non-admin calling these endpoints gets a 403.
- The API refuses to delete the caller's own account. Since only an admin can reach the
endpoint, that is what guarantees at least one admin always remains.
## Notes
- The API issues 16-minute tokens. A lapsed token is treated as signed out on load;
there is no refresh flow yet, so long sessions will need a re-login. The guarded
pages turn a 401 into a redirect back to `/login` (see `toErrorMessage`).
- The API has no exception middleware, so an unhandled server error arrives as an
HTML developer page. `ApiError` then carries only the status, which is why each
call site supplies its own fallback message.
- `GetResults` only fills `slug` when **one** event id is requested, so the
"Voir le Bracket" links appear only for a single-event export.
- Player names are merged case-insensitively across brackets, matching the desktop app.
- `src/routes/+layout.ts` sets `ssr = false`: the JWT lives in the browser, so there
is nothing meaningful to render on the server.
## Checks
```bash
npm run check # svelte-check (types + template diagnostics)
npm run build # static build into ./build
```