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>
101 lines
4.4 KiB
TypeScript
101 lines
4.4 KiB
TypeScript
import { useCallback } from 'react';
|
|
import { Link } from 'react-router';
|
|
import { Loader2, TerminalSquare } from 'lucide-react';
|
|
import { offscaleSectionPath } from './shared';
|
|
import { useOffscaleServers } from './useOffscaleServers';
|
|
import { TerminalView } from 'officerdev';
|
|
import { Button } from './Cards';
|
|
|
|
// A shell on the machine behind the active Headscale server — the escape hatch for everything the API cannot
|
|
// answer (why headscale won't start, what the logs say, whether the disk is full).
|
|
//
|
|
// It is deliberately the SAME terminal every other panel uses, driven by nothing more than `ssh <host>` typed
|
|
// into a login shell. Officer holds no key, no password and no port: whatever `ssh` on this box can already
|
|
// reach, this can reach, and nothing more. If the connection needs a jump host or an odd port, that belongs in
|
|
// `~/.ssh/config` as a Host alias — which this field accepts by name.
|
|
//
|
|
// The session id is derived from the server id rather than minted per panel, so re-opening the Console lands
|
|
// back in the shell that is already running and mid-command, and switching servers is a different shell rather
|
|
// than the same one re-purposed. TerminalView suppresses its initial input when the sidecar replays a buffer,
|
|
// which is what stops a re-attach from typing a second `ssh` inside the first.
|
|
|
|
// Renamed from `headscale-console-…` on 2026-08-16. This is a pty session KEY, not a label, so the rename
|
|
// has a real one-time 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 indefinitely.
|
|
const consoleSessionId = (serverId: number) => `offscale-console-${serverId}`;
|
|
|
|
const Centred = ({ children }: { children: React.ReactNode }) => (
|
|
<div className="flex h-full flex-col items-center justify-center gap-3 p-6 text-center">
|
|
<div className="flex h-14 w-14 items-center justify-center rounded-2xl bg-white/5 text-zinc-400">
|
|
<TerminalSquare className="h-6 w-6" />
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
|
|
export const ConsoleView = () => {
|
|
const { active, isLoading } = useOffscaleServers();
|
|
|
|
// The terminal reports its connection state; nothing in this section acts on it yet, but TerminalView wants
|
|
// a stable callback and an inline arrow would remount its effect on every render.
|
|
const onConnectionChange = useCallback(() => {}, []);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex h-full items-center justify-center text-sm text-zinc-500">
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Loading servers…
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!active) {
|
|
return (
|
|
<Centred>
|
|
<div>
|
|
<div className="text-base font-semibold text-zinc-100">No server selected</div>
|
|
<p className="mt-1 text-sm text-zinc-500">Pick one in the Servers section to open its console.</p>
|
|
</div>
|
|
</Centred>
|
|
);
|
|
}
|
|
|
|
if (!active.sshHost) {
|
|
return (
|
|
<Centred>
|
|
<div>
|
|
<div className="text-base font-semibold text-zinc-100">No SSH address for {active.name}</div>
|
|
<p className="mt-1 max-w-sm text-sm text-zinc-500">
|
|
Add one on the server to open a shell on the machine behind it. Use the machine's own address rather than
|
|
the Headscale hostname — the console is most useful exactly when that name has stopped answering.
|
|
</p>
|
|
</div>
|
|
<Link to={offscaleSectionPath('servers')}>
|
|
<Button variant="primary">Go to Servers</Button>
|
|
</Link>
|
|
</Centred>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-full flex-col">
|
|
<div className="flex shrink-0 items-center gap-2 border-b border-white/10 px-3 py-2 text-[11px] text-zinc-500">
|
|
<TerminalSquare className="h-3.5 w-3.5" />
|
|
<span className="truncate">
|
|
ssh <span className="font-mono text-zinc-300">{active.sshHost}</span> · {active.name}
|
|
</span>
|
|
</div>
|
|
<TerminalView
|
|
// Remount on a server switch: the session id is a mount-time argument, so without this the panel would
|
|
// keep showing the previous server's shell under the new server's name.
|
|
key={active.id}
|
|
className="min-h-0 flex-1 p-2"
|
|
sessionId={consoleSessionId(active.id)}
|
|
initialInput={`ssh ${active.sshHost}`}
|
|
onConnectionChange={onConnectionChange}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|