Skip to content

"Scan now": enqueue pending scans immediately, bypassing the recency pacing.

POST
/v1/admin/library/scan
curl --request POST \
--url http://localhost:8080/v1/admin/library/scan \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "app_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "user_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0" }'

IT BYPASSES PACING AND NEVER A GATE. What it drops is the janitor’s “no successful scan for this triple inside QUASAR_LIBRARY_SCAN_INTERVAL” predicate - the rule that stops a six-hourly sweep re-walking every home on every pass, and precisely the rule an operator who just installed a game or just fixed a home mount is overriding on purpose. Every GATE still applies and is still reported: the library_discovery_enabled switch, the ‘volume’ storage provider, QUASAR_LIBRARY_SCAN_INTERVAL=0 as the hard kill switch, and the per-home provider=‘local’ filter (enforced inside the enqueue, so a volume-backed home on an otherwise-local instance is never forced into a pending row nothing could claim). AN INERT INSTANCE IS A 200 CARRYING inert_reason, DELIBERATELY NOT A 409 OR A 400. It mirrors GET /v1/admin/library/status, whose inert_reason exists for exactly this question, so a client renders ONE code path for “here is what happened” and “here is why nothing did”; the server computes both surfaces’ reasons through one shared helper so the wording cannot drift between the status panel and the button. Scope is optional and AN EMPTY BODY IS VALID, meaning “everything, now” - the common case and what the admin button sends. A non-provider app_id is 400 and an app_id that does not exist at all is 404, reusing the two-mistakes-two-fixes split the four /v1/admin/apps/{id}/library/* routes already apply rather than inventing a second vocabulary for the same mistake; a malformed uuid or malformed JSON is likewise 400. ORDER OF CHECKS IS OBSERVABLE AND THEREFORE CONTRACTUAL: body shape first (400), then INERTNESS (200 + inert_reason), then the scope (400/404). A bad app_id on a switched-off instance is the 200, not the 400 - the instance-level answer explains why nothing at all can happen, and reporting a scope defect first would send an operator fixing an app id on a control plane that would have refused any scope. Double-press is IDEMPOTENT: library_scans_open_uk is partial on pending/claimed and the enqueue is ON CONFLICT DO NOTHING, so the second press inserts nothing, reports it as skipped, and is not an error. Audited as library.scan.force, identifiers and counts only - this makes the whole fleet walk every user’s home directory on demand.

Media typeapplication/json

The optional scope for a force scan. EVERY FIELD IS OPTIONAL AND AN EMPTY BODY IS VALID, meaning “everything, now” - the common case, and what the admin UI’s button sends. An absent body is not an error; only malformed JSON is.

object
app_id

Narrow to one LIBRARY-PROVIDER app. A real app that is not a library provider is 400 validation_failed - the same rule the four /v1/admin/apps/{id}/library/* routes apply, reused rather than given a second vocabulary, because it is the same operator mistake: a scope naming an app the reconciler will never read can only ever produce a bare zero. An app id that does not exist at all is 404 - the app being REAL is what makes the other one a 400. A malformed uuid is also 400 (a format guard at the handler, so it is never a Postgres 22P02 surfacing as a 500).

string format: uuid
user_id

Narrow to one user. Malformed uuid is 400.

string format: uuid
Examplegenerated
{
"app_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"user_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0"
}

Enqueued, or inert with a stated reason.

Media typeapplication/json

What one force scan actually did. THERE IS NO PATH THROUGH THIS ENDPOINT THAT RETURNS A BARE ZERO: either work was queued, or something already had it queued, or inert_reason says why not. Under auto-publish silence is indistinguishable from success, which is the same ambiguity GET /v1/admin/library/status exists to close.

object
queued
required

‘pending’ library_scans rows actually inserted by this call.

integer
skipped
required

Triples that already had an OPEN scan (pending or claimed). NOT AN ERROR COUNT: library_scans_open_uk is partial on those two states, so “already queued” is the CORRECT answer to “scan now”, not a failure. Enqueue is ON CONFLICT DO NOTHING, which is what makes a double-press idempotent - no duplicates, no 409, no 500.

integer
eligible
required

Queued + skipped: the (user, library-provider app, host) triples this request matched AT ALL. THIS IS THE NON-OBVIOUS FIELD AND IT IS WHY IT EXISTS - two zeros mean opposite things. queued:0 with eligible>0 means “everything is already queued, wait for the agent to claim it”; queued:0 with eligible:0 means “your scope matched nothing, fix the scope”. One says do nothing, the other says do something, and without this field a client cannot tell them apart.

integer
inert_reason
required

“” when the call could do work; otherwise why it could not - the 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’), which is the first-run state and is answered LAST of the four for the reason LibraryStatus.inert_reason gives. AN INERT INSTANCE IS THIS 200 AND NOT A 4xx, mirroring GET /v1/admin/library/status so a client has one code path; the two surfaces share one server-side helper so their wording cannot drift, and the janitor logs the same reason once rather than every six hours. An eligible:0 result also carries a reason here - naming the eligibility rule that matched nothing - rather than a silent zero; that fifth reason is route-specific (“your scope matched nothing”) and is only reached once none of the four instance-level ones applies. CLIENTS MUST NOT PARSE THIS STRING; only its presence is contractual.

string
Examplegenerated
{
"queued": 1,
"skipped": 1,
"eligible": 1,
"inert_reason": "example"
}

Malformed or invalid request.

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"
}
]
}
}

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"
}
]
}
}

No such resource.

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 enqueue scans.

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"
}
]
}
}