13 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 70b4477c3b release 2026.08.27.01
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>
2026.08.27.01
2026-08-27 07:36:32 +00:00
pastilhas 67776e4123 AGENTS.md, so both harnesses find the same guidance
opencode discovers AGENTS.md and nothing else; claude code discovers CLAUDE.md.
this repo had neither, so an agent sent to change something here arrived with no
guidance at all under either.

it deliberately does NOT repeat README.md. that file says what the plugin is,
for someone deciding whether to install it. this one answers the other question:
i have to change something in here, what will i break.

the facts in it are read from the repository rather than recalled — the panel
keys from web/panels.ts, the tables from db/schema.ts, the permission keys, the
host binaries and the route claims from manifest.ts. presence is the declaration
everywhere in this system, so the file says what the platform will actually
conclude from these files existing.

CLAUDE.md beside it is a pointer, not a copy: two copies drift and the stale one
is the one somebody reads.
2026-08-27 07:24:15 +00:00
pastilhas 22076f9989 say what this plugin is for, in the store's words
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.
2026-08-27 06:49:35 +00:00
pastilhasandClaude Opus 5 a7fd6236d0 art: the splash the app now ships
The app moved to the new splash on 2026-08-20 — the hex mesh with the Officer
wordmark at its centre, the duck, and the scattered devices — while the
marketplace was still serving the previous one. The icon is re-exported from the
app's own copy at 256 so the two cannot drift apart silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 03:10:26 +01:00
pastilhasandClaude Opus 5 ad41bafd59 rename and tags fields submit on enter, not just the buttons
The node rename input and the owner/tags editor's tags field sat in a div, so
pressing Enter did nothing and the check/apply button was the only way in.
Both are now real forms with the right button wrapped as type="submit" and
their manual Enter handling removed so there's no double-submit; Escape
handling stays since a form has no native equivalent for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:44:02 +00:00
Claude 80f952a9a6 art: the splash, beside the icon
Copied from its existing master — this art was already drawn to the current
brief, it simply had nowhere to live outside a designer's folder. The pair is
the point: the icon is a reduction of the splash's world.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:50:59 +01:00
Claude 2a87f0b4bc icon: transparent corners, so the tile is a tile
The app icon is flattened onto black because iOS rejects an alpha channel, and
copying that straight over made every dock tile an opaque black square. The tile
wants the frame's own rounded edge with the corners cut away — which is what the
icon this replaces already had, and what tile: bare means.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:28:41 +01:00
Claude dfb06af3a3 icon: the app's own artwork, at 256
The dock tile takes assets/icon.png when the plugin ships one and falls back to
the manifest's lucide glyph when it does not. This is the same icon the phone
shows, resized — one mark per product, wherever it appears.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 06:04:05 +01:00
pastilhas 58fbf12e4b the picker's register link opens the form
reported as "I click register a server and nothing happens", and nothing was broken. three controls on
that screen carry the label. two are buttons that open the form; the one in the server picker — top left,
where you look first — was a plain <Link> to the servers section.

/offscale and /offscale/servers render the same view (useOffscaleSection defaults to servers when there is
no param), so from either one the click navigated to something identical. the URL moved and nothing else
did, and the only visible result was a second button with the same label further down the page.

so the link now carries ?register=1 and ServersView consumes it. the flag is stripped with replace on the
way in, which is not tidiness: left in the URL, closing the form and clicking the picker again would be a
navigation to the URL already showing, and nothing would happen — the same bug, restored.

ServersView.test.tsx pins all three: the form opens on arrival, the flag is consumed, and no flag means no
form. removing the effect fails the first two.
2026-08-17 12:58:47 +00:00
pastilhasandClaude Opus 5 373d67e0a0 finish the rebrand: the parts a user reads, and the manifest that was wrong
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>
2026-08-16 04:23:28 +00:00
pastilhas fa81f05206 readme 2026-08-16 04:09:19 +00:00
pastilhas 95b84ea748 rebrand to OffScale, and fix what the first extraction missed
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.
2026-08-15 18:41:52 +00:00
pastilhasandClaude Opus 5 8a446bb4b5 offscale, extracted from the platform into its own repository
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>
2026-08-15 18:12:57 +00:00