first dated version. plugins were all sitting on 1.0.0, which said nothing about
when a build was made or which of two copies is newer — the platform already
uses YYYY.MM.DD.NN and this follows it.
the version lives in manifest.ts because a plugin has no package.json; that is
the field the marketplace publishes and the installer compares.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tags are how the marketplace is browsed rather than scrolled: a list of them on
the left, the plugins that carry one on the right. a closed vocabulary
(PLUGIN_TAGS in the platform) because free text becomes homelab, home-lab, Home
Lab and selfhosted inside a month, and a filter list of synonyms is worse than
no filter at all.
declared here rather than curated in the catalogue, so a plugin can say what it
is and nobody edits a table when one ships. the marketplace caches it on sync,
exactly as it already does for label and summary.
The extraction landed months ago and the platform side is clean. What was left was
inside the plugin, and it was the half that shows.
── Names a user actually reads ──
The three registered panels were still called "Headscale servers", "Headscale" and
"Headscale" in the panel picker, and the server picker rendered "Headscale" as its
own heading. Those name OUR panels, not the upstream server, so they were simply
missed.
Deliberately NOT a find-and-replace. Most of the ~175 "headscale" strings in here
are correct — "no active Headscale server", "headscale timed out", "is this a
Headscale server?" all refer to the thing this plugin drives, and renaming them
would make the errors lie. What was fixed is the plugin's own identity, plus nine
stale `/headscale` route paths that are wrong rather than merely old (the route is
`/offscale`) and one reference to `queries/headscale.ts`, a file the extraction
deleted.
The console's pty session key moves from `headscale-console-<id>` to
`offscale-console-<id>`. That one has a real cost: a console open at the moment of
the change keeps running in the sidecar under the old key with nothing pointing at
it, until that sidecar restarts. Paid once, deliberately, rather than carrying the
pre-extraction name in persisted state forever.
── The manifest was missing a dependency it has always had ──
`sidecar/ssh.ts` spawns `ssh`, and so does the reachability probe. The manifest
said "Offscale needed nothing", so install checked for nothing and a host without
an ssh client failed at RUNTIME with a spawn error — precisely the failure
`osDependencies` exists to turn into a refusal at install time. PLUGIN.md listed
ssh as a host dependency all along; the manifest was the file that was wrong.
Missed because ssh is on effectively every Linux box, which is also why declaring
it costs nothing there and is the whole difference where it is absent.
── Documentation that said the opposite of the code ──
PLUGIN.md instructed the reader to carry `assist.ts` in "but do not wire it up",
as a marker. It has been wired for some time: `policy.ts` routes
`POST /_officer/policy/assist`, `PolicyView.tsx` renders `<PolicyAssistant />`, and
the manifest declares the route in `readOnlyWrites`. Corrected rather than deleted,
because that instruction is exactly the kind someone would follow — a person
tidying "unused" code would have found it wired and assumed the document knew
something they did not.
Present-tense identifiers corrected to the shipped names; the "before" columns of
the migration tables still say headscale, because that is what they record. A note
at the top says which is which.
tsgo clean, 21 plugin tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Offscale was the first plugin extracted and it was done before we knew what
"extracted" meant. Music, done last, is the standard. This brings offscale to it.
── The rebrand ──
The plugin was `offscale` to the platform and `headscale` to itself: sidecar
name and handles, the port announcement, the API proxy name, the React
components, every hook, the react-query keys, the panel ids and appTypes, and
the Postgres table. Now all of those say offscale.
The line drawn, and it is deliberate: OffScale is Officer's tooling layer, and
Headscale is the server it manages. So every IDENTIFIER is offscale, while a
message like `headscale unreachable`, the `headscale apikeys create` hint and the
ACL assistant's prompt still say Headscale — because they are talking about the
remote server, and renaming them would make the code lie about what it reached.
495 occurrences became 180, and the 180 are all of that second kind.
── The live bug this uncovered ──
`headscaleSectionPath` built links to `/headscale/<section>`. The shell has no
such route — plugin routes come from `plugin.route`, which is `/offscale` — and
it redirects unknown paths to the home page. So every section link in the nav,
the console and the server picker silently went home. The extraction moved the
route and left the link builder behind.
Also live: ServersView told the user to run
`pm2 start ecosystem.config.cjs --only officer-headscale`, a process that has not
existed since the sidecar was renamed.
── The correctness fix music already had ──
api/router.ts hardcoded `prefix: '/api/offscale'`. The proxy strips
`prefix.length` characters, so a literal is correct only for a first-party
publisher; published by anyone else this mounts at `/api/p/<publisher>/offscale`
and forwards the wrong subpath. Derived from `mountPrefix()` now, as music does.
── The rest ──
- assets/icon.png — the OffScale artwork, 256px to match music's. The tile stops
being a glyph badge.
- First tests: 21 of them, over the version floor and the protobuf normalisers.
Those are the two places a Headscale release actually breaks this, and they had
no coverage at all. `meetsFloor` has a real trap pinned now — comparing minor
first would refuse 1.0 as older than 0.29.
- OFFSCALE_API.md — the contract was a 45-line comment inside sidecar/index.ts,
which is not linkable and not published. Now a document, as MUSIC_API.md is.
- web/panels.ts re-exported three components. A plugin cannot export components;
that was residue of the platform importing them before extraction.
- Comments pointed at src/servers/api/headscale/ and src/servers/sidecar/headscale/,
neither of which has existed since the extraction.
The crypto purpose moved headscale → offscale too, and the secret-store row was
renamed rather than left to create a fresh key — the material is preserved, so
this is reversible. Free to do only because offscale_servers had 0 rows; with one
stored API key it would have been a migration.
The tailnet plugin — machines, users, pre-auth keys, access policy and device
invites. Moved out of officerdev/platform, where it had lived in plugins/ since
the plugin system was built.
Until now this code existed in exactly one place: the platform repository. That
made "gitignore the plugins directory" impossible to do safely, because
untracking it would have left 49 files on a single disk with no remote. This
repository is what makes that move safe.
Same extraction as plugins/music before it: source only, no history. The
platform's history still holds every commit that shaped this, and the SHAs cited
across the codebase keep resolving — replaying it here would have created a
second, divergent account of the same work.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>