From a52d3e06f3d6f519e36fc630eb799273629c9e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 16 Aug 2026 23:07:09 +0000 Subject: [PATCH] one red and one green, in the core apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 82 raw palette classes in FileBrowser, FileViewer, SystemMonitor, Chat and Plugins now name a state instead of a colour. `docs/design-language-interface.md` has said to do this for a long time and `tone.ts` implements it; eight files were following it. Only the families whose semantic token IS the colour already in use: destructive #ef4444 == text-red-500 exact success #16a34a == text-green-600 exact So `red-500` and `green-600` are byte-identical renames, and the neighbouring shades (`red-400`, `red-600`, `green-500`, `emerald-500`) shift by a little on purpose — having one red is the point. Verified on screen, not just typechecked. Three greys became `muted-foreground`, which is what "quieter than the text" means here. ── Amber is deliberately untouched, and needs a decision ── 38 amber classes remain and a regex must not have them. They are not warnings. Chat's own comment says it plainly: "the amber a RUNNING task chip wears". It is a BUSY tone, and the token set has no such thing — `--status-in-progress` exists and is blue. Mapping them to `warning` would change both meaning and colour: Duck's `--warning` is #fbbd23 (amber-400) while the code uses amber-600 #d97706 as text and amber-950 as ink on a fill. Text at that lightness on white is not readable, and "running" would start looking like "something is wrong". Two ways out, both the owner's call: add a sixth tone for busy/queued, or accept warning's colour and meaning for it. ── Deliberately out of scope ── The plugin candidates — Soulseek 308, Invoices 95, Wallet 68, Jellyfin 48, Photos 47, Transmission 41 — are two thirds of the debt and are leaving this repository. Their colours belong to their extraction, when the rules exist and their imports are being rewritten anyway. White and black are also untouched: they are media overlays in the file viewer and photo surfaces, correct in every theme because they sit on someone's photograph rather than on a themed surface. Co-Authored-By: Claude Opus 5 --- .../officerdev/src/apps/Chat/ChatHeader.tsx | 2 +- .../src/apps/Chat/components/CodeSurface.tsx | 4 +- .../src/apps/Chat/components/CopyButton.tsx | 2 +- .../components/AgentRunnerModal.tsx | 2 +- .../components/DictateDialog.tsx | 2 +- .../FileBrowserApp/components/FileItem.tsx | 2 +- .../components/NewEntryDialog.tsx | 2 +- .../components/TaskRunnerModal.tsx | 44 +++++---- .../components/Toolbar/SelectionActions.tsx | 2 +- .../components/VideoDownloadDialog.tsx | 6 +- .../src/apps/FileViewer/FileViewerBody.tsx | 2 +- .../src/apps/FileViewer/FileViewerHeader.tsx | 2 +- .../FileViewer/renderers/AudioRenderer.tsx | 7 +- .../FileViewer/renderers/ImageRenderer.tsx | 2 +- .../FileViewer/renderers/VideoRenderer.tsx | 29 +++++- .../src/apps/Plugins/PluginDetail.tsx | 12 +-- .../src/apps/Plugins/PluginsList.tsx | 6 +- .../src/apps/SystemMonitor/BtopView.tsx | 92 +++++++++++++++---- .../src/apps/SystemMonitor/DockerView.tsx | 26 ++++-- .../src/apps/SystemMonitor/LogStream.tsx | 27 +++++- .../src/apps/SystemMonitor/Pm2View.tsx | 40 ++++++-- 21 files changed, 221 insertions(+), 92 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx index dd570c47..6fcbda95 100644 --- a/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/ChatHeader.tsx @@ -139,7 +139,7 @@ export const ChatHeader = ({ panelId }: { panelId?: string }) => { if (s.id === sessionId) setSessionId(null); void deleteSession(s.id); }} - className="p-0.5 opacity-50 hover:opacity-100 text-red-500 cursor-pointer" + className="p-0.5 opacity-50 hover:opacity-100 text-destructive cursor-pointer" > diff --git a/src/workspaces/officerdev/src/apps/Chat/components/CodeSurface.tsx b/src/workspaces/officerdev/src/apps/Chat/components/CodeSurface.tsx index 323d1132..38d63ee2 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/CodeSurface.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/CodeSurface.tsx @@ -131,7 +131,7 @@ export const CodeSurface = ({ code, lang = '', startLine = 1, maxLines, numbered aria-label="Copy the code" className={CONTROL} > - {copied === 'raw' ? : } + {copied === 'raw' ? : } ); }; diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/AgentRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/AgentRunnerModal.tsx index a5b71754..cd34af44 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/AgentRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/AgentRunnerModal.tsx @@ -160,7 +160,7 @@ export const AgentRunnerModal = ({ open, onOpenChange, agent, entryName, entryFu )} {error && ( -
+
{error}
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/DictateDialog.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/DictateDialog.tsx index a8edfa39..11edb806 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/DictateDialog.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/DictateDialog.tsx @@ -187,7 +187,7 @@ export const DictateDialog = ({ fileBrowserManager }: DictateDialogProps) => {
- {recording && } + {recording && } {recording ? 'Recording...' : 'Starting...'}
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx index 3611c39d..80572b80 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx @@ -413,7 +413,7 @@ const MenuItems = ({ )} - onDelete(entry)} className="text-red-600 cursor-pointer"> + onDelete(entry)} className="text-destructive cursor-pointer"> Delete diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx index b38dc25b..f6b0b14f 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx @@ -138,7 +138,7 @@ export const NewEntryDialog = ({ kind, onClose, onSubmit, existingNames }: NewEn /> {/* Reserves its own line whether or not there is a problem, so the dialog does not jump as you type past a colliding name. */} -

{problem ?? ''}

+

{problem ?? ''}

diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx index 39040675..0f3ce561 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -240,18 +240,18 @@ const AgenticTaskRunner = ({ {phase === 'running' ? ( ) : accRef.current.some((m) => m.role === 'error') ? ( - + Task failed ) : ( - + Task complete @@ -544,9 +544,7 @@ const AudioMetaPanel = ({ meta, fileName }: { meta: AudioMeta; fileName: string {dur ? {dur} : null} Lyrics: {meta.hasLyrics ? 'Yes' : 'No'} @@ -1268,7 +1266,7 @@ const ScriptRunner = ({ -
+          
             
               {runner.output.map((line, i) => (
                 
             
             Stop
           
         ) : runner.exitCode !== 0 ? (
-          
+          
             
             Task failed (exit {runner.exitCode})
           
         ) : (
-          
+          
             
             Task complete
           
@@ -1528,7 +1526,7 @@ const PipelineRunner = ({ taskDirName, context, cwd, onClose }: PipelineRunnerPr
             {pipeline.currentStep.status === 'running' && (
               
             )}
-            {pipeline.currentStep.status === 'complete' && done}
+            {pipeline.currentStep.status === 'complete' && done}
           
)} @@ -1541,12 +1539,12 @@ const PipelineRunner = ({ taskDirName, context, cwd, onClose }: PipelineRunnerPr {pDone}/{pTotal} done {pRunning > 0 && · {pRunning} running} - {pError > 0 && · {pError} failed} + {pError > 0 && · {pError} failed} {pDone + pError < pTotal && ( )} - {pDone + pError === pTotal && pTotal > 0 && done} + {pDone + pError === pTotal && pTotal > 0 && done} )} @@ -1577,10 +1575,10 @@ const PipelineRunner = ({ taskDirName, context, cwd, onClose }: PipelineRunnerPr )} {it.status === 'running' && } - {it.status === 'complete' && } - {it.status === 'error' && } + {it.status === 'complete' && } + {it.status === 'error' && } {it.label} @@ -1590,7 +1588,7 @@ const PipelineRunner = ({ taskDirName, context, cwd, onClose }: PipelineRunnerPr )} {it.error && ( - + {it.error} )} @@ -1618,18 +1616,18 @@ const PipelineRunner = ({ taskDirName, context, cwd, onClose }: PipelineRunnerPr {pipeline.phase === 'running' ? ( ) : pipeline.hasError ? ( - + Pipeline failed ) : ( - + Pipeline complete diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx index b6308148..0c8647c0 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/Toolbar/SelectionActions.tsx @@ -56,7 +56,7 @@ export const SelectionActions = ({ fileBrowserManager }: SelectionActionsProps) diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx index 5539661d..50d11480 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/VideoDownloadDialog.tsx @@ -249,7 +249,7 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP {e.status === 'loading' ? ( ) : e.status === 'error' ? ( - + ) : e.thumbnail && !audioOnly ? ( ) : ( @@ -265,7 +265,7 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP ) : e.status === 'error' ? ( <> -

Could not fetch

+

Could not fetch

{e.error || e.url}

) : ( @@ -276,7 +276,7 @@ export const VideoDownloadDialog = ({ fileBrowserManager }: VideoDownloadDialogP

{[e.uploader, fmtDuration(e.duration)].filter(Boolean).join(' · ')}

- {e.dl === 'error' &&

{e.dlError}

} + {e.dl === 'error' &&

{e.dlError}

} )} diff --git a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx index e0eedfbd..4aa9816a 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/FileViewerBody.tsx @@ -43,7 +43,7 @@ export const FileViewerBody = () => { // action that still works, and it is offered rather than left to be guessed at.
- {error} + {error}
- { if (error) { return (
- Failed to load image + Failed to load image
); } diff --git a/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx b/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx index a6015275..8005bf27 100644 --- a/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx +++ b/src/workspaces/officerdev/src/apps/FileViewer/renderers/VideoRenderer.tsx @@ -213,7 +213,7 @@ export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererPr if (error) { return (
- Failed to load video + Failed to load video
); } @@ -227,9 +227,22 @@ export const VideoRenderer = ({ src, fileName, filePath, root }: VideoRendererPr if (playing) setShowControls(false); }} > -
{/* Raw output is dimmer and indented under the step that produced it — present when you want it, @@ -165,7 +165,7 @@ const ActionLog = ({ ))} - {result?.error ?
{result.error}
: null} + {result?.error ?
{result.error}
: null}
); }; @@ -309,7 +309,7 @@ const Purge = ({ plugin, busy, onPurge }: { plugin: PluginItem; busy: boolean; o type="button" disabled={busy} onClick={() => setConfirming(true)} - className="cursor-pointer text-xs text-duck-dark/40 underline underline-offset-2 transition-colors hover:text-red-600 disabled:opacity-40" + className="cursor-pointer text-xs text-duck-dark/40 underline underline-offset-2 transition-colors hover:text-destructive disabled:opacity-40" > Delete {plugin.label} and all its data @@ -318,7 +318,7 @@ const Purge = ({ plugin, busy, onPurge }: { plugin: PluginItem; busy: boolean; o } return ( -
+

Delete {plugin.label} completely?

  • diff --git a/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx b/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx index 9da7ce61..9cbeebc4 100644 --- a/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx +++ b/src/workspaces/officerdev/src/apps/Plugins/PluginsList.tsx @@ -13,7 +13,7 @@ const Status = ({ plugin }: { plugin: PluginItem }) => { if (!plugin.installed) return not installed; if (!plugin.enabled) return disabled; if (plugin.outdated) return update available; - return enabled; + return enabled; }; export const PluginsList = () => { @@ -97,10 +97,10 @@ export const PluginsList = () => { manifest looks exactly like a plugin nobody wrote. */} {broken.map((b) => (
    - +
    {b.appName}
    -
    {b.error}
    +
    {b.error}
    ))} diff --git a/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx b/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx index 8f1f5094..fbef980b 100644 --- a/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx +++ b/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx @@ -10,29 +10,53 @@ type Stats = { uptimeSec: number; loadavg: number[]; cpu: { model: string; cores: number; usagePct: number; perCore: number[] }; - mem: { totalBytes: number; usedBytes: number; freeBytes: number; usedPct: number; swapTotalBytes: number; swapUsedBytes: number } | null; + mem: { + totalBytes: number; + usedBytes: number; + freeBytes: number; + usedPct: number; + swapTotalBytes: number; + swapUsedBytes: number; + } | null; disks: { mount: string; fsType: string; totalBytes: number; usedBytes: number; usedPct: number }[]; processes: { pid: number; user: string; cpuPct: number; memPct: number; command: string }[]; temp: { cpuC: number | null; cpuLabel: string | null; sensors: Sensor[] } | null; gpu: { busyPct: number; vramUsedBytes: number; vramTotalBytes: number } | null; - net: { rxBytesPerSec: number; txBytesPerSec: number; interfaces: { name: string; rxBytesPerSec: number; txBytesPerSec: number }[] } | null; + net: { + rxBytesPerSec: number; + txBytesPerSec: number; + interfaces: { name: string; rxBytesPerSec: number; txBytesPerSec: number }[]; + } | null; power: { cpuWatts: number | null; gpuWatts: number | null } | null; timestamp: number; }; const POLL_MS = 2000; -const barColor = (pct: number) => (pct >= 85 ? 'bg-red-500' : pct >= 60 ? 'bg-amber-500' : 'bg-emerald-500'); -const tempColor = (c: number) => (c >= 80 ? 'text-red-500' : c >= 65 ? 'text-amber-500' : 'text-emerald-500'); +const barColor = (pct: number) => (pct >= 85 ? 'bg-destructive' : pct >= 60 ? 'bg-amber-500' : 'bg-success'); +const tempColor = (c: number) => (c >= 80 ? 'text-destructive' : c >= 65 ? 'text-amber-500' : 'text-success'); const fmtRate = (bps: number) => `${fmtBytes(bps)}/s`; const Bar = ({ pct }: { pct: number }) => (
    -
    +
    ); -const Card = ({ title, icon: Icon, children, className }: { title: string; icon: typeof Cpu; children: React.ReactNode; className?: string }) => ( +const Card = ({ + title, + icon: Icon, + children, + className, +}: { + title: string; + icon: typeof Cpu; + children: React.ReactNode; + className?: string; +}) => (
    @@ -77,12 +101,16 @@ export const BtopView = () => {
    up {fmtDuration(stats.uptimeSec * 1000)} load {stats.loadavg.map((l) => l.toFixed(2)).join(' ')} - +
    )}
    - {error &&
    {error}
    } + {error && ( +
    + {error} +
    + )} {stats && ( <> @@ -93,7 +121,9 @@ export const BtopView = () => { {stats.cpu.cores} cores
    -

    {stats.cpu.model}

    +

    + {stats.cpu.model} +

    {stats.cpu.perCore.map((c, i) => (
    @@ -130,8 +160,12 @@ export const BtopView = () => { {stats.disks.map((d) => (
    - {d.mount} - {fmtBytes(d.usedBytes)} / {fmtBytes(d.totalBytes)} + + {d.mount} + + + {fmtBytes(d.usedBytes)} / {fmtBytes(d.totalBytes)} +
    @@ -147,10 +181,14 @@ export const BtopView = () => { <>
    {stats.gpu.busyPct}% - VRAM {fmtBytes(stats.gpu.vramUsedBytes)} / {fmtBytes(stats.gpu.vramTotalBytes)} + + VRAM {fmtBytes(stats.gpu.vramUsedBytes)} / {fmtBytes(stats.gpu.vramTotalBytes)} +
    - {stats.gpu.vramTotalBytes > 0 && } + {stats.gpu.vramTotalBytes > 0 && ( + + )} ) : (

    no gpu

    @@ -161,7 +199,9 @@ export const BtopView = () => { {stats.temp?.cpuC != null ? ( <>
    - {stats.temp.cpuC.toFixed(0)}°C + + {stats.temp.cpuC.toFixed(0)}°C + {stats.temp.cpuLabel}
    @@ -170,7 +210,9 @@ export const BtopView = () => { .slice(0, 6) .map((s, i) => (
    - {s.name} · {s.label} + + {s.name} · {s.label} + {s.celsius.toFixed(0)}°C
    ))} @@ -192,7 +234,9 @@ export const BtopView = () => { {stats.net.interfaces.slice(0, 4).map((i) => (
    {i.name} - ↓{fmtRate(i.rxBytesPerSec)} ↑{fmtRate(i.txBytesPerSec)} + + ↓{fmtRate(i.rxBytesPerSec)} ↑{fmtRate(i.txBytesPerSec)} +
    ))} {stats.net.interfaces.length === 0 && idle} @@ -208,11 +252,15 @@ export const BtopView = () => {
    CPU package - {stats.power.cpuWatts != null ? `${stats.power.cpuWatts} W` : '—'} + + {stats.power.cpuWatts != null ? `${stats.power.cpuWatts} W` : '—'} +
    GPU - {stats.power.gpuWatts != null ? `${stats.power.gpuWatts} W` : '—'} + + {stats.power.gpuWatts != null ? `${stats.power.gpuWatts} W` : '—'} +
    ) : ( @@ -239,8 +287,12 @@ export const BtopView = () => { {p.pid} {p.user} {p.cpuPct.toFixed(1)} - {p.memPct.toFixed(1)} - {p.command} + + {p.memPct.toFixed(1)} + + + {p.command} + ))} diff --git a/src/workspaces/officerdev/src/apps/SystemMonitor/DockerView.tsx b/src/workspaces/officerdev/src/apps/SystemMonitor/DockerView.tsx index 48bc7f49..4c93b485 100644 --- a/src/workspaces/officerdev/src/apps/SystemMonitor/DockerView.tsx +++ b/src/workspaces/officerdev/src/apps/SystemMonitor/DockerView.tsx @@ -5,7 +5,7 @@ import { LogStream } from './LogStream'; type Container = { id: string; name: string; image: string; state: string; status: string; ports: string }; const POLL_MS = 3000; -const stateColor = (s: string) => (s === 'running' ? 'bg-emerald-500' : s === 'exited' ? 'bg-red-500' : 'bg-amber-500'); +const stateColor = (s: string) => (s === 'running' ? 'bg-success' : s === 'exited' ? 'bg-destructive' : 'bg-amber-500'); export const DockerView = () => { const { get } = useClient(); @@ -45,7 +45,9 @@ export const DockerView = () => { ); const q = filter.trim().toLowerCase(); - const filtered = (containers ?? []).filter((c) => !q || c.name.toLowerCase().includes(q) || c.image.toLowerCase().includes(q)); + const filtered = (containers ?? []).filter( + (c) => !q || c.name.toLowerCase().includes(q) || c.image.toLowerCase().includes(q), + ); return (
    @@ -61,7 +63,11 @@ export const DockerView = () => { className="w-56 rounded-md border border-border bg-background px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary" />
    - {error &&
    {error}
    } + {error && ( +
    + {error} +
    + )}
    {filtered.map((c) => (
    -
    {c.image}
    -
    {c.status}
    - {c.ports &&
    {c.ports}
    } +
    + {c.image} +
    +
    + {c.status} +
    + {c.ports && ( +
    + {c.ports} +
    + )} ))}
    diff --git a/src/workspaces/officerdev/src/apps/SystemMonitor/LogStream.tsx b/src/workspaces/officerdev/src/apps/SystemMonitor/LogStream.tsx index 9ff2487e..84281448 100644 --- a/src/workspaces/officerdev/src/apps/SystemMonitor/LogStream.tsx +++ b/src/workspaces/officerdev/src/apps/SystemMonitor/LogStream.tsx @@ -6,7 +6,17 @@ const MAX_LINES = 1200; // Generic live log viewer: SSE-tails `${streamUrl}&token=…` (server sends one `data:` frame per line), // on a solid-black terminal-style pane. Used for both pm2 and docker logs. -export const LogStream = ({ streamUrl, title, backLabel, onBack }: { streamUrl: string; title: string; backLabel: string; onBack: () => void }) => { +export const LogStream = ({ + streamUrl, + title, + backLabel, + onBack, +}: { + streamUrl: string; + title: string; + backLabel: string; + onBack: () => void; +}) => { const { token } = useClient(); const [lines, setLines] = useState([]); const scrollRef = useRef(null); @@ -25,14 +35,21 @@ export const LogStream = ({ streamUrl, title, backLabel, onBack }: { streamUrl: return (
    - {title} - +
    -
    -        {lines.length === 0 ? connecting… : lines.join('\n')}
    +      
    +        {lines.length === 0 ? connecting… : lines.join('\n')}
           
    ); diff --git a/src/workspaces/officerdev/src/apps/SystemMonitor/Pm2View.tsx b/src/workspaces/officerdev/src/apps/SystemMonitor/Pm2View.tsx index db854b34..771c1713 100644 --- a/src/workspaces/officerdev/src/apps/SystemMonitor/Pm2View.tsx +++ b/src/workspaces/officerdev/src/apps/SystemMonitor/Pm2View.tsx @@ -3,12 +3,27 @@ import { useClient } from 'hooks/useClient'; import { fmtBytes, fmtDuration } from './shared'; import { LogStream } from './LogStream'; -type Proc = { id: number; name: string; status: string; pid: number | null; cpuPct: number; memBytes: number; restarts: number; uptimeMs: number }; +type Proc = { + id: number; + name: string; + status: string; + pid: number | null; + cpuPct: number; + memBytes: number; + restarts: number; + uptimeMs: number; +}; type SortKey = 'id' | 'name' | 'status' | 'cpuPct' | 'memBytes' | 'restarts' | 'uptimeMs' | 'pid'; const POLL_MS = 2000; const statusColor = (s: string) => - s === 'online' ? 'bg-emerald-500' : s === 'stopped' ? 'bg-muted-foreground/40' : s === 'errored' ? 'bg-red-500' : 'bg-amber-500'; + s === 'online' + ? 'bg-success' + : s === 'stopped' + ? 'bg-muted-foreground/40' + : s === 'errored' + ? 'bg-destructive' + : 'bg-amber-500'; const COLS: { key: SortKey; label: string; align: 'left' | 'right' }[] = [ { key: 'id', label: '#', align: 'right' }, @@ -62,7 +77,8 @@ export const Pm2View = () => { arr.sort((a, b) => { const av = a[sortKey] ?? 0; const bv = b[sortKey] ?? 0; - const cmp = typeof av === 'string' && typeof bv === 'string' ? av.localeCompare(bv) : (av as number) - (bv as number); + const cmp = + typeof av === 'string' && typeof bv === 'string' ? av.localeCompare(bv) : (av as number) - (bv as number); return sortDir === 'asc' ? cmp : -cmp; }); return arr; @@ -81,8 +97,14 @@ export const Pm2View = () => { return (
    -
    pm2 · {procs?.length ?? 0} processes · click a name for live logs
    - {error &&
    {error}
    } +
    + pm2 · {procs?.length ?? 0} processes · click a name for live logs +
    + {error && ( +
    + {error} +
    + )}
    @@ -95,7 +117,9 @@ export const Pm2View = () => { className={`inline-flex items-center gap-1 hover:text-foreground ${c.align === 'right' ? 'flex-row-reverse' : ''} ${sortKey === c.key ? 'text-foreground' : ''}`} > {c.label} - {sortKey === c.key ? (sortDir === 'asc' ? '▲' : '▼') : ''} + + {sortKey === c.key ? (sortDir === 'asc' ? '▲' : '▼') : ''} + ))} @@ -129,7 +153,9 @@ export const Pm2View = () => { ))} {procs && procs.length === 0 && ( - + )}
    no processes + no processes +