Skip to content

Is discovery actually doing anything, and if not, WHY.

GET
/v1/admin/library/status
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.

OK.

Media typeapplication/json
object
interval_overridden_by_env
required

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.

boolean
appdetails_overridden_by_env
required

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.

boolean
last_scan_completed_at
required

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.

string | null format: date-time
recent_scans
required

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.

Array<object>
<= 20 items
object
user
required

Username of the scanned home’s owner.

string
host
required

Node_name of the host that ran the walk.

string
state
required
string
Allowed values: reported failed
completed_at
required
string format: date-time
observed
required
integer
suppressed
required
integer
created
required
integer
disabled
required
integer
granted
required
integer
revoked
required
integer
rejected
required
integer
backfilled
required
integer
error
required

Empty for reported scans.

string
enabled
required

Instance_settings.library_discovery_enabled - the master switch and the only switch.

boolean
storage_provider
required

Managed-home backing store: auto = local when the session host has an effective home root, volume otherwise. Affects new homes only.

string
Allowed values: auto local volume
scan_interval_secs
required

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.

number
appdetails_lookup
required

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.

boolean
inert_reason
required

“” 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.

string
scans
required
object
pending
required
integer
claimed
required

A claim older than 30 minutes is reaped back to pending - the only recovery for an agent that died mid-walk.

integer
reported
required

Reported AND reconciled: those commit in one transaction, so there is no third state.

integer
failed
required
integer
Example
{
"recent_scans": [
{
"state": "reported"
}
],
"storage_provider": "auto"
}

Missing/invalid/expired/revoked token.

Media typeapplication/json
object
error
required
object
code
required

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.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

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.

string format: uuid
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.

Array<object>
object
id
required
string format: uuid
name
required
string
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).

Media typeapplication/json
object
error
required
object
code
required

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.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

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.

string format: uuid
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.

Array<object>
object
id
required
string format: uuid
name
required
string
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.

Media typeapplication/json
object
error
required
object
code
required

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.

string
message
required
string
live_sessions

Present on restart_required.

integer
session_id

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.

string format: uuid
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.

Array<object>
object
id
required
string format: uuid
name
required
string
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"
}
]
}
}