From c40d7617f448f977110933e32ed9eed00f88ba75 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 14 Jul 2026 09:27:45 +0000 Subject: [PATCH] =?UTF-8?q?Add=20Shell=20tab=20to=20AdminMonitor=20?= =?UTF-8?q?=E2=80=94=20run=20commands=20on=20app=20containers=20via=20SSH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- src/pages/AdminMonitor.tsx | 121 +++++++++++++++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 5 deletions(-) diff --git a/src/pages/AdminMonitor.tsx b/src/pages/AdminMonitor.tsx index 2c5ce83..9c03619 100644 --- a/src/pages/AdminMonitor.tsx +++ b/src/pages/AdminMonitor.tsx @@ -1,5 +1,5 @@ -import { useEffect, useState } from 'react' -import { ArrowUpCircle, Activity, ScrollText, RefreshCw } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' +import { ArrowUpCircle, Activity, ScrollText, RefreshCw, TerminalSquare } from 'lucide-react' import { Sidebar } from '../components/Sidebar' import type { User } from '../types' @@ -29,6 +29,7 @@ interface StatRange { total: number; used: number } interface HealthStatus { name: string + host: string up: boolean ms: number | null ram: StatRange | null @@ -68,12 +69,17 @@ interface DeployEntry { } export function AdminMonitor({ user }: { user: User }) { - const [tab, setTab] = useState<'updates' | 'states' | 'deploys'>('updates') + const [tab, setTab] = useState<'updates' | 'states' | 'deploys' | 'shell'>('updates') const [statuses, setStatuses] = useState([]) const [deploys, setDeploys] = useState([]) const [health, setHealth] = useState([]) const [loading, setLoading] = useState(false) const [deploying, setDeploying] = useState>(new Set()) + const [shellContainer, setShellContainer] = useState('') + const [shellCmd, setShellCmd] = useState('') + const [shellOutput, setShellOutput] = useState('') + const [shellRunning, setShellRunning] = useState(false) + const termRef = useRef(null) async function fetchStatus(force = false) { setLoading(true) @@ -106,6 +112,34 @@ export function AdminMonitor({ user }: { user: User }) { } } + async function runShellCmd() { + const cmd = shellCmd.trim() + if (!shellContainer || !cmd || shellRunning) return + const [name, host] = shellContainer.split('|') + setShellOutput(prev => prev + `[${name} @ ${host}] $ ${cmd}\n`) + setShellRunning(true) + setShellCmd('') + try { + const res = await fetch('/deploy/exec', { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ host, command: cmd }), + }) + const data = await res.json() + if (!res.ok) { + setShellOutput(prev => prev + `Error: ${data.error}\n\n`) + } else { + const out = [data.stdout, data.stderr].filter(Boolean).join('') + setShellOutput(prev => prev + (out.trim() || '(no output)') + '\n\n') + } + } catch (e: any) { + setShellOutput(prev => prev + `Request failed: ${e.message}\n\n`) + } finally { + setShellRunning(false) + } + } + async function triggerDeploy(repo: string) { setDeploying(prev => new Set(prev).add(repo)) try { @@ -127,14 +161,20 @@ export function AdminMonitor({ user }: { user: User }) { const id = setInterval(fetchStates, 30_000) return () => clearInterval(id) } + if (tab === 'shell' && health.length === 0) fetchStates() }, [tab]) + useEffect(() => { + if (termRef.current) termRef.current.scrollTop = termRef.current.scrollHeight + }, [shellOutput]) + const updatesAvailable = statuses.filter(s => s.updateAvailable).length const tabs = [ - { key: 'updates' as const, label: 'Updates', icon: }, - { key: 'states' as const, label: 'States', icon: }, + { key: 'updates' as const, label: 'Updates', icon: }, + { key: 'states' as const, label: 'States', icon: }, { key: 'deploys' as const, label: 'Deploy Log', icon: }, + { key: 'shell' as const, label: 'Shell', icon: }, ] return ( @@ -358,6 +398,77 @@ export function AdminMonitor({ user }: { user: User }) { )} + {/* Shell tab */} + {tab === 'shell' && ( +
+
+ + setShellCmd(e.target.value)} + onKeyDown={e => e.key === 'Enter' && runShellCmd()} + placeholder="e.g. docker compose logs --tail 50" + disabled={shellRunning} + style={{ + flex: 1, background: 'var(--card-bg)', border: '1px solid var(--card-border)', + borderRadius: '6px', padding: '0.4rem 0.75rem', fontSize: '0.82rem', + color: 'var(--text-dark)', fontFamily: 'monospace', + }} + /> + + {shellOutput && ( + + )} +
+
+              {shellOutput || Select a container and run a command…}
+            
+
+ )} + )