chore: remove dead code — sidecar host-PTY path, node-pty, unused lang keys + component

The "Clusev host" terminal resolves to an SSH login now (resolve never returns kind:'host'), so the
sidecar's local-shell path was unreachable:
- docker/terminal/server.js: drop startHost(), the spec.kind==='host' branch, the node-pty require
  and HOST_SHELL/HOST_CWD constants — the sidecar only ever opens an SSH PTY.
- Dockerfile: drop the python3/make/g++ toolchain (only there to compile node-pty's native addon);
  package.json: drop the node-pty dependency. Leaner, faster image.
- compose (dev+prod): drop the now-unused TERMINAL_HOST_CWD env and the .:/workspace:ro mount.
- remove grep-confirmed-unused lang keys (settings 2FA/stub-tab keys, accounts twofa/cannot_remove,
  auth recovery_done/regenerate_confirm, common back/retry/more/loading, servers firewall/fail2ban
  unavailable variants) and the unused x-server-item Blade component.

Verified: lean sidecar rebuilds + the server terminal still connects/runs; full suite 467 pass; all
pages 200 with no raw-key leaks and no console errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-26 07:16:11 +02:00
parent e38409bf31
commit fd4f6ceb0f
16 changed files with 6 additions and 122 deletions

View File

@ -95,10 +95,6 @@ services:
environment:
TERMINAL_SIDECAR_SECRET: "${TERMINAL_SIDECAR_SECRET:-}"
APP_INTERNAL_URL: "http://app:80"
# Land the host shell in a real, writable home (~) — not the read-only repo mount.
TERMINAL_HOST_CWD: "/home/node"
volumes:
- .:/workspace:ro
expose:
- "3000"
networks:

View File

@ -110,10 +110,7 @@ services:
environment:
TERMINAL_SIDECAR_SECRET: "${TERMINAL_SIDECAR_SECRET:-}"
APP_INTERNAL_URL: "http://app:80"
# Land the host shell in a real, writable home (~) — not the read-only repo mount.
TERMINAL_HOST_CWD: "/home/node"
volumes:
- .:/workspace:ro
# Dev: bind the sidecar source over the baked copy so edits need only `restart terminal`
# (no rebuild). Prod bakes it into the image (no mount) — see docker-compose.prod.yml.
- ./docker/terminal/server.js:/app/server.js:ro

View File

@ -1,11 +1,6 @@
# Clusev terminal sidecar — Node ws↔SSH/PTY bridge.
# Clusev terminal sidecar — Node ws↔SSH bridge (pure JS: ws + ssh2, no native addons).
FROM node:20-bookworm-slim
# node-pty compiles a native addon → needs python3 + a toolchain at install time.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev && npm cache clean --force

View File

@ -2,14 +2,13 @@
"name": "clusev-terminal-sidecar",
"version": "1.0.0",
"private": true,
"description": "Clusev terminal sidecar — bridges a browser WebSocket to an SSH PTY (per server) or a local shell PTY (the Clusev host).",
"description": "Clusev terminal sidecar — bridges a browser WebSocket to an SSH PTY (per server and the Clusev host).",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"ssh2": "^1.15.0",
"ws": "^8.18.0",
"node-pty": "^1.0.0"
"ws": "^8.18.0"
}
}

View File

@ -16,13 +16,10 @@
const http = require('http');
const { WebSocketServer } = require('ws');
const { Client } = require('ssh2');
const pty = require('node-pty');
const PORT = parseInt(process.env.TERMINAL_PORT || '3000', 10);
const APP_URL = (process.env.APP_INTERNAL_URL || 'http://app:80').replace(/\/$/, '');
const SECRET = process.env.TERMINAL_SIDECAR_SECRET || '';
const HOST_SHELL = process.env.TERMINAL_HOST_SHELL || 'bash';
const HOST_CWD = process.env.TERMINAL_HOST_CWD || '/home/node';
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
@ -66,27 +63,6 @@ function bindInput(ws, io) {
});
}
function startHost(ws, size) {
let p;
try {
p = pty.spawn(HOST_SHELL, ['-l'], {
name: 'xterm-256color', cwd: HOST_CWD, cols: size.cols, rows: size.rows,
// HOME = the landing dir so the shell opens at ~ (not a bare absolute path) and the prompt
// reads cleanly; the container's `hostname: clusev` makes \h show "clusev", not the hash.
env: { ...process.env, TERM: 'xterm-256color', HOME: HOST_CWD },
});
} catch (e) {
send(ws, { type: 'error', data: 'Shell konnte nicht gestartet werden.' });
ws.close();
return;
}
send(ws, { type: 'ready' });
p.onData((d) => send(ws, { type: 'data', data: d }));
p.onExit(() => { send(ws, { type: 'exit' }); try { ws.close(); } catch (_) {} });
bindInput(ws, { write: (d) => p.write(d), resize: (c, r) => { try { p.resize(c, r); } catch (_) {} } });
ws.on('close', () => { try { p.kill(); } catch (_) {} });
}
function startSsh(ws, spec, size) {
const conn = new Client();
let stream = null;
@ -161,8 +137,9 @@ wss.on('connection', async (ws, req) => {
return;
}
if (spec && spec.kind === 'host') startHost(ws, size);
else if (spec && spec.kind === 'server' && spec.host && spec.username) startSsh(ws, spec, size);
// resolve() always returns kind:'server' (the host terminal resolves to an SSH login too), so the
// sidecar only ever opens an SSH PTY — there is no local-shell path.
if (spec && spec.kind === 'server' && spec.host && spec.username) startSsh(ws, spec, size);
else { send(ws, { type: 'error', data: 'Kein gültiges Ziel.' }); ws.close(); }
});

View File

@ -10,8 +10,6 @@ return [
// List
'you' => 'du',
'twofa_on' => '2FA aktiv',
'twofa_off' => '2FA aus',
'none' => 'Keine Konten',
// Create modal
@ -41,8 +39,6 @@ return [
'remove_heading' => 'Konto entfernen',
'remove_body' => 'Löscht das Konto dauerhaft und beendet alle seine Sitzungen. Das kann nicht rückgängig gemacht werden.',
'remove_notify' => 'Konto entfernt.',
'cannot_remove_self' => 'Du kannst dein eigenes Konto nicht entfernen.',
'cannot_remove_last' => 'Du kannst das letzte verbleibende Konto nicht entfernen.',
// Force logout (R5 confirm)
'logout' => 'Überall abmelden',

View File

@ -91,9 +91,7 @@ return [
'recovery_warning' => 'Speichere diese Codes jetzt sicher ab — jeder Code funktioniert genau einmal. Lade sie herunter, solange sie angezeigt werden.',
'recovery_download' => 'Als Datei herunterladen',
'recovery_regenerate' => 'Neu erzeugen',
'recovery_regenerate_confirm' => 'Neue Codes erzeugen? Die alten werden ungültig.',
'recovery_regenerated' => 'Neue Backup-Codes erzeugt.',
'recovery_done' => 'Gespeichert — weiter',
'recovery_manage' => 'Backup-Codes verwalten',
'recovery_hidden_notice' => 'Aus Sicherheitsgründen werden Backup-Codes nur einmal bei der Erstellung angezeigt. Erzeuge einen neuen Satz, um ihn zu sehen — die alten werden dabei ungültig.',

View File

@ -15,11 +15,8 @@ return [
'disable' => 'Deaktivieren',
'lock' => 'Sperren',
'unlock' => 'Entsperren',
'back' => 'Zurück',
'retry' => 'Erneut versuchen',
'confirm' => 'Bestätigen',
'open' => 'Öffnen',
'more' => 'mehr',
'apply' => 'Anwenden',
'or' => 'oder',
@ -32,7 +29,6 @@ return [
'secure' => 'sicher',
'unsupported' => 'n. v.',
'unknown' => 'unbekannt',
'loading' => 'Lädt…',
'none' => '—',
// Generic

View File

@ -92,14 +92,10 @@ return [
// ── Show: firewall rules ──────────────────────────────────────────────
'firewall_title' => 'Firewall-Regeln',
'firewall_unavailable' => 'Nicht verfügbar',
'firewall_sub_active' => ' · aktiv',
'firewall_sub_inactive' => ' · inaktiv',
'firewall_sub_not_installed' => ' · nicht installiert',
'firewall_add_rule' => 'Regel',
'firewall_not_supported' => 'Firewall-Verwaltung ist auf diesem System nicht verfügbar.',
'firewall_read_error' => 'Firewall-Status konnte nicht gelesen werden (Verbindung/Rechte). Bitte später erneut laden.',
'firewall_tool_not_installed' => ':tool ist nicht installiert.',
'firewalld_inactive' => 'firewalld ist installiert, aber inaktiv.',
'firewall_default' => 'Standard',
'firewall_incoming' => 'eingehend: :value',
@ -112,15 +108,11 @@ return [
// ── Show: fail2ban status ─────────────────────────────────────────────
'fail2ban_title' => 'fail2ban-Status',
'fail2ban_unavailable' => 'Nicht verfügbar',
'fail2ban_sub_active' => 'aktiv · :count gesperrt',
'fail2ban_sub_installed_inactive' => 'installiert · inaktiv',
'fail2ban_sub_not_installed' => 'nicht installiert',
'fail2ban_ban_ip' => 'IP sperren',
'fail2ban_not_supported' => 'fail2ban ist auf diesem System nicht verfügbar.',
'fail2ban_read_error' => 'fail2ban-Status konnte nicht gelesen werden. Bitte später erneut laden.',
'fail2ban_state_installed_inactive' => 'fail2ban ist installiert, aber inaktiv.',
'fail2ban_state_not_installed' => 'fail2ban ist nicht installiert.',
'fail2ban_banned' => 'gebannt: :count',
'fail2ban_failed' => 'Fehlversuche: :count',
'fail2ban_no_banned' => 'Keine gebannten IPs.',

View File

@ -5,8 +5,6 @@ return [
// Identity header
'account' => 'Konto',
'role_admin' => 'Administrator',
'two_factor_on' => '2FA aktiv',
'two_factor_off' => '2FA aus',
// Section nav tabs
'tab_profile' => 'Profil',
@ -53,13 +51,6 @@ return [
'disable_2fa_notify' => 'Authenticator entfernt.',
// Stub tabs (filled in 0.9.0)
'coming_soon' => 'Wird in 0.9.0 ergänzt.',
'users_title' => 'Benutzer',
'users_subtitle' => 'Konten und Rollen verwalten',
'sessions_title' => 'Sitzungen',
'sessions_subtitle' => 'Aktive Anmeldungen',
'email_title' => 'E-Mail',
'email_subtitle' => 'SMTP und Benachrichtigungen',
// Page title
'title' => 'Einstellungen — Clusev',

View File

@ -10,8 +10,6 @@ return [
// List
'you' => 'you',
'twofa_on' => '2FA on',
'twofa_off' => '2FA off',
'none' => 'No accounts',
// Create modal
@ -41,8 +39,6 @@ return [
'remove_heading' => 'Remove account',
'remove_body' => 'This permanently deletes the account and ends all of its sessions. This cannot be undone.',
'remove_notify' => 'Account removed.',
'cannot_remove_self' => 'You cannot remove your own account.',
'cannot_remove_last' => 'You cannot remove the last remaining account.',
// Force logout (R5 confirm)
'logout' => 'Sign out everywhere',

View File

@ -91,9 +91,7 @@ return [
'recovery_warning' => 'Save these codes somewhere safe now — each works exactly once. Download them while they are shown.',
'recovery_download' => 'Download as file',
'recovery_regenerate' => 'Regenerate',
'recovery_regenerate_confirm' => 'Generate new codes? The old ones stop working.',
'recovery_regenerated' => 'New backup codes generated.',
'recovery_done' => 'Saved — continue',
'recovery_manage' => 'Manage backup codes',
'recovery_hidden_notice' => 'For security, backup codes are shown only once, at creation. Generate a new set to see them — the old ones stop working.',

View File

@ -15,11 +15,8 @@ return [
'disable' => 'Disable',
'lock' => 'Lock',
'unlock' => 'Unlock',
'back' => 'Back',
'retry' => 'Try again',
'confirm' => 'Confirm',
'open' => 'Open',
'more' => 'more',
'apply' => 'Apply',
'or' => 'or',
@ -32,7 +29,6 @@ return [
'secure' => 'secure',
'unsupported' => 'n/a',
'unknown' => 'unknown',
'loading' => 'Loading…',
'none' => '—',
// Generic

View File

@ -92,14 +92,10 @@ return [
// ── Show: firewall rules ──────────────────────────────────────────────
'firewall_title' => 'Firewall rules',
'firewall_unavailable' => 'Not available',
'firewall_sub_active' => ' · active',
'firewall_sub_inactive' => ' · inactive',
'firewall_sub_not_installed' => ' · not installed',
'firewall_add_rule' => 'Rule',
'firewall_not_supported' => 'Firewall management is not available on this system.',
'firewall_read_error' => 'Firewall status could not be read (connection/permissions). Please reload later.',
'firewall_tool_not_installed' => ':tool is not installed.',
'firewalld_inactive' => 'firewalld is installed but inactive.',
'firewall_default' => 'Default',
'firewall_incoming' => 'incoming: :value',
@ -112,15 +108,11 @@ return [
// ── Show: fail2ban status ─────────────────────────────────────────────
'fail2ban_title' => 'fail2ban status',
'fail2ban_unavailable' => 'Not available',
'fail2ban_sub_active' => 'active · :count banned',
'fail2ban_sub_installed_inactive' => 'installed · inactive',
'fail2ban_sub_not_installed' => 'not installed',
'fail2ban_ban_ip' => 'Ban IP',
'fail2ban_not_supported' => 'fail2ban is not available on this system.',
'fail2ban_read_error' => 'fail2ban status could not be read. Please reload later.',
'fail2ban_state_installed_inactive' => 'fail2ban is installed but inactive.',
'fail2ban_state_not_installed' => 'fail2ban is not installed.',
'fail2ban_banned' => 'banned: :count',
'fail2ban_failed' => 'failed attempts: :count',
'fail2ban_no_banned' => 'No banned IPs.',

View File

@ -5,8 +5,6 @@ return [
// Identity header
'account' => 'Account',
'role_admin' => 'Administrator',
'two_factor_on' => '2FA on',
'two_factor_off' => '2FA off',
// Section nav tabs
'tab_profile' => 'Profile',
@ -53,13 +51,6 @@ return [
'disable_2fa_notify' => 'Authenticator removed.',
// Stub tabs (filled in 0.9.0)
'coming_soon' => 'Coming in 0.9.0.',
'users_title' => 'Users',
'users_subtitle' => 'Manage accounts and roles',
'sessions_title' => 'Sessions',
'sessions_subtitle' => 'Active sign-ins',
'email_title' => 'Email',
'email_subtitle' => 'SMTP and notifications',
// Page title
'title' => 'Settings — Clusev',

View File

@ -1,26 +0,0 @@
@props(['name', 'ip', 'status' => 'online', 'cpu' => 0, 'mem' => 0, 'os' => null])
@php
$label = ['online' => __('common.online'), 'warning' => __('common.warning'), 'offline' => __('common.offline'), 'pending' => __('servers.status_pending')][$status] ?? ucfirst($status);
@endphp
<a href="#" class="flex items-center gap-3 rounded-md border border-line bg-inset px-3 py-2.5 transition-colors hover:border-line-strong">
<x-status-dot :status="$status" :ping="in_array($status, ['online', 'pending'], true)" />
<div class="min-w-0 flex-1">
<p class="truncate text-sm text-ink">{{ $name }}</p>
<p class="truncate font-mono text-[11px] text-ink-3">{{ $ip }}@if ($os) · {{ $os }}@endif</p>
</div>
<div class="hidden shrink-0 items-center gap-4 sm:flex">
<div class="w-16">
<p class="font-mono text-[10px] uppercase text-ink-4">CPU</p>
<div class="mt-1 h-1 w-full overflow-hidden rounded-full bg-line">
<div class="h-full rounded-full bg-accent" style="width: {{ $cpu }}%"></div>
</div>
</div>
<div class="w-16">
<p class="font-mono text-[10px] uppercase text-ink-4">RAM</p>
<div class="mt-1 h-1 w-full overflow-hidden rounded-full bg-line">
<div class="h-full rounded-full bg-cyan" style="width: {{ $mem }}%"></div>
</div>
</div>
</div>
<x-status-pill :status="$status" class="shrink-0">{{ $label }}</x-status-pill>
</a>