A field report: the Steam plugin installed, the grid stayed empty, and the only clue was one warn per sync — `art.hero: local art must be an image file … inside an allowed art root`. Two defects, both here. The art roots defaulted to the users base (`C:\Users`, from `%PUBLIC%`'s parent). That covers the launchers that install per-user, but not Steam, which installs to `C:\Program Files (x86)\Steam` and keeps both the things the plugin publishes there — `appcache\librarycache\<appid>\<hash>\` and each account's `userdata\<id>\config\grid\`. So every cover was out of root. It is a v0.28.0 regression: the built-in scanner the plugin replaced served covers through the legacy `steam:` art-proxy branch, which never passed through the H-2 confinement, so deleting the scanner routed that art through a gate it had never been measured against. `art_roots()` now also carries every Steam install it can find, from the three Program Files vars and from HKLM `Valve\Steam\InstallPath` so a Steam on another drive counts too. POSIX needs no equivalent — native and Flatpak Steam are both already under `$HOME`. The confinement is not weakened. It exists to stop the host (SYSTEM) reading what the plugin lane (LocalService) cannot reach itself; the Steam directory is readable by LocalService already, so nothing there is reachable *because* the host is privileged, and the extension, regular-file, magic-byte and config-dir gates still apply on top. Tested: `config.vdf` is not servable from an art root, nor is a non-image wearing `.png`. Second, and the reason this cost a whole library rather than a thumbnail: the provider reconcile validated art per entry and 400'd the WHOLE payload on the first bad value. A path mismatch therefore deleted every game from that store, and the plugin — which only ever sees `HostRequestError` — could not say which. A reconcile now strips unservable local art and syncs the rest, logging one aggregated warn with the count, an example path and the env var. The invariant the 400 held is unchanged: no unservable path is persisted. The operator's own single-entry writes keep the hard 400, because there the path was typed by hand and silence would be the wrong answer. Verified on Linux (.25: 493/493, clippy clean) and Windows (.133: 12/12 art tests, clippy clean). The new Windows test is hermetic — it repoints `%ProgramFiles(x86)%` at a synthetic Steam tree rather than asserting over whatever Steam the box happens to have, since the vacuous version of that test is what would have let this ship. Confirmed non-vacuous by disabling the fix: it fails on "the DEFAULT art roots must include it".
punktfunk-docs
The Punktfunk documentation site: Fumadocs on TanStack Start (Vite + Nitro/bun preset).
Content lives in content/docs/ as .md/.mdx. This site is the source of truth
for the user-facing guides; design rationale lives in the internal punktfunk-planning repo.
API reference
/api renders the host's management REST API as an interactive
Scalar reference (linked from the top nav, the docs
sidebar, and the landing page). It reads public/openapi.json — a
snapshot of the repo's generated spec. Refresh it after a management-API change:
# from the repo root — regenerate the spec, then copy the snapshot in:
cargo run -p punktfunk-host -- openapi > api/openapi.json
cp api/openapi.json docs-site/public/openapi.json
Nothing in CI diffs the two, so the snapshot goes stale silently — that manual cp is the only
thing keeping them in sync. Before publishing docs, check that they match:
diff <(jq -S . api/openapi.json) <(jq -S . docs-site/public/openapi.json)
That should print nothing. Right now it doesn't: the committed snapshot predates the
/api/v1/update/check, /api/v1/update/apply and /api/v1/update/status endpoints, so the
published /api reference is missing the host self-update surface — re-copy it.
Develop
bun install
bun run dev # http://localhost:3001 (docs at /docs)
CI gates every change on bun run build followed by bun run lint (the TypeScript typecheck), in
that order — the build emits the .source typegen the typecheck imports. Run both before you push.
Build & serve
bun run build
bun run start # serves .output/ via Bun
Layout
source.config.ts Fumadocs MDX collection (content/docs)
content/docs/ the docs content (.md/.mdx) + meta.json nav
src/
routes/
__root.tsx RootProvider + html shell
index.tsx landing page
docs/$.tsx catch-all docs renderer (Fumadocs DocsLayout)
api/index.tsx Scalar API reference (reads public/openapi.json)
api/search.ts Orama search endpoint
lib/source.ts Fumadocs loader over the generated collection
lib/layout.shared.tsx shared nav chrome
components/mdx.tsx MDX component map
styles/app.css Tailwind 4 + Fumadocs preset