Commit Graph
19 Commits
Author SHA1 Message Date
enricobuehlerandClaude Fable 5 7c0313a7cb fix(web): flush card content, so a full-bleed table stops doubling its inset
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 58s
apple / swift (push) Successful in 1m18s
ci / bench (push) Successful in 6m42s
apple / screenshots (push) Successful in 6m21s
ci / rust (push) Successful in 24m18s
android-screenshots / screenshots (push) Successful in 3m2s
decky / build-publish (push) Successful in 21s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
android / android (push) Successful in 15m7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 37s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
windows-host / package (push) Successful in 10m7s
release / apple (push) Successful in 10m12s
flatpak / build-publish (push) Successful in 6m55s
deb / build-publish (push) Successful in 11m44s
linux-client-screenshots / screenshots (push) Successful in 7m40s
docker / deploy-docs (push) Successful in 28s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m22s
web-screenshots / screenshots (push) Successful in 3m27s
deb / build-publish-host (push) Successful in 12m25s
arch / build-publish (push) Successful in 15m42s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m32s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 15m48s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m41s
`<CardContent className="p-0">` never removed the padding it looked like it
removed: tailwind-merge resolves conflicts within a variant, so `p-0` cancels
`p-4` but leaves `sm:p-6` standing. Every call site that used it nests a
CardHeader inside, which brings its own `sm:p-6` — so at >=640px the title sat
24px further in than its neighbours'. Visible on the Plugins page as 'Installed
plugins' not lining up with 'Plugin runner'.

