the platform stops explaining what Headscale is

Correcting my own reasoning from the last commit. I left "Headscale's API", "the
Headscale admin credential" and similar alone on the grounds that they name the
thing the plugin drives, so renaming them would make the sentences false. True as
far as it goes, and it misses the point: those sentences are in the PLATFORM. It
has no business knowing what Headscale is at all. Correct language in the wrong
repository is still a remnant.

Ten of them, each replaced with the platform's own vocabulary rather than the
plugin's domain:

- `crypto.ts` and `secret-store.ts` listed "the Headscale admin credential" among
  what one key used to protect. It is "the tailnet's admin credential" — the
  purpose, which is a word this repository owns.
- `catalogue.ts` and `availability.ts` explained `alsoServes` — a LIVE mechanism —
  entirely through a Headscale example that left the repository. Restated as the
  general shape, with one parenthesis saying which case produced it.
- The photos types file explained another plugin's sidecar.
- `App.tsx` cited `/headscale/:section` as the example of a route pair; that route
  does not exist. `/photos/:section` does.
- `plugins/router.ts` pointed at `useCompanionLogStream`, a hook that lives in a
  plugin now.
- `registry.ts` said in the present tense that `headscale` is `admin` — the
  permission is the plugin's and it declares its own, so a member-facing enrolment
  surface would be the PLUGIN's to add, not this file's.
- `proxy.sh` cited `headscale/schema.ts` as a style reference. That file is not in
  this repository.
- machine-setup said "Its Headscale app" — the app is the Offscale plugin.

`machine-setup/lib/tailscale.sh` keeps all ten of its mentions: it exists to
explain tailscale, headscale and offscale to somebody choosing between them, on a
core install, before any plugin exists. That is machine setup's subject, not the
plugin's domain leaking upward.

What survives elsewhere is only dated history — "was here until 2026-08-15", "It
was: music, vault, slskd, headscale, …" — plus the notes about the real
`headscale_servers` orphan and the secret-store key removed yesterday. Those record
that something called headscale used to be here, which is a different claim from
explaining what it does.

