feat(host/web): per-scanner library toggles in the console
apple / swift (push) Successful in 1m16s
apple / screenshots (push) Successful in 6m29s
ci / web (push) Successful in 1m1s
ci / docs-site (push) Failing after 24s
ci / bench (push) Successful in 5m31s
deb / build-publish (push) Successful in 9m21s
decky / build-publish (push) Successful in 24s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 13s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 9s
android / android (push) Successful in 19m41s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish-host (push) Successful in 9m44s
arch / build-publish (push) Successful in 18m17s
ci / rust (push) Successful in 19m0s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9m14s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 25m20s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 25m10s
windows-host / package (push) Successful in 17m47s
docker / deploy-docs (push) Successful in 10s

Every installed-store scanner (Steam; Lutris+Heroic on Linux; Epic/GOG/
Xbox on Windows) was hardwired on. New library-scanners.json persists the
operator's disabled set (default all on; absent/malformed = all on);
all_games() gates each provider, so disabling one hides its titles from
every surface (console grid, native clients, GameStream app list, launch
resolve). GET /library/scanners lists this platform's scanners + state;
PUT /library/scanners/{id} toggles and emits library.changed — admin lane
only (the cert allowlist's exact-path /library match keeps both off the
LAN surface). The console's Library page grows a "Game sources" card with
one chip per scanner (platform-shaped by the API), EN+DE strings, story.
The scanners are slated to become plugins; the stable per-scanner ids are
the migration seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 19:58:05 +02:00
parent 940a260506
commit c2bba13405
11 changed files with 552 additions and 7 deletions
+46
View File
@@ -138,6 +138,13 @@ async fn cert_auth_is_a_read_only_allowlist() {
"the client roster {p} must require the bearer token, not just a paired cert"
);
}
// The scanner settings are admin-only in BOTH directions: the exact-path `/api/v1/library`
// cert match must not leak the settings GET, and the toggle PUT is operator configuration.
assert_eq!(
send_cert(&app, get_req("/api/v1/library/scanners"), fp).await,
StatusCode::UNAUTHORIZED,
"the scanner settings must require the bearer token, not just a paired cert"
);
// The plugin directory is admin-only — a paired streaming cert has no business enumerating the
// host's running plugins or reaching a plugin UI's proxy credential (plugin-ui-surface §3).
for p in [
@@ -1309,6 +1316,45 @@ async fn hooks_get_shape_and_put_validation() {
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
// ------------------------------------------------------------------ library scanners
/// The scanner list is platform-shaped and read-only-safe; the toggle rejects unknown ids with
/// 404. (A successful toggle PUT would write the developer's real `library-scanners.json`, so the
/// write path is exercised only through the unknown-id rejection here — the settings round-trip
/// itself is unit-tested in `library::scanners` against pure shapes.)
#[tokio::test]
async fn library_scanner_list_and_unknown_toggle() {
let app = test_app(test_state(), None);
let (s, json) = send(&app, get_req("/api/v1/library/scanners")).await;
assert_eq!(s, StatusCode::OK);
let scanners = json.as_array().expect("a scanner array");
assert!(
scanners
.iter()
.any(|sc| sc["id"] == "steam" && sc["label"].is_string() && sc["enabled"].is_boolean()),
"steam must be a scanner on every platform: {json}"
);
// Only platform-available scanners appear (`custom` is a store, never a scanner).
assert!(scanners.iter().all(|sc| sc["id"] != "custom"));
let (s, json) = send(
&app,
axum::http::Request::put("/api/v1/library/scanners/not-a-store")
.header(axum::http::header::CONTENT_TYPE, "application/json")
.body(Body::from(
serde_json::json!({"enabled": false}).to_string(),
))
.unwrap(),
)
.await;
assert_eq!(
s,
StatusCode::NOT_FOUND,
"unknown scanner id must 404: {json}"
);
}
// ------------------------------------------------------------------ library providers
/// Provider reconcile validation (the write path itself is unit-tested in `library::custom`