Three findings from granting Files to a role and signing in as the member.
THE BLANK SCREEN. WorkspaceView returns null until workspace.isLoaded, and isLoaded
was the success flag of GET /api/dashboards — which the `dashboards` capability gated.
So a member with files granted got a completely blank Files screen and no request to
/api/file-browser at all: the panel never mounted. Terminal, Chat and every other
workspace screen were the same.
/api/dashboards is not a feature. It is the per-user key-value store where every
screen keeps its layout, entirely `personal`, every row keyed to the caller. Gating it
does not restrict an account, it breaks it — which is the definition of `core` at the
top of the registry. Moved there.
And the failure mode was wrong independently: `isLoaded` now covers a failed fetch as
well as a successful one, with `loadFailed` for the difference, so a screen that cannot
remember its layout still renders with defaults instead of showing nothing and
explaining nothing.
THE STRAY REQUESTS. Six shell-level queries gated on isAuthenticated but not on
capability, so a member's first paint fired 403s at /server-settings/settings,
/jobs/counts (every three seconds, forever), /chat/models, /plans, /music/now-playing
and the chat access policy. Each now checks the capability it needs. JobsIndicator and
RescanButton also render nothing without `tasks` and `items` — the header was offering
two links to a screen the member cannot open and a button that would 403.
THE PERMISSIONS SCREEN. It listed all fourteen app capabilities on a server where none
of their sidecars are installed. Offering to grant Photos on a machine with no Immich
is not a permission decision. It now shows only what is installed, lists the rest as
"nothing installed for these yet" so their absence reads as a fact rather than a bug,
and marks confined rows as needing a Linux account. Fails open on a degraded read.
Found while checking that: the headscale catalogue entry claimed only the `headscale`
capability, but the same sidecar also serves `vpn` — a member enrolling their own
device — so vpn was never subtracted. Hence `alsoServes`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>