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

10 KiB
Raw Blame History

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:

nvm use          # resolves lts/*
npm install

Running

The API must be up — it serves on http://localhost:5000 in development:

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:

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:

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
  • /usersAdmin 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. EventsGET /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 resultsPOST /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.

  • SavePOST /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.
  • DeleteDELETE /api/Game/{id}, behind a confirm.
  • Find on start.ggGET /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:

# 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

npm run check    # svelte-check (types + template diagnostics)
npm run build    # static build into ./build