shc / arch / doctor ← lab docs

What is wrong with my cluster?

Fifteen diagnoses hang off one bus. Each names the subsystem it interrogates, returns a verdict, and only six carry a repair.

not yet run

10 core — seeded by doctor's own init()

5 module probes — adopted at daemon wire time

pass warn fail fixed skipped fix = carries a prescription

tap any check for its real finding and remedy — or unplug the daemon and run again

the same run, from a shell simulated

simulated — no daemon, no docker, no sqlite. The findings on this page are one plausible cluster; the diagnosis names, categories, descriptions, verdict rules and prescriptions are copied from the code. Nothing advances on its own: the probe head only moves while a sweep you started is in flight, and the failing lamps breathe because a live alarm should. The registry is exactly these fifteen. Ten are seeded by the doctor package's own init()system.directories, system.docker-daemon, database.orphaned-stacks, database.orphaned-dirs, database.stale-status, database.storage, deployments.missing-compose, state.container-mismatch, doctor.self, keycloak.realms (internal/modules/doctor/register.go:38-67). Five more are published by sibling modules through the central registry and adopted into the same table only when the daemon wires them: vault.status, vault.secrets, vault.crypto_format (internal/modules/vault/doctor.go:39-54), storage.local (internal/modules/storage/doctor.go:18-25) and repo.sync-state (internal/modules/repo/doctor.go:57-66), all through RegisterModuleDiagnoses (bridge.go:28-42), which the daemon calls once at wire time (internal/daemon/services.go:1097). That is why unplugging the daemon does not merely skip them — they are never registered, and shc doctor ls in a bare CLI lists ten. Severity is not the verdict. A diagnosis with no findings is pass; findings that are all info roll up to warn; anything else is fail, which is why a single warning-severity finding still fails its diagnosis. A diagnose callback that returns an error or panics is skipped, never a failure, and only fail drives exit 1 (framework.go:142-204, commands/run.go:52-58). Prescriptions never run unless you ask. Six carry one — system.directories (MkdirAll 0o755), database.orphaned-stacks (DELETE FROM deployment), database.stale-status (normalises the stopped flag to the status enum), state.container-mismatch (rewrites status to degraded), keycloak.realms (ProvisionTenantRealm) and repo.sync-state (a module-owned re-sync, which the bridge adopts onto the same slot). deployments.missing-compose deliberately has none in the Go port because the stack service is not ported yet; database.orphaned-dirs has none by design, because deleting a tenant's data directory is not a doctor-level repair. The daemon fallback is the honest part: with no daemon the CLI still wires the paths seam (internal/cli/cli.go:249) so system.directories, system.docker-daemon and database.storage genuinely run, but SetDBResolver and SetKeycloakRealms are daemon-only (services.go:1061-1075), so the six checks that need them return X500918DCRNWR and land as skipped — a clean-looking exit 0 that saw almost nothing. --fix in that state is refused outright with X503920DCRNAV rather than repairing against a partial view (commands/run.go:84-86). The subcommands are shc doctor run and shc doctor ls; the flags are --fix and --category/-c; the HTTP surface is GET /api/doctor/diagnoses plus POST /api/doctor/run with an optional {"category","fix"} body (router.go:28-32). There is no watch mode and no dashboard — shc doctor run is a one-shot sweep that prints and exits. Two small truths the page shows on purpose: the bridged module probes carry a name and a category but no description, because the bridge copies neither; and doctor.self reports warn even on a fully wired daemon, because the MkdirAll and compose-ps seams keep their defaults there and it says so at info.