Is discovery actually doing anything, and if not, WHY.
const url = 'http://localhost:8080/v1/admin/library/status';const options = {method: 'GET', headers: {Authorization: 'Bearer <token>'}};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}use reqwest;
#[tokio::main]pub async fn main() { let url = "http://localhost:8080/v1/admin/library/status";
let mut headers = reqwest::header::HeaderMap::new(); headers.insert("Authorization", "Bearer <token>".parse().unwrap());
let client = reqwest::Client::new(); let response = client.get(url) .headers(headers) .send() .await;
let results = response.unwrap() .json::<serde_json::Value>() .await .unwrap();
dbg!(results);}curl --request GET \ --url http://localhost:8080/v1/admin/library/status \ --header 'Authorization: Bearer <token>'Phase 4. INERT_REASON IS THE REASON THIS ENDPOINT EXISTS, and it is part of the contract rather than a nicety: under auto-publish the only observable signal of a working scan is tiles appearing, so “nothing appeared” and “nothing ran” look identical to an operator unless something says which. It is “” when discovery is live, and otherwise names exactly one of - the master switch is off; QUASAR_LIBRARY_SCAN_INTERVAL is 0 (which disables discovery regardless of the database flag); or the instance storage provider is ‘volume’, which has no host path a node-agent can walk. scan_interval_secs is the RESOLVED interval, so an operator can see what their “6h” actually became.
Authorizations
Section titled “Authorizations”Responses
Section titled “Responses”OK.
object
Admin-libraries amendment (2026-08-01). True when QUASAR_LIBRARY_SCAN_INTERVAL is set in the environment, so scan_interval_secs resolved from it rather than from instance_settings.library_discovery_interval_minutes. A UI should grey its interval control and say the environment pinned it.
Admin-libraries amendment (2026-08-01). True when QUASAR_STEAM_APPDETAILS_LOOKUP is set in the environment; appdetails_lookup then reports the env value, not the database column. Same UI treatment as interval_overridden_by_env.
Admin-libraries amendment (2026-08-01). When the most recent scan finished (any terminal outcome), or null when no scan has ever completed - the one-glance “is discovery actually doing anything” companion to the counters.
Scan-observability amendment (2026-08-01, same-day follow-on). The last 20 TERMINAL scans (reported or failed), newest first, each with its stored outcome counts - the validation surface the log line used to be the only copy of. Under auto-publish the only observable signal of a working scan was tiles appearing; an operator whose library is already fully published pressed “Scan now” and could not tell it from a no-op. Counts are STORED AT RECONCILE (migration 0048) - scans reported before 0048 display zeros, which the UI should present as “not recorded”, not as “nothing happened”. backfilled counts existing tiles whose missing data (description, initially) a scan filled in - see the backfill paragraph on the scan-report route: a backfill FILLS BLANKS AND NEVER OVERWRITES a non-empty field, so an operator edit is never clobbered by a scan.
object
Username of the scanned home’s owner.
Node_name of the host that ran the walk.
Empty for reported scans.
Instance_settings.library_discovery_enabled - the master switch and the only switch.
Managed-home backing store: auto = local when the session host has an effective home root, volume otherwise. Affects new homes only.
The RESOLVED scan interval in seconds, so an operator can see what their setting actually became. Admin-libraries amendment (2026-08-01): resolution is instance_settings.library_discovery_interval_minutes (default 360 = 21600s) unless QUASAR_LIBRARY_SCAN_INTERVAL is set, in which case the env var wins - including its unchanged 0 semantics: 0 disables discovery ENTIRELY regardless of the database flag, the kill switch that lets an operator guarantee no scan and no third-party call without database access.
The RESOLVED third-party appdetails switch, DEFAULT OFF. Rung 5 of the ladder. IT DISCLOSES TO A THIRD PARTY EXACTLY WHICH STEAM APPIDS THIS INSTANCE HAS INSTALLED - the same privacy class as artwork hotlinking, which was rejected for the same reason - which is why it is an operator’s decision and never a default. Admin-libraries amendment (2026-08-01): resolution is instance_settings.library_discovery_appdetails_enabled unless QUASAR_STEAM_APPDETAILS_LOOKUP is set, in which case the env var wins. Contained so that enabling it cannot override a rule an admin wrote, and bounded per scan.
“” when discovery is live; otherwise why it is doing nothing. THIS FIELD IS THE REASON THE ENDPOINT EXISTS: under auto-publish the only observable signal of a working scan is tiles appearing, so “nothing appeared” and “nothing ran” are indistinguishable to an operator otherwise - and leaving them to conclude from an unchanged library that nobody has any games installed is the failure this closes. FOUR REASONS, REPORTED IN THIS ORDER: the library_discovery_enabled switch is off; QUASAR_LIBRARY_SCAN_INTERVAL is 0; the instance storage provider is ‘volume’; or NO APP IS MARKED AS A LIBRARY PROVIDER (library_provider=‘steam’), so the eligibility query joins against nothing. The fourth is the first-run state - an operator flips the toggle before marking the app - and it is reported LAST because the three above it are instance-level facts that make the provider question moot: telling someone who has the feature switched off that they also have no provider app would send them configuring an app for nothing. It is a REASON AND NOT A GATE: enqueue behaviour is unchanged and zero eligible triples remains a normal outcome. CLIENTS MUST NOT PARSE THIS STRING - it is human-readable prose rendered verbatim, the set will grow, and only its presence is contractual. The message names the remedy (set Library provider to Steam in the app editor’s Identity section), because an operator told only that discovery is inert is barely better off than one told nothing.
object
A claim older than 30 minutes is reaped back to pending - the only recovery for an agent that died mid-walk.
Reported AND reconciled: those commit in one transaction, so there is no third state.
Example
{ "recent_scans": [ { "state": "reported" } ], "storage_provider": "auto"}Missing/invalid/expired/revoked token.
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}Authenticated but insufficient role / not the owner (precedes resource lookup).
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}Could not read settings or scan counts.
object
object
E.g. validation_failed, unauthorized, forbidden, not_found, conflict, session_quota_exceeded, home_in_use, home_not_provisioned, parent_app_disabled, profile_ineligible, profile_not_launchable_for_app, no_host_available, capacity_exhausted, restart_required, rate_limited, internal. Open string, not an enum: new codes are additive and an unknown one falls through to a client’s generic per-status branch.
Present on restart_required.
Steam library discovery Phase 3, ADDITIVE: present on home_in_use when the guard could name the CONFLICTING live session - the one already holding the home. It is here so the client can offer “go to your running session” with a link instead of a dead-end toast. OMITTED rather than empty when the conflict is known but the session is not, so a client branches on presence and never renders a link to nowhere. Load-bearing once derived tiles exist: the lock is held by the PARENT’s home, so a user who clicks a game tile can be refused because a DIFFERENT app (the Steam launcher, or another game from the same install) is running, and without the session id the refusal reads as a bug. See control-api.md §Derived tiles.
Steam library discovery Phase 3, ADDITIVE: present on the 409 conflict from DELETE /v1/apps/{id} when the app has derived tiles and ?delete_derived=true was not sent. A LIST, not a count - the point of the confirmation is that the admin sees what they are about to destroy. Capped; an empty array means the tiles could not be listed, never that there are none.
object
Examplegenerated
{ "error": { "code": "example", "message": "example", "live_sessions": 1, "session_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "derived_tiles": [ { "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "name": "example" } ] }}