tsgo clean, frontend builds, 873 pass / 7 fail unchanged, setup scripts parse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 11:33:28 +00:00
co-authored by Claude Opus 5
parent 27aa72d923
commit 0230e62a71
10 changed files with 17 additions and 16 deletions
+1 -1
View File
@@ -165,7 +165,7 @@ tailscale_networks_help() {
echo " before anything that can lock you out, so there is always a second"
echo " way in."
echo ""
echo " Officer also administers it. Its Headscale app talks to headscale"
echo " Officer also administers it. Its Offscale plugin talks to headscale"
echo " and offscale servers alike: register as many as you run, see which"
echo " are actually up — each is probed, not remembered — and switch"
echo " between them. On whichever is active you get the nodes, the users,"
+1 -1
View File
@@ -327,7 +327,7 @@ proxy_claim_admin() {
# Deliberately NOT written to a file. The platform's shape is that
# secrets/officer-keys.db holds ENCRYPTION KEYS, one per purpose, and the credential
# itself lives encrypted in Postgres. A third plaintext location is the pattern
# headscale/schema.ts calls "debt to avoid copying, not a precedent to follow".
# the platform elsewhere calls "debt to avoid copying, not a precedent to follow".
#
# Nothing programmatic needs this after setup — only a human logging into the admin
# UI — so not storing it is a legitimate outcome rather than a gap.
+1 -1
View File
@@ -95,7 +95,7 @@ export function App() {
hands the whole subtree to the plugin's own router, which react-router nests natively. */}
{installedPlugins.flatMap((plugin) => [
<Route key={plugin.appName} path={plugin.route} element={<Dashboard.PluginScreen {...plugin} />} />,
// The section pair, exactly as the core screens do it (`/headscale/:section`): the plugin's
// The section pair, exactly as the core screens do it (`/photos/:section`): the plugin's
// panels read `useParams` themselves, so which section is open is the URL rather than state
// passed between them.
<Route
+1 -1
View File
@@ -9,7 +9,7 @@ import { getKey } from './secret-store';
// ── One key per purpose ──
//
// This took a single VAULT_STORE_KEY from the environment until 2026-08-13. That key encrypted seven
// unrelated things — the Headscale admin credential, the wallet seed, Vaultwarden's token set, Jellyfin,
// unrelated things — the tailnet's admin credential, the wallet seed, Vaultwarden's token set, Jellyfin,
// Immich, InvoiceShelf, and every app-store upstream secret — so one leak opened all of them, and its
// name pointed at whichever plugin happened to need it first.
//
+1 -1
View File
@@ -20,7 +20,7 @@ import { join, resolve } from 'node:path';
//
// ── One key per purpose, not one key for everything ──
//
// This replaced a single VAULT_STORE_KEY that encrypted seven unrelated things — the Headscale admin
// This replaced a single VAULT_STORE_KEY that encrypted seven unrelated things — the tailnet's admin
// credential, the wallet seed, Jellyfin tokens, Immich, InvoiceShelf, Vaultwarden's token set and every
// app-store upstream secret. One leaked key opened all of them, and the name pointed at whichever plugin
// happened to need it first, so nobody reading it could tell what changing it would destroy.
+1 -1
View File
@@ -211,7 +211,7 @@ pluginsRouter.post('/:appName/disable', async (ctx) => {
//
// POST rather than GET, so `EventSource` cannot be used: it sends no `Authorization` header, and these
// routes are owner-only. The client reads the body and parses frames itself, which is exactly what
// `useCompanionLogStream` already does for the headscale container logs.
// a plugin's own log-stream hooks already do.
// `userId` is the caller, threaded through so install can pin the new tile to THEIR dock rather than
// guessing at the owner. Only install uses it; the others take it and ignore it so the map stays one shape.
+3 -3
View File
@@ -24,9 +24,9 @@ import { CATALOGUE, type CatalogueEntry } from './catalogue';
/**
* Permission key → the sidecar that has to be installed for it to mean anything.
*
* Includes `alsoServes`, because one sidecar can back more than one permission: Headscale serves both the
* owner's tailnet administration and a member enrolling their own device, and only listing the first left the
* second looking available on a machine that had no Headscale.
* Includes `alsoServes`, because one sidecar can back more than one permission — an owner-only admin
* surface and a member-facing one, say. Listing only the first left the second looking available on a
* machine where the sidecar was not installed at all.
*/
const PERMISSION_TO_SIDECAR = new Map(
CATALOGUE.flatMap((entry) =>
+4 -4
View File
@@ -118,10 +118,10 @@ export type CatalogueEntry = {
/**
* Other permissions that stop working when this sidecar is absent, for availability only.
*
* Headscale is the case: one sidecar serves both `headscale` (administering the tailnet, owner-only) and
* `vpn` (a member enrolling their own device). With only `permission` to go on, `vpn` was never subtracted,
* so the Permissions screen offered it on a machine with no Headscale at all — a grant that would have
* produced a refusal the owner could not account for.
* The case that produced it: one sidecar served both an owner-only admin permission and a member-facing
* one. With only `permission` to go on, the second was never subtracted, so the Permissions screen offered
* it on a machine where that sidecar was not installed — a grant that would have produced a refusal the
* owner could not account for. (It was the tailnet, which is a plugin now, but the shape is general.)
*/
alsoServes?: string[];
/**
+3 -2
View File
@@ -243,8 +243,9 @@ const CORE_REGISTRY: Permission[] = [
// `${invite.base}/api/v1/enroll/claim`, which is a different component entirely and does not involve
// Officer. Confirmed against the mobile monorepo and the companion repo before removal.
//
// What this does cost: `headscale` is `admin`, so with `vpn` gone no member-grantable headscale surface
// remains. Reintroduce one here if members ever need to enrol their own devices through Officer.
// What this cost: the tailnet's own permission was `admin`, so with `vpn` gone no member-grantable
// tailnet surface remained. That permission has since left with the offscale plugin, which declares its
// own — so a member-facing enrolment surface would be the PLUGIN's to add, not this file's.
// Core, not app — and this was a real defect, not a preference. `/api/dashboards` is not a feature, it is
// the per-user key-value store where EVERY workspace screen keeps its layout (`screens/files`,
// `ws-layout-*`, panel config). `WorkspaceView` renders nothing until that store has loaded, so gating it
@@ -1,6 +1,6 @@
// Shared types, constants and pure helpers for the /photos workspace panels.
//
// Unlike the offscale plugin's panels — whose sidecar reshapes Headscale's API into Officer's own types — the
// Unlike a plugin whose sidecar reshapes an upstream API into Officer's own types — offscale does — the
// photos sidecar is a deliberate pass-through, so these types mirror Immich's wire shapes directly. They were
// generated from the LIVE instance's OpenAPI document (`GET /api/spec.json`, Immich 3.0.3), not from memory:
// several of them changed shape in 3.0 (`isArchived` became `visibility`, and the timeline bucket response