offscale is a plugin
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>
This commit is contained in:
@@ -57,7 +57,16 @@ async function readDisks() {
|
||||
const { stdout } = await exec('df', [
|
||||
'-B1',
|
||||
'--output=target,fstype,size,used,pcent',
|
||||
'-x', 'tmpfs', '-x', 'devtmpfs', '-x', 'squashfs', '-x', 'overlay', '-x', 'efivarfs',
|
||||
'-x',
|
||||
'tmpfs',
|
||||
'-x',
|
||||
'devtmpfs',
|
||||
'-x',
|
||||
'squashfs',
|
||||
'-x',
|
||||
'overlay',
|
||||
'-x',
|
||||
'efivarfs',
|
||||
]);
|
||||
return stdout
|
||||
.trim()
|
||||
@@ -131,7 +140,9 @@ async function readTemps() {
|
||||
}
|
||||
const CPU_DRIVERS = ['k10temp', 'zenpower', 'coretemp', 'k8temp', 'cpu_thermal'];
|
||||
const cpu =
|
||||
sensors.find((s) => CPU_DRIVERS.includes(s.name.toLowerCase()) && /tctl|tdie|package|composite|core 0/i.test(s.label)) ??
|
||||
sensors.find(
|
||||
(s) => CPU_DRIVERS.includes(s.name.toLowerCase()) && /tctl|tdie|package|composite|core 0/i.test(s.label),
|
||||
) ??
|
||||
sensors.find((s) => CPU_DRIVERS.includes(s.name.toLowerCase())) ??
|
||||
null;
|
||||
return { cpuC: cpu?.celsius ?? null, cpuLabel: cpu ? `${cpu.name} · ${cpu.label}` : null, sensors };
|
||||
@@ -151,15 +162,18 @@ async function readGpu() {
|
||||
const busyRaw = await readFile(`${dev}/gpu_busy_percent`, 'utf8').catch(() => null);
|
||||
if (busyRaw == null) continue;
|
||||
const busyPct = Number.parseInt(busyRaw.trim(), 10);
|
||||
const vramUsed = Number.parseInt((await readFile(`${dev}/mem_info_vram_used`, 'utf8').catch(() => '0')).trim(), 10) || 0;
|
||||
const vramTotal = Number.parseInt((await readFile(`${dev}/mem_info_vram_total`, 'utf8').catch(() => '0')).trim(), 10) || 0;
|
||||
const vramUsed =
|
||||
Number.parseInt((await readFile(`${dev}/mem_info_vram_used`, 'utf8').catch(() => '0')).trim(), 10) || 0;
|
||||
const vramTotal =
|
||||
Number.parseInt((await readFile(`${dev}/mem_info_vram_total`, 'utf8').catch(() => '0')).trim(), 10) || 0;
|
||||
return { busyPct: Number.isFinite(busyPct) ? busyPct : 0, vramUsedBytes: vramUsed, vramTotalBytes: vramTotal };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Network throughput — bytes/sec computed from the delta since the previous /stats call (~2s window).
|
||||
let lastNet: { total: { rx: number; tx: number }; per: Record<string, { rx: number; tx: number }>; ts: number } | null = null;
|
||||
let lastNet: { total: { rx: number; tx: number }; per: Record<string, { rx: number; tx: number }>; ts: number } | null =
|
||||
null;
|
||||
async function readNet() {
|
||||
const now = Date.now();
|
||||
let data: string;
|
||||
@@ -185,11 +199,20 @@ async function readNet() {
|
||||
}
|
||||
const prev = lastNet;
|
||||
lastNet = { total: { rx: totRx, tx: totTx }, per, ts: now };
|
||||
if (!prev || now <= prev.ts) return { rxBytesPerSec: 0, txBytesPerSec: 0, interfaces: [] as { name: string; rxBytesPerSec: number; txBytesPerSec: number }[] };
|
||||
if (!prev || now <= prev.ts)
|
||||
return {
|
||||
rxBytesPerSec: 0,
|
||||
txBytesPerSec: 0,
|
||||
interfaces: [] as { name: string; rxBytesPerSec: number; txBytesPerSec: number }[],
|
||||
};
|
||||
const dt = (now - prev.ts) / 1000;
|
||||
const rate = (cur: number, old: number) => Math.max(0, Math.round((cur - old) / dt));
|
||||
const interfaces = Object.entries(per)
|
||||
.map(([name, v]) => ({ name, rxBytesPerSec: rate(v.rx, prev.per[name]?.rx ?? v.rx), txBytesPerSec: rate(v.tx, prev.per[name]?.tx ?? v.tx) }))
|
||||
.map(([name, v]) => ({
|
||||
name,
|
||||
rxBytesPerSec: rate(v.rx, prev.per[name]?.rx ?? v.rx),
|
||||
txBytesPerSec: rate(v.tx, prev.per[name]?.tx ?? v.tx),
|
||||
}))
|
||||
.filter((i) => i.rxBytesPerSec > 0 || i.txBytesPerSec > 0)
|
||||
.sort((a, b) => b.rxBytesPerSec + b.txBytesPerSec - (a.rxBytesPerSec + a.txBytesPerSec));
|
||||
return { rxBytesPerSec: rate(totRx, prev.total.rx), txBytesPerSec: rate(totTx, prev.total.tx), interfaces };
|
||||
@@ -215,7 +238,9 @@ async function readPower() {
|
||||
for (const d of await readdir('/sys/class/hwmon')) {
|
||||
const base = `/sys/class/hwmon/${d}`;
|
||||
if ((await readFile(`${base}/name`, 'utf8').catch(() => '')).trim() !== 'amdgpu') continue;
|
||||
const p = (await readFile(`${base}/power1_average`, 'utf8').catch(() => null)) ?? (await readFile(`${base}/power1_input`, 'utf8').catch(() => null));
|
||||
const p =
|
||||
(await readFile(`${base}/power1_average`, 'utf8').catch(() => null)) ??
|
||||
(await readFile(`${base}/power1_input`, 'utf8').catch(() => null));
|
||||
if (p != null) {
|
||||
const uw = Number.parseInt(p.trim(), 10);
|
||||
if (Number.isFinite(uw)) gpuWatts = Math.round((uw / 1e6) * 10) / 10;
|
||||
@@ -233,8 +258,7 @@ systemMonitorRouter.get('/stats', async (ctx) => {
|
||||
await new Promise((r) => setTimeout(r, 120));
|
||||
const second = await readCpuSample().catch(() => null);
|
||||
|
||||
const cpuUsage =
|
||||
first && second ? pct(second.busy - first.busy, second.total - first.total) : 0;
|
||||
const cpuUsage = first && second ? pct(second.busy - first.busy, second.total - first.total) : 0;
|
||||
const perCore =
|
||||
first && second
|
||||
? second.perCore.map((c, i) => {
|
||||
@@ -311,7 +335,9 @@ systemMonitorRouter.get('/pm2', async (ctx) => {
|
||||
// GET /docker — running docker containers (the "dockers" scope).
|
||||
systemMonitorRouter.get('/docker', async (ctx) => {
|
||||
try {
|
||||
const { stdout } = await exec('docker', ['ps', '--no-trunc', '--format', '{{json .}}'], { maxBuffer: 8 * 1024 * 1024 });
|
||||
const { stdout } = await exec('docker', ['ps', '--no-trunc', '--format', '{{json .}}'], {
|
||||
maxBuffer: 8 * 1024 * 1024,
|
||||
});
|
||||
return ctx.json({
|
||||
containers: stdout
|
||||
.trim()
|
||||
|
||||
Reference in New Issue
Block a user