headscale leaves the platform. 45 files move to plugins/offscale/ and the
platform stops knowing it exists.
api/router.ts the thin auth-gated proxy, now at /api/offscale
sidecar/ 18 files, the whole headscale contract and its admin keys
db/ schema + queries, offscale_servers
web/ 26 files as panels and a layout — no screen, per the rule
removed from the platform: the hono mount, the `headscale` capability, the
App.tsx route pair, the screen and its barrel, the AppRegistry spread, the
officerdev re-exports, the dock tile, the page-title rule, and both database
barrels. tsgo is clean and nothing references it.
the imports tell the story of what the plugin↔host API actually is. the sidecar
takes @@/sidecar/protocol, @@/sidecar/connect, @@/data-path and
@@/officer-url.mjs; the queries take officerdb/db and officerdb/crypto; the
schema takes officerdb/auth/schema for the one reference a plugin may make; the
web half takes useClient, copyToClipboard, WorkspaceView and TerminalView from
the officerdev barrel. all of it resolves because a plugin lives inside the repo
— no publishing, no version negotiation.
AND IT FOUND A REAL BUG IN THE INSTALLER. createSidecarProxy learns its port
from a one-shot `<name>:server` event and subscribes when the plugin's router is
first imported — at mount. install started the sidecar BEFORE mounting, so the
announcement fired into a void: process online, routes mounted, every request
answering `503 sidecar not available` until something forced a reconnect. it
would have hit every plugin with an http sidecar. `example` never caught it
because it has no listener to announce.
install and enable now mount before starting; disable still unmounts before
stopping. neither direction leaves a mounted route in front of a sidecar that
cannot be reached.
verified live: /api/offscale/_officer/servers answers {"servers":[]}, /offscale
and /offscale/nodes serve, the old /api/headscale is 404, the offscale
capability is registered from the manifest, and officer-offscale is online.
757 pass, same 10 pre-existing failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 lines
2.7 KiB
TypeScript
61 lines
2.7 KiB
TypeScript
import { Check, Network, Plus } from 'lucide-react';
|
|
import { Link } from 'react-router';
|
|
import { headscaleSectionPath } from './shared';
|
|
import { useHeadscaleServers } from './useHeadscaleServers';
|
|
|
|
// Top-left panel of the /headscale workspace: which server everything else acts on.
|
|
//
|
|
// It is its own panel rather than a block inside HeadscaleNav because the two answer different questions —
|
|
// "which server" and "which section" — and only one of them is navigation. Activating a server is a mutation
|
|
// (a DB write that re-scopes every other query), so these stay buttons with no URL of their own, while the
|
|
// section list below is real links.
|
|
//
|
|
// Every registered server is listed, including when there is only one: the panel's whole job is to say what
|
|
// the rest of the screen is talking to, and a picker that hides itself at one server makes that invisible.
|
|
|
|
export const HeadscaleServerPicker = () => {
|
|
const { servers, active, activate, isLoading } = useHeadscaleServers();
|
|
|
|
return (
|
|
<div className="flex h-full flex-col overflow-y-auto bg-muted/30">
|
|
<div className="flex items-center gap-3 px-4 py-4">
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-xl bg-indigo-500/15 text-indigo-400 ring-1 ring-black/5">
|
|
<Network className="h-5 w-5" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="truncate text-sm font-semibold leading-tight">Headscale</div>
|
|
<div className="truncate text-xs text-muted-foreground">{active ? active.name : 'no server'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-0.5 px-2 pb-3">
|
|
{servers.map((server) => (
|
|
<button
|
|
key={server.id}
|
|
type="button"
|
|
onClick={() => !server.isActive && activate.mutate(server.id)}
|
|
disabled={activate.isPending}
|
|
title={server.url}
|
|
className={`flex items-center gap-2 rounded-lg px-3 py-1.5 text-left text-xs transition-colors ${
|
|
server.isActive ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground hover:bg-muted'
|
|
}`}
|
|
>
|
|
<Check className={`h-3.5 w-3.5 shrink-0 ${server.isActive ? 'text-primary' : 'opacity-0'}`} />
|
|
<span className="min-w-0 flex-1 truncate">{server.name}</span>
|
|
</button>
|
|
))}
|
|
|
|
{servers.length === 0 && !isLoading && (
|
|
<Link
|
|
to={headscaleSectionPath('servers')}
|
|
className="flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
|
|
>
|
|
<Plus className="h-3.5 w-3.5 shrink-0" />
|
|
Register a server
|
|
</Link>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|