Give CardContent a `flush` prop that omits the padding outright, and use it at
the three call sites (Store, Pairing, Stats) rather than fixing the symptom
three times and leaving the footgun in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 00:25:31 +02:00
enricobuehlerandClaude Fable 5 833f3348a0 feat(store): console plugin store, index repo, and the fixes on-glass found
apple / swift (push) Successful in 1m22s
release / apple (push) Successful in 9m59s
windows-host / package (push) Successful in 9m52s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m20s
apple / screenshots (push) Successful in 6m28s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m21s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m39s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m45s
audit / cargo-audit (push) Successful in 2m58s
audit / bun-audit (push) Successful in 13s
ci / web (push) Successful in 52s
ci / docs-site (push) Successful in 59s
android / android (push) Has been cancelled
arch / build-publish (push) Has been cancelled
ci / bench (push) Has been cancelled
ci / rust (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
decky / build-publish (push) Successful in 23s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 10s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 35s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 49s
flatpak / build-publish (push) Successful in 7m53s
docker / deploy-docs (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
deb / build-publish (push) Successful in 11m41s
deb / build-publish-host (push) Successful in 13m32s
Console: a Plugins section (Browse / Installed / Sources) on a static nav
entry, with install friction proportional to trust — a plain confirm for a
verified entry, a warning naming the curator for an external one, and a
danger dialog that makes you retype the spec for a raw package. Tier badges
are permanent and follow the plugin onto its own UI page.

Index: unom/punktfunk-plugin-index published and served from Gitea's raw
endpoint (real HTTPS, byte-exact, no vhost to stand up) — merge to main is
publish, which resolves the design's open hosting question.

Four things only running it could find:
- runner discovery matched @punktfunk/plugin-* only, so a third-party
  scoped plugin (which D8 requires) would install and never run
- ...and that convention also matches @punktfunk/plugin-kit, a plugin's
  own framework: it listed as installed and would have been imported as a
  unit. Both now key off the plugins dir's top-level dependencies, with an
  emptied dependency list meaning 'nothing installed' rather than falling
  back to the naming convention
- the store must not pass new flags to the runner: the scripting package
  ships separately and an older one reads an unknown flag's value as a
  package name. The host writes the bunfig scope mapping itself
- ureq reports only >= 400 as Err, so a conditional request's 304 arrives
  as Ok with an empty body — handled as an error it made every refresh
  after the first verify a signature over zero bytes and sit stale

Also: the console's first Tabs use exposed an @unom/ui theme gap that
rendered inactive tabs invisible (caught in a browser pass, not by types).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 20:48:02 +02:00
enricobuehlerandClaude Opus 4.8 770994b7aa fix(web): tighten mobile padding so content isn't too narrow
apple / swift (push) Successful in 1m15s
apple / screenshots (push) Successful in 6m8s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m3s
ci / bench (push) Successful in 5m28s
arch / build-publish (push) Successful in 11m41s
decky / build-publish (push) Successful in 23s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 36s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m8s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m33s
android / android (push) Successful in 16m39s
windows-host / package (push) Successful in 16m15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 16s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 14s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 10s
docker / deploy-docs (push) Successful in 24s
deb / build-publish (push) Successful in 8m59s
ci / rust (push) Successful in 19m26s
deb / build-publish-host (push) Successful in 10m5s
The main content wrapper used p-6 (24px) on mobile while the mobile top bar uses
px-4 (16px) — an inconsistent, oversized side gutter that ate into the usable
width on phones. Drop it to a 16px side gutter (px-4) on mobile, matching the top
bar; sm+ padding is unchanged. Also make the shared Card padding responsive
(p-4 on mobile → p-6 from sm up) so card content isn't double-inset on small
screens. Desktop layout is identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 13:50:14 +02:00
enricobuehlerandClaude Opus 4.8 ec84b30eae feat(plugins): console-hosted plugin UI surface (host registry + SDK servePluginUi + console proxy/nav)
Implements planning/design/plugin-ui-surface.md (U1-U3):

- host: in-memory lease-based plugin registry (mgmt/plugins.rs) — PUT/GET/DELETE
  /api/v1/plugins + GET /plugins/{id}/ui-credential; bearer+loopback only (not on
  the mTLS read-only allowlist); plugins.changed event; port-only registration
  (proxy always dials 127.0.0.1); secret never in the listing.
- sdk: servePluginUi — loopback ephemeral bind + per-boot secret + constant-time
  check + /__health + static/SPA-fallback + register/renew(30s)/deregister via
  pf.request (skew-proof, D7). Example + tests.
- console: /plugin-ui/{id}/** reverse proxy (server-side secret injection, cookie
  strip, SSE streaming, stale-secret 401-retry) + credential cache; BFF denylist
  for the credential endpoint; dynamic Plugins nav (desktop + mobile) fed by a
  polled list; iframe-in-shell page with health probe, offline card, open-in-tab,
  deep-link sync. Dev-mode /plugin-ui middleware in vite.config.ts.

OpenAPI regen for the new endpoints follows in the next commit (built on Linux).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 09:48:37 +02:00
enricobuehlerandClaude Fable 5 8743f63406 polish(web): favicon, Punktfunk casing, display-save toast, idle stop button
- Favicon: the brand-mark lens SVG at /favicon.svg, linked from the root head.
- Branding: user-facing "punktfunk" → "Punktfunk" (tab title, app_name,
  pairing prose, the logo aria-labels/SVG titles) — protocol identifiers
  (punktfunk/1, _punktfunk._udp, binary/module names) stay lowercase.
- The display policy auto-saves on every preset pick / field edit with no
  signal; mount @unom/ui's sonner Toaster in the root and fire a success toast
  from the save funnel's onSuccess (new i18n key, en + de). First toast usage
  in the console — errors stay inline as before.
- The stop-session button rendered destructive-red even with nothing to stop:
  variant switches to the muted `secondary` while no session is active.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 22:18:37 +02:00
enricobuehlerandClaude Opus 4.8 b28ddfc2d4 fix(web): clearer topology/identity copy, capped description width, mobile More-nav, preset spacing
Console polish on the Virtual displays card + shell:
- Topology help now leads with the streamed display's role (Extend/Primary/Exclusive) instead of
  the confusing physical-monitor-only framing; notes the headless case. Identity help spells out the
  actual behavior (stable per-client identity → the desktop reapplies that client's scaling/resolution
  on reconnect) + what Shared / Per-client / Per-client+resolution each do.
- Cap description/help width at max-w-prose so long help text isn't a full-viewport line on large screens.
- Mobile bottom nav: 8 flat tabs were too cramped → 4 pinned tabs + a "More" tab whose sheet holds the
  rest (Performance/Logs/Pairing/Settings), "More" highlighted when the active route is in the overflow.
- More breathing room under the "Preset" heading.

web tsc + biome + vite build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:40:35 +00:00
enricobuehlerandClaude Opus 4.8 167b3ae251 feat(web): move Virtual displays to its own nav section; roomier preset grid
The Host page was crowded (identity, codecs, ports, GPU, displays, compositors) and the
virtual-display config surface is large enough to warrant its own home.

- New **Virtual displays** nav section: `/displays` route + `sections/Displays` (moved
  DisplayCard out of `sections/Host`), a `MonitorPlay` sidebar entry after Host, and
  `nav_displays` i18n. Removed the displays card from the Host page/view.
- On its own page the card splits into two: **Configuration** (presets + custom axes) and
  **Live displays** (the live list + arrangement table) — room to breathe.
- Presets now render in a max-2-column grid (`sm:grid-cols-2`) with larger padding, a bigger
  section heading + preset titles (text-base semibold), roomier spacing, and bottom-aligned
  "what it sets" badges so the cards line up.

web tsc + vite build + biome-lint green; deployed + verified on the Mutter box (.21).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:54:24 +00:00
enricobuehlerandClaude Fable 5 160a0ad015 fix(web): pin the sidebar at viewport height
Sticky h-dvh sidebar: long pages scroll the content, not the nav — the flex
stretch was pushing the language switcher below the fold; overflow-y-auto keeps
the nav usable on short viewports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:09:52 +00:00
enricobuehlerandClaude Fable 5 d86f1a6bdd feat(host,web): host log ring + GET /api/v1/logs + console Logs page
Remote debugging without shell access: a tracing layer tees every
event at DEBUG-and-up — independent of the RUST_LOG filter gating
stderr/host.log, so console-side debugging never needs a restart —
into a bounded in-memory ring (log_capture.rs, 4096 newest entries,
OnceLock singleton like config()), installed at both init sites
(stderr path in main, the Windows service file path). The mgmt API
serves it cursor-paged at GET /api/v1/logs?after=&limit= — bearer-only
and deliberately NOT on the mTLS cert allowlist (log lines can name
client identities and host paths). The web console grows a Logs page
(follow/pause · min-level filter · text search · eviction-gap badge);
polling self-paces: a non-empty page advances the after-cursor (new
query key → immediate refetch, drains backlogs), an empty page idles
at the 2s interval. OpenAPI regenerated; ring pagination/eviction,
layer wiring, and the authed route are unit-tested; Storybook story
included.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:33:56 +00:00
enricobuehlerandClaude Opus 4.8 ae51276a03 feat(web): consolidate paired devices, self-contained sections, docs + lint
Web console
- Pairing/Library/Stats refactored into self-contained subsections that each own
  their own queries + mutations; a shared slot-based layout (view.tsx) is filled by
  the live page (containers) and Storybook (pure cards + fixtures) so the layout can't
  drift.
- All paired devices in one list on Pairing with a protocol column (punktfunk/1 +
  Moonlight), routing each unpair to the right endpoint; the redundant Clients page is
  removed.
- Library: overview grid split from the add/edit form into separate files.
- Login screen links out to the docs.

Docs
- "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows)
  plus a new "Forgot your Password?" troubleshooting page, linked from the login screen.
- Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the
  Bun entry, launchers, systemd units, and packaging.

Tooling
- Biome now respects .gitignore (stops linting generated code), config migrated to
  2.5.1; all lint issues fixed cleanly.

Also includes this branch's in-progress host, Apple client, packaging, and CI changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 19:05:22 +02:00
enricobuehlerandClaude Opus 4.8 da3f37317b feat(host): web-console performance capture — record stream stats, graph them
Arm streaming-perf-stats capture from the web console, play, stop, and review the
run as graphs; finished captures are saved to disk as browsable/exportable
recordings. Covers both the native punktfunk/1 path and GameStream.

- stats_recorder.rs: one shared Arc<StatsRecorder> ring (created in gamestream::serve,
  shared with the mgmt API + both streaming loops, mirroring NativePairing). The
  hot-path gate is a runtime AtomicBool that replaces the startup-only PUNKTFUNK_PERF
  for *recording* (PERF stdout logging unchanged); bounded ring (~3 h); atomic
  temp+rename writes to ~/.config/punktfunk/captures/*.json; path-traversal-safe ids;
  poison-resilient locks.
- native (punktfunk1.rs) + GameStream (stream.rs) emit a StatsSample at their existing
  ~2 s / ~1 s aggregation boundary — per-stage latency p50/p99, fps new/repeat, goodput,
  loss/FEC deltas — with no new per-frame work beyond the cheap atomic check.
  FrameMsg.was_measured keeps pre-arm in-flight frames out of the first window's
  percentiles (without zeroing the Windows-relay path's fps/encode).
- mgmt.rs: 7 bearer-only /api/v1/stats/* endpoints (capture start/stop/status/live;
  recordings list/get/delete); api/openapi.json regenerated, in sync.
- web: new "Performance" page (recharts, rendered SSR-safe) — capture control, live
  graphs while armed, recordings table (view / download-JSON / delete), and a detail
  view with the latency stacked-area bottleneck breakdown (p50/p99 toggle) + throughput
  + health. Charts adapt to either path's stage set.

Design: design/stats-capture-plan.md. Built and adversarially reviewed via a multi-agent
workflow; workspace build/clippy(-D warnings)/fmt/tests green, OpenAPI no-drift. Not yet
on-glass validated against a live session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 13:59:39 +00:00
enricobuehler 187738ed86 improve web ui 2026-06-26 05:43:34 +00:00
enricobuehlerandClaude Opus 4.8 6bd2e3341f feat(web): Storybook for offline UI design + light theme + brand spinner
Stand up Storybook so the management console can be designed without a running
host, plus the design-system work that surfaced along the way.

Storybook (@storybook/react-vite):
- Slim Start/Nitro-free vite config; the preview imports the app's real
  src/styles.css directly so the design tokens stay single-sourced (no mirror).
- Stories for the @unom/ui primitives (Button/Card/Inputs/Badge), brand marks,
  the AppShell (throwaway in-memory TanStack router), and every data-driven page
  (Dashboard/Host/Clients/Library/Settings) rendered offline via a window.fetch
  stub + typed fixtures. The route page components are exported so stories can
  render them.

Light theme:
- styles.css now carries a light :root (lavender, from the docs palette) with the
  existing violet chrome moved to .dark; the live console still pins html.dark by
  default, so this only adds the option (Storybook's toolbar toggles it).
- Fixes a stray `*/` inside a comment that prematurely closed it and silently
  broke Tailwind's @theme processing.

Spinner:
- The punktfunk lens recreated with motion/react: two circles surge through one
  another in depth (JS perspective scale + z-index — robust where mix-blend-mode
  flattens CSS preserve-3d) with a screen-blend lens highlight. Replaces the
  skeleton loading state in QueryState; removes ui/skeleton.tsx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 21:58:36 +00:00
enricobuehlerandClaude Opus 4.8 7bc5deee7d fix(web): session-card button overflow + bottom-nav icon alignment
- Dashboard session card: the header stacks the title above the action buttons
  on narrow screens (flex-col -> sm:flex-row) and the button group wraps
  (flex-wrap), so "Request IDR" / "Stop session" no longer overflow the card.
- Mobile bottom nav: give each label a fixed two-line-tall centered box so a
  1- or 2-line label (labels vary by locale) keeps every tab icon at the same
  height.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:33:33 +00:00
enricobuehlerandClaude Opus 4.8 700b965c31 feat(web): drop material gloss, full punktfunk theme for Scalar, center mobile tabs
- console: remove @unom/ui's specular "material" gloss (drop UnomProviders +
  the material.css import) so components render flat like the marketing site;
  the violet brand + Geist stay.
- mobile bottom tab bar: center the labels (w-full text-center, leading-tight)
  and even out the per-tab layout.
- docs /api: roll the punktfunk dark-violet palette across the whole Scalar
  reference (surfaces/text/sidebar/links/buttons/method colours via the full
  --scalar-* token set), locked to dark (hideDarkModeToggle).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:19:51 +00:00
enricobuehlerandClaude Opus 4.8 56f724815e feat(web): unify console + docs on @unom/ui; host OpenAPI via Scalar
Move the management console (web/) off shadcn/ui to the shared @unom/ui
design system the marketing site + docs are built on, on the punktfunk
violet brand over dark chrome:

- Add @unom/ui/@unom/style/motion/radix-ui/zod + Geist; web/.npmrc maps the
  @unom scope (packages are public-read, so CI needs no npm auth).
- styles.css: one dark-violet palette (#141019/#1c1530, brand #6c5bf3 ->
  #a79ff8) exposed under BOTH the shadcn token names the routes use and
  @unom/ui's contract, so routes + components both resolve; pulls in
  @unom/ui's material gloss + easings.
- components/ui/* now back onto @unom/ui (AnimatedButton/InputText/Label/
  AnimatedCard); brand-mark/wordmark/logo replace the generic Radio icon in
  the shell + login.
- MaterialProvider (specular gloss) at the root. No UI sounds, like the site.

docs-site: new /api route renders the host management REST API as an
interactive Scalar reference (reads public/openapi.json, a snapshot of
docs/api/openapi.json), branded violet and linked from the top nav, the
docs sidebar, the landing page, and host-cli.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 12:00:46 +00:00
enricobuehlerandClaude Opus 4.8 5e7521fa5b feat(web/library): game library page — grid + custom-entry CRUD
Consumes the new library API (47f87a4) via the orval-generated hooks. A poster grid
over GET /api/v1/library (all stores merged), plus create/edit/delete for custom
entries — the admin-UI half of "create custom entries via the web console".

- GameCard: portrait (600×900) art with an onError fallback chain portrait → header
  → text placeholder (many Steam titles lack a 600×900 capsule). A store badge marks
  Steam vs Custom; only custom cards expose edit/delete.
- Inline add/edit form (title + portrait/hero/header URLs + optional launch command,
  mapped to LaunchSpec{kind:"command"}) wired to useCreateCustomGame /
  useUpdateCustomGame / useDeleteCustomGame; the CRUD id strips the `custom:` prefix;
  every mutation invalidates the library query. QueryState handles load/empty/error.
- Nav entry (LibraryBig) + en/de i18n strings.

`bun run lint` (tsc) and `bun run build` both green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:48:00 +00:00
enricobuehlerandClaude Opus 4.8 a46abb1b56 fix(web): mobile navigation — add a bottom tab bar + top bar
The app shell's only navigation was the desktop sidebar (`hidden … sm:flex`), so
on phones (< sm) it was hidden with no replacement — you couldn't navigate at all.

Add a responsive mobile layout shown only below `sm`: a top bar (brand + language
switcher) and a fixed bottom tab bar with the five nav items (icon + label). The
desktop sidebar is unchanged. Page content gets bottom padding so the fixed bar
doesn't cover it, and the bar respects the iOS `safe-area-inset-bottom`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:59:54 +00:00
enricobuehlerandClaude Opus 4.8 7d64bb5a43 feat(web): management console — TanStack Start + orval + shadcn + Paraglide
Browser UI for the host's management REST API (mgmt.rs / docs/api/openapi.json).

Stack, exactly as specified:
- TanStack Start (Vite, SPA mode) — file-based routes, SSR shell + client hydration.
- React Query via orval codegen from the checked-in OpenAPI spec: a custom fetch mutator
  (src/api/fetcher.ts) centralizes the base URL, the bearer token (Settings → localStorage),
  JSON, and a throwing ApiError; the query client skips retries on 4xx. orval returns the
  response body directly (includeHttpResponseReturnType:false) so a query's `.data` is the
  typed payload; GET→useQuery, POST/DELETE→useMutation by method.
- shadcn/ui on Tailwind v4 (CSS-first tokens, dark-first) — button/card/badge/input/label/
  table/skeleton primitives hand-authored from the canonical source.
- Paraglide i18n (en + de) with a reactive useLocale() hook and a language switcher.

Pages: dashboard (live status — video/audio/session/stream, stop-session + request-IDR,
2s polling), host (identity/codecs/ports), clients (paired list + unpair), pairing (PIN
submit, polls pin_pending), settings (API token + language).

Dev server proxies /api → 127.0.0.1:47990 (same-origin, no CORS; PUNKTFUNK_MGMT_URL to
override). Generated code (orval client, paraglide runtime, routeTree) is gitignored and
reproduced by `pnpm codegen` (prepare/pre* scripts). Validated live against `serve`: API
shapes match, dev proxy works, SSR shell renders the localized nav, build + tsc green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:00:12 +00:00