several servers + groups + url

This commit is contained in:
2026-05-26 19:31:55 +03:00
parent 1daab31d03
commit faa37edf33
3 changed files with 146 additions and 7 deletions
+68
View File
@@ -359,3 +359,71 @@ pre {
.main { grid-template-columns: 1fr; } .main { grid-template-columns: 1fr; }
.panel-left { border-right: none; border-bottom: 1px solid var(--border); } .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;
}
+76 -6
View File
@@ -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 { io } from "socket.io-client";
import axios from "axios"; import axios from "axios";
import "./App.css"; 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" }]; 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 ────────────────────────────────────────────────────────────── // ─── StatusBadge ──────────────────────────────────────────────────────────────
function StatusBadge({ status, ok }) { function StatusBadge({ status, ok }) {
if (status === undefined) return <span className="badge pending">PENDING</span>; if (status === undefined) return <span className="badge pending">PENDING</span>;
@@ -171,6 +184,7 @@ export default function App() {
const [progress, setProgress] = useState(null); // { done, total } const [progress, setProgress] = useState(null); // { done, total }
const [running, setRunning] = useState(false); const [running, setRunning] = useState(false);
const [log, setLog] = useState([]); const [log, setLog] = useState([]);
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
const pushLog = useCallback((msg) => { const pushLog = useCallback((msg) => {
setLog((l) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...l].slice(0, 50)); setLog((l) => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...l].slice(0, 50));
@@ -178,7 +192,7 @@ export default function App() {
// ── Connect socket once ────────────────────────────────────────────────── // ── Connect socket once ──────────────────────────────────────────────────
useEffect(() => { useEffect(() => {
const socket = io(SERVER_URL); const socket = io({ path: SOCKET_IO_PATH });
socketRef.current = socket; socketRef.current = socket;
socket.on("connect", () => { socket.on("connect", () => {
@@ -274,7 +288,7 @@ export default function App() {
console.log("sending:", JSON.stringify(payload)); console.log("sending:", JSON.stringify(payload));
try { try {
await axios.post("http://localhost:4000/api/parallel-fetch", payload); await axios.post(`${API_BASE}/parallel-fetch`, payload);
} catch (err) { } catch (err) {
pushLog(`POST error: ${err.message}`); pushLog(`POST error: ${err.message}`);
setRunning(false); setRunning(false);
@@ -282,6 +296,22 @@ export default function App() {
}; };
const pct = progress ? Math.round((progress.done / progress.total) * 100) : 0; 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 ( return (
<div className="app"> <div className="app">
@@ -354,10 +384,50 @@ export default function App() {
)} )}
<div className="results-list"> <div className="results-list">
{results.map((r, i) => ( {groupedResults.map((group) => {
<ResultCard key={i} result={r} index={i} /> const { serverName, items, errors } = group;
const isCollapsed = collapsedGroups.has(serverName);
const hasErrors = errors > 0;
const groupClass = `server-group ${
hasErrors ? "group-err" : "group-ok"
}`;
return (
<div className={groupClass} key={serverName}>
<div
className="server-group-header"
onClick={() =>
setCollapsedGroups((prev) => {
const next = new Set(prev);
if (next.has(serverName)) next.delete(serverName);
else next.add(serverName);
return next;
})
}
>
<div className="server-group-title">
<span className="group-chevron">
{isCollapsed ? "▶" : "▼"}
</span>
<strong>{serverName}</strong>
</div>
<div className="server-group-meta">
<span className="server-errors">
{errors} error{errors === 1 ? "" : "s"}/
{items.length} total
</span>
</div>
</div>
{!isCollapsed && (
<div className="server-group-results">
{items.map(({ result, index }) => (
<ResultCard key={index} result={result} index={index} />
))} ))}
</div> </div>
)}
</div>
);
})}
</div>
</section> </section>
</main> </main>
</div> </div>
+1
View File
@@ -4,6 +4,7 @@ import babel from '@rolldown/plugin-babel'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
base: '/tool-gui/',
plugins: [ plugins: [
react(), react(),
babel({ presets: [reactCompilerPreset()] }) babel({ presets: [reactCompilerPreset()] })