ProfileSettings reaches zero: api keys, profile, voice, dock, languages and the
model defaults.
Language names now come from Intl.DisplayNames rather than the catalog. The runtime
already knows "German" in English and "alemao" in Portuguese, so 28 names would have
been 56 hand-written entries that Intl gets right for free -- including for locales
not shipped yet. Same reasoning D7 gives for dates and numbers. The tag LIST stays
hardcoded: which languages you may claim to speak is a product choice.
ApiKeys had toLocaleDateString(undefined, ...), which followed the BROWSER locale --
the exact contradiction D7 describes. It uses useFormat().date now. Its revoked-key
toggle hand-rolled a plural with a ternary on n === 1; that is a real plural entry now.
Scanner: an Authorization header value like "Bearer <token>" is not a sentence.
Excluded by auth-scheme prefix; verified the delta is exactly the four such
credentials across both remaining areas.
Layout reaches zero. The dock, header, user menu, theme switcher, spotlight,
shortcuts modal, screen-error fallback, rescan and bug report.
Three things worth knowing:
CORE_DOCK_ITEMS now carries labelKey instead of label, and one useDockItems() hook
resolves the composed list. DashboardLayout, Spotlight and DockSettings each spread
the same two sources before, so each would have had to resolve labels itself.
Plugin tiles keep the name from their own manifest — a plugin's strings are not ours.
Spotlight resolves labels INSIDE the hook, so rank() still matches what the reader
can see rather than a dot-path. Its to: '/settings/...' literals are untouched, which
is what docs:check parses — verified still 20 offered, 0 missing.
The rescan toast said 'N skill(s)'. That is English giving up on a plural, so it is
now a real plural entry per noun and reads correctly at 0, 1 and 2 in both locales.
ThemeSwitcher used t as its theme loop variable; renamed to entry.
Installing Music left the dock unchanged. The tile existed server-side the
moment the plugin was enabled, and the only way to see it was Profile → Dock,
by hand, after a page refresh. Installing something is already the decision to
use it; saying so twice is the bug.
Three separate causes, which is why it looked like one stubborn one:
1. `dock_configs.paths` is a COMPLETE ordered list, not a diff — useDock renders
the saved list rather than everything available, so anything not named in it
is invisible. Nothing in the plugin system had ever written to that table.
installPlugin now appends the new route, for the caller (threaded through the
route) rather than a guessed owner id.
Only for a user who HAS a row. A user without one is left alone deliberately:
there is nothing to append to, and writing one would freeze today's defaults
into their account permanently. That case is (2).
2. The fallback for a user who never customised was DEFAULT_DOCK_PATHS, which is
core-only — so a plugin tile was hidden there too. `defaultDockPaths(plugins)`
is now that list plus a tile per installed plugin, and the dock and its
settings screen both call it so they cannot disagree about what "default"
means.
3. useDock holds ['DOCK'] at staleTime: Infinity and the plugin verbs invalidated
only ['plugins'] and ['self-permissions']. So even once the server wrote the
path, nothing asked again — that is the part that made a reload necessary.
Also fixes a guard I broke reaching for this: typing VERBS as
Record<string, VerbFn> widens keyof to `string`, which makes `isVerb` prove
nothing and lets an unknown verb index the map. `satisfies` instead, and there is
now a check that Verb is still the four-member union.
Uninstall deliberately does not remove the path. A stale path with no item behind
it is dropped by useDock, and keeping it means disable/enable and reinstall put
the tile back where the user had it rather than at the end.
tsgo clean, frontend builds. 787 pass / 7 fail — 6 identical to before; the 7th
is plugins/music/cliamp, which install pulled into test discovery and which the
manifest documents as parked.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The word meant four different things in this repo, not the three the offscale
doc records:
1. the permission registry → RENAMED here
2. $OFFICER_ROOT/capabilities/ items → kept; this is what capabilities are
3. sidecar routing keys → step 3, becoming `handles`
4. Lightning wallet features → kept; a domain term, and on the wire
to the mobile apps
The fourth was not in the doc and a global find-and-replace would have broken
the mobile wallet, which reads `{ kind, capabilities: Capability[] }` from the
wallet sidecar. So this renamed against an explicit file allowlist rather than
by sweeping the tree, and `CapabilityPage.tsx` — the UI for the item store, and
correctly named already — was left alone.
Moved: servers/capabilities/ → servers/permissions/, capability-gate.ts →
permission-gate.ts, users/capabilities-routes.ts → permissions-routes.ts,
hooks/useCapabilities.ts → usePermissions.ts. Identifiers follow.
Three breaks the typechecker could not see, all found by exercising it live.
The route paths moved with the prose sweep, so the server served
/user/permissions while the frontend still called /user/capabilities. A 404 on
every page load, and tsgo clean throughout.
The response FIELD moved too. `client.get<SelfPermissions>()` is an unchecked
cast, so `data.capabilities` became `undefined` at runtime with no compile
error — `can()` would have answered "no" to everything and the dock would have
emptied itself.
And the grants list was passed straight out of the database, so it arrived as
`{ role, capability, level }` while the screen read `grant.permission`. Every
role would have rendered as holding nothing. It is now mapped in the route:
the wire says `permission`, the column still says `capability`, and step 2
therefore changes nothing any client can see.
The stale react-query keys were the quiet one: two files still invalidated
['self-capabilities'] after the hook moved to ['self-permissions'], so
installing a plugin would have silently stopped refreshing the dock.
The database is untouched — `role_capabilities` and its `capability` column are
step 2, and the two call sites that cross that boundary say so in a comment.
Round-tripped the 9 live grants through the admin endpoint to prove the PUT
contract survived: 9 before, 9 after, Member's three intact.
Also reverted prettier churn on five landing-page files that a broad --write
picked up. Second time today; the lesson is not sticking.
tsgo clean. 797 tests, 787 pass, same 7 pre-existing failures — two of which
now read "path → permission" rather than "path → capability".
ALL_DOCK_ITEMS was a hardcoded list of everything, so a fresh machine offered Photos, Jellyfin,
Transmission and the rest — each leading to a screen reporting itself unavailable — and adding a sidecar
meant editing the shell. Neither survives sidecars shipping from their own repositories.
Split in two. CORE_DOCK_ITEMS is the baseline that exists on every install: chat, files, terminal, the
app's own screens, and Gitea, which is in the light profile because it fronts a remote instance.
Everything else is derived from installed sidecars' UI manifests, delivered with /capabilities.
Sent with the capability answer rather than fetched separately so the dock has ONE source. Two requests
means two moments, and a dock rendered between them shows a tile for something uninstalled or nothing
for something installed. Filtered by capability server-side too: a member is not handed the manifest of
a feature they cannot use, because "hidden in the client" is the kind of privacy that lasts until
someone opens the network tab.
Verified live. The owner — who bypasses every permission check — does not bypass this: /photos is absent
from routes and present in deniedRoutes because Photos is not installed. Flipping a row's `enabled`
makes its tile leave and return with no process touched.
Two things fell out. A manifest can declare extraTiles, because CalDAV is one sidecar presenting as
Calendar AND Contacts, and collapsing them to keep the model tidy would make the app worse. And
DEFAULT_DOCK_PATHS no longer pins /music: useDock drops a path with nothing behind it, so the default
dock came up a tile short on any machine where Music was never installed — a default that references an
optional feature is how an app looks subtly wrong on a fresh install for no stated reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>