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
parent
e38409bf31
commit
fd4f6ceb0f
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(); }
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue