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>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
// Shared visual language for the /headscale panels, matching the /soulseek grouped views: almost-black
|
||||
// Shared visual language for the /offscale panels, matching the /soulseek grouped views: almost-black
|
||||
// cards on hairline white borders. Kept local to the app so the look changes in one place.
|
||||
|
||||
export const Card = ({ children }: { children: ReactNode }) => (
|
||||
|
||||
+5
-1
@@ -19,7 +19,11 @@ import { Button } from './Cards';
|
||||
// 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.
|
||||
|
||||
const consoleSessionId = (serverId: number) => `headscale-console-${serverId}`;
|
||||
// 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">
|
||||
|
||||
+2
-2
@@ -4,10 +4,10 @@ import { Server, Laptop, Users, KeyRound, Smartphone, ShieldCheck, Activity, Ter
|
||||
import { OFFSCALE_SECTIONS, offscaleSectionPath, type OffscaleSectionId } from './shared';
|
||||
import { useOffscaleServers } from './useOffscaleServers';
|
||||
|
||||
// Lower-left panel of the /headscale workspace: the section list. Which server it all acts on is the panel
|
||||
// Lower-left panel of the /offscale workspace: the section list. Which server it all acts on is the panel
|
||||
// above (OffscaleServerPicker) — that one mutates, this one navigates, which is why they are separate.
|
||||
//
|
||||
// Sections are real links to /headscale/<section>, not channel writes — so they cmd-click into a new tab,
|
||||
// Sections are real links to /offscale/<section>, not channel writes — so they cmd-click into a new tab,
|
||||
// survive a reload, and answer the back button. Active state comes from react-router's NavLink rather than
|
||||
// being derived in JS, per the navigation audit's Phase 4.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Link } from 'react-router';
|
||||
import { offscaleSectionPath } from './shared';
|
||||
import { useOffscaleServers } from './useOffscaleServers';
|
||||
|
||||
// Top-left panel of the /headscale workspace: which server everything else acts on.
|
||||
// Top-left panel of the /offscale workspace: which server everything else acts on.
|
||||
//
|
||||
// It is its own panel rather than a block inside OffscaleNav because the two answer different questions —
|
||||
// "which server" and "which section" — and only one of them is navigation. Activating a server is a mutation
|
||||
@@ -23,7 +23,7 @@ export const OffscaleServerPicker = () => {
|
||||
<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-sm font-semibold leading-tight">Offscale</div>
|
||||
<div className="truncate text-xs text-muted-foreground">{active ? active.name : 'no server'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PolicyView } from './PolicyView';
|
||||
import { DiagnosticsView } from './DiagnosticsView';
|
||||
import { ConsoleView } from './ConsoleView';
|
||||
|
||||
// Right panel of the /headscale workspace — renders the section named by the URL.
|
||||
// Right panel of the /offscale workspace — renders the section named by the URL.
|
||||
//
|
||||
// Every section except `servers` acts on whichever server is active; each handles the "none selected" case
|
||||
// itself through ViewShell, so there is no gating to do here.
|
||||
|
||||
+3
-3
@@ -12,15 +12,15 @@ import { OffscaleViewHeader } from './OffscaleViewHeader';
|
||||
export const appRegistryMetas: AppRegistryMeta[] = [
|
||||
{
|
||||
key: 'offscale-servers',
|
||||
name: 'Headscale servers',
|
||||
name: 'Offscale servers',
|
||||
icon: Network,
|
||||
component: OffscaleServerPicker,
|
||||
availableOnPanel: false,
|
||||
},
|
||||
{ key: 'offscale-nav', name: 'Headscale', icon: PanelLeft, component: OffscaleNav, availableOnPanel: false },
|
||||
{ key: 'offscale-nav', name: 'Offscale', icon: PanelLeft, component: OffscaleNav, availableOnPanel: false },
|
||||
{
|
||||
key: 'offscale-view',
|
||||
name: 'Headscale',
|
||||
name: 'Offscale',
|
||||
icon: LayoutGrid,
|
||||
component: OffscaleView,
|
||||
header: OffscaleViewHeader,
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
// Shared types/constants for the /headscale workspace panels. Everything here mirrors the wire shapes the
|
||||
// Shared types/constants for the /offscale workspace panels. Everything here mirrors the wire shapes the
|
||||
// officer-offscale sidecar returns under /api/offscale/_officer/* — deliberately NOT Headscale's own API
|
||||
// shapes. The sidecar absorbs Headscale's quirks (uint64-as-string ids, zero-date sentinels, the version
|
||||
// floor), so these types are stable across Headscale releases and the browser never learns the upstream
|
||||
@@ -17,7 +17,7 @@ export const OFFSCALE_SECTIONS = [
|
||||
|
||||
export type OffscaleSectionId = (typeof OFFSCALE_SECTIONS)[number]['id'];
|
||||
|
||||
/** Where /headscale lands, and where an unrecognised section redirects to. */
|
||||
/** Where /offscale lands, and where an unrecognised section redirects to. */
|
||||
export const DEFAULT_OFFSCALE_SECTION: OffscaleSectionId = 'servers';
|
||||
|
||||
export const isOffscaleSection = (value: string | undefined): value is OffscaleSectionId =>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import type { OffscaleServer, OffscaleHealth, OffscaleSshTest } from './shared';
|
||||
|
||||
// The registered-servers cache. Every panel in the /headscale workspace reads this one query, so switching
|
||||
// The registered-servers cache. Every panel in the /offscale workspace reads this one query, so switching
|
||||
// the active server anywhere updates the whole screen at once.
|
||||
//
|
||||
// Registration is validated server-side before anything is saved (reachable, >=0.29, key accepted), which
|
||||
|
||||
Reference in New Issue
Block a user