diff --git a/src/App.css b/src/App.css index f75d1e5..4eb40f9 100644 --- a/src/App.css +++ b/src/App.css @@ -359,3 +359,71 @@ pre { .main { grid-template-columns: 1fr; } .panel-left { border-right: none; border-bottom: 1px solid var(--border); } } +/* ─── Server Group ─────────────────────────────────────────── */ +.server-group { + border: 1px solid var(--border); + border-radius: 8px; + margin-bottom: 16px; + overflow: hidden; +} + +.server-group-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + cursor: pointer; + background: var(--surface); +} + +.server-group-title { + display: flex; + align-items: center; + gap: 8px; +} + +.server-host-sub { + font-size: 0.75rem; + opacity: 0.6; +} + +.server-status-dot { + width: 10px; + height: 10px; + border-radius: 50%; +} +.dot-ok { background: #22c55e; } +.dot-err { background: #ef4444; } +.dot-pending { background: #f59e0b; } + +.group-ok { border-color: #22c55e44; } +.group-err { border-color: #ef444444; } + +.server-group-results { + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.server-group-meta { + display: flex; + align-items: center; + gap: 10px; +} + +.group-chevron { + font-size: 11px; + color: var(--muted); +} + +.server-errors { + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted); +} + +.group-err .server-errors { + color: var(--err); + font-weight: 600; +} diff --git a/src/App.jsx b/src/App.jsx index 45a6426..06fab20 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,9 +1,11 @@ -import React, { useState, useEffect, useRef, useCallback } from "react"; +import React, { useState, useEffect, useRef, useCallback, useMemo } from "react"; import { io } from "socket.io-client"; import axios from "axios"; import "./App.css"; -const SERVER_URL = "http://localhost:4000"; +const BASE = (import.meta.env.BASE_URL ?? "/").replace(/\/$/, ""); +const API_BASE = `${BASE}/api`; +const SOCKET_IO_PATH = `${BASE}/socket.io`; const DEFAULT_HOSTS = [{ url: "", method: "GET" }]; @@ -27,6 +29,17 @@ function isToolResult(result) { ); } +function isResultError(result) { + if (!result) return false; + if (result.type === "error") return true; + const body = getToolBody(result); + if (body && typeof body.returncode === "number" && body.returncode !== 0) { + return true; + } + if (result.ok === false) return true; + return false; +} + // ─── StatusBadge ────────────────────────────────────────────────────────────── function StatusBadge({ status, ok }) { if (status === undefined) return PENDING; @@ -171,6 +184,7 @@ export default function App() { const [progress, setProgress] = useState(null); // { done, total } const [running, setRunning] = useState(false); const [log, setLog] = useState([]); + const [collapsedGroups, setCollapsedGroups] = useState(new Set()); const pushLog = useCallback((msg) => { setLog((l) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...l].slice(0, 50)); @@ -178,7 +192,7 @@ export default function App() { // ── Connect socket once ────────────────────────────────────────────────── useEffect(() => { - const socket = io(SERVER_URL); + const socket = io({ path: SOCKET_IO_PATH }); socketRef.current = socket; socket.on("connect", () => { @@ -274,7 +288,7 @@ export default function App() { console.log("sending:", JSON.stringify(payload)); try { - await axios.post("http://localhost:4000/api/parallel-fetch", payload); + await axios.post(`${API_BASE}/parallel-fetch`, payload); } catch (err) { pushLog(`POST error: ${err.message}`); setRunning(false); @@ -282,6 +296,22 @@ export default function App() { }; const pct = progress ? Math.round((progress.done / progress.total) * 100) : 0; + const groupedResults = useMemo(() => { + const groups = new Map(); + results.forEach((result, index) => { + if (!result) return; + const key = result.serverName || result.host || "Unknown server"; + const entry = groups.get(key) || { items: [], errors: 0 }; + entry.items.push({ result, index }); + if (isResultError(result)) entry.errors += 1; + groups.set(key, entry); + }); + return Array.from(groups.entries()).map(([serverName, data]) => ({ + serverName, + items: data.items, + errors: data.errors, + })); + }, [results]); return (
@@ -354,9 +384,49 @@ export default function App() { )}
- {results.map((r, i) => ( - - ))} + {groupedResults.map((group) => { + const { serverName, items, errors } = group; + const isCollapsed = collapsedGroups.has(serverName); + const hasErrors = errors > 0; + const groupClass = `server-group ${ + hasErrors ? "group-err" : "group-ok" + }`; + return ( +
+
+ setCollapsedGroups((prev) => { + const next = new Set(prev); + if (next.has(serverName)) next.delete(serverName); + else next.add(serverName); + return next; + }) + } + > +
+ + {isCollapsed ? "▶" : "▼"} + + {serverName} +
+
+ + {errors} error{errors === 1 ? "" : "s"}/ + {items.length} total + +
+
+ {!isCollapsed && ( +
+ {items.map(({ result, index }) => ( + + ))} +
+ )} +
+ ); + })}
diff --git a/vite.config.js b/vite.config.js index d1203cd..8e3c110 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,6 +4,7 @@ import babel from '@rolldown/plugin-babel' // https://vite.dev/config/ export default defineConfig({ + base: '/tool-gui/', plugins: [ react(), babel({ presets: [reactCompilerPreset()] })