Files
The-transit-Project/monitoring_ui/src/App.jsx
T
Sven laptop da5c6b2839
Build and Push Docker Images / detect-changes (push) Successful in 9s
Build and Push Docker Images / build-transit-api (push) Has been skipped
Build and Push Docker Images / build-auth-api (push) Has been skipped
Build and Push Docker Images / build-employee-api (push) Has been skipped
Build and Push Docker Images / build-error-log-api (push) Has been skipped
Build and Push Docker Images / build-admin-ui (push) Has been skipped
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m11s
Build and Push Docker Images / deploy-test-node (push) Has been skipped
added docs tab and improved authentication on dash
2026-09-21 23:53:54 +02:00

917 lines
27 KiB
React

import { useCallback, useEffect, useMemo, useState } from "react";
const runtime =
typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {};
const configured = {
auth: runtime.AUTH_API || import.meta.env.VITE_AUTH_API || "",
employee: runtime.EMPLOYEE_API || import.meta.env.VITE_EMPLOYEE_API || "",
transit: runtime.TRANSIT_API || import.meta.env.VITE_TRANSIT_API || "",
errors: runtime.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API || "",
};
const serviceMeta = {
auth: {
label: "Auth API",
role: "Identity & tokens",
color: "amber",
data: "Login, JWT sessions, current user profile",
routes: ["/login", "/me", "/health"],
docs: "https://github.com/your-org/auth_api/blob/main/README.md",
},
employee: {
label: "Employee API",
role: "People operations",
color: "teal",
data: "Employee onboarding and employee records",
routes: ["/health", "/createemployee"],
docs: "https://github.com/your-org/employee_api/blob/main/README.md",
},
transit: {
label: "Transit API",
role: "Routes & schedules",
color: "blue",
data: "Schedules, routes, GPS, fleet and admin data",
routes: [
"/health",
"/public/schedules/:scheduleId/progress",
"/schedule/daily?date=YYYY-MM-DD",
"/admin/stops",
],
docs: "https://github.com/your-org/transit_api/blob/main/README.md",
},
errors: {
label: "Error Log API",
role: "Incident archive",
color: "coral",
data: "Centralized bad requests and service errors",
routes: ["/health", "/error_log?limit=100"],
docs: "https://github.com/your-org/error_log_api/blob/main/README.md",
},
};
const healthPaths = {
auth: "/health",
employee: "/health",
transit: "/health",
errors: "/health",
};
const testerRoutes = [
{ service: "auth", method: "POST", path: "/login", label: "Login (userCode + password)" },
{ service: "auth", method: "GET", path: "/me", label: "Current session" },
{
service: "employee",
method: "GET",
path: "/health",
label: "Employee health",
},
{
service: "transit",
method: "GET",
path: "/health",
label: "Transit health",
},
{
service: "transit",
method: "GET",
path: "/public/schedules/1/progress",
label: "Public schedule progress",
},
{
service: "transit",
method: "GET",
path: "/schedule/daily?date=2026-09-20",
label: "Driver daily schedule",
},
{
service: "errors",
method: "GET",
path: "/error_log?limit=25",
label: "Recent error log",
},
];
function pretty(value) {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function App() {
const [urls, setUrls] = useState(
() =>
JSON.parse(localStorage.getItem("transit-monitor-urls") || "null") ||
configured,
);
const [token, setToken] = useState(
() => localStorage.getItem("transit-monitor-token") || "",
);
const [user, setUser] = useState(
() => JSON.parse(localStorage.getItem("transit-monitor-user") || "null"),
);
const [health, setHealth] = useState({});
const [history, setHistory] = useState({});
const [errors, setErrors] = useState([]);
const [errorState, setErrorState] = useState("idle");
const [interval, setIntervalValue] = useState(30);
const [lastRefresh, setLastRefresh] = useState(null);
const [activeView, setActiveView] = useState("overview");
const [tester, setTester] = useState({
service: "transit",
method: "GET",
path: "/health",
body: "",
});
const [testerResult, setTesterResult] = useState(null);
const [busy, setBusy] = useState(false);
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState("");
const login = useCallback(async (userCode, password) => {
const url = urls.auth;
if (!url) {
setLoginError("Auth API URL not configured");
return;
}
setLoginLoading(true);
setLoginError("");
try {
const response = await fetch(`${url}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userCode, password }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
setToken(data.token);
setUser(data.user);
localStorage.setItem("transit-monitor-token", data.token);
localStorage.setItem("transit-monitor-user", JSON.stringify(data.user));
setLoginError("");
} catch (error) {
setLoginError(error.message);
} finally {
setLoginLoading(false);
}
}, [urls.auth]);
const logout = useCallback(() => {
setToken("");
setUser(null);
localStorage.removeItem("transit-monitor-token");
localStorage.removeItem("transit-monitor-user");
}, []);
const checkService = useCallback(
async (key) => {
const started = performance.now();
const url = urls[key];
if (!url) {
return {
ok: false,
status: 0,
body: { error: "No URL configured" },
latency: 0,
checkedAt: new Date().toISOString(),
};
}
const headers = token ? { Authorization: `Bearer ${token}` } : {};
try {
const response = await fetch(`${url}${healthPaths[key]}`, {
headers,
});
const body = await response.json().catch(() => null);
return {
ok: response.ok,
status: response.status,
body,
latency: Math.round(performance.now() - started),
checkedAt: new Date().toISOString(),
};
} catch (error) {
return {
ok: false,
status: 0,
body: { error: error.message },
latency: Math.round(performance.now() - started),
checkedAt: new Date().toISOString(),
};
}
},
[token, urls],
);
const refreshHealth = useCallback(async () => {
const results = await Promise.all(
Object.keys(serviceMeta).map(async (key) => [
key,
await checkService(key),
]),
);
const next = Object.fromEntries(results);
setHealth(next);
setHistory((current) =>
Object.fromEntries(
Object.entries(next).map(([key, value]) => [
key,
[...(current[key] || []), value.latency].slice(-18),
]),
),
);
setLastRefresh(new Date());
}, [checkService]);
const loadErrors = useCallback(async () => {
if (!token) {
setErrors([]);
setErrorState("missing-token");
return;
}
setErrorState("loading");
try {
const response = await fetch(`${urls.errors}/error_log?limit=80`, {
headers: { Authorization: `Bearer ${token}` },
});
const body = await response.json();
if (!response.ok)
throw new Error(body.error || `HTTP ${response.status}`);
setErrors(body.errors || []);
setErrorState("ready");
} catch (error) {
setErrorState(error.message);
}
}, [token, urls.errors]);
useEffect(() => {
refreshHealth();
const timer = setInterval(refreshHealth, interval * 1000);
return () => clearInterval(timer);
}, [refreshHealth, interval]);
useEffect(() => {
if (activeView === "incidents") loadErrors();
}, [activeView, loadErrors]);
const saveSettings = (nextUrls, nextToken) => {
setUrls(nextUrls);
setToken(nextToken);
localStorage.setItem("transit-monitor-urls", JSON.stringify(nextUrls));
localStorage.setItem("transit-monitor-token", nextToken);
};
const runRequest = async () => {
const route = testerRoutes.find(
(item) =>
item.service === tester.service &&
item.method === tester.method &&
item.path === tester.path,
);
const isMutation = tester.method !== "GET";
if (
isMutation &&
!window.confirm("This request can change data. Continue?")
)
return;
setBusy(true);
setTesterResult(null);
const started = performance.now();
try {
const response = await fetch(`${urls[tester.service]}${tester.path}`, {
method: tester.method,
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(tester.body ? { "Content-Type": "application/json" } : {}),
},
body: tester.body || undefined,
});
const text = await response.text();
let body;
try {
body = JSON.parse(text);
} catch {
body = text;
}
setTesterResult({
ok: response.ok,
status: response.status,
latency: Math.round(performance.now() - started),
body,
label: route?.label,
});
} catch (error) {
setTesterResult({
ok: false,
status: 0,
latency: Math.round(performance.now() - started),
body: { error: error.message },
});
} finally {
setBusy(false);
}
};
const healthyCount = Object.values(health).filter((item) => item.ok).length;
const configuredCount = Object.keys(serviceMeta).length;
const avgLatency = Math.round(
Object.values(health).reduce((sum, item) => sum + (item.latency || 0), 0) /
Math.max(Object.keys(health).length, 1),
);
return (
<div className="app-shell">
<header className="topbar">
<div className="brand">
<span className="brand-mark">X</span>
<div>
<strong>TRANSIT CONTROL ROOM</strong>
<span>service observability</span>
</div>
</div>
<div className="top-actions">
<span className="live-dot" /> Live polling{" "}
<select
value={interval}
onChange={(event) => setIntervalValue(Number(event.target.value))}
>
<option value="15">15 sec</option>
<option value="30">30 sec</option>
<option value="60">1 min</option>
</select>
<button className="ghost-button" onClick={refreshHealth}>
Refresh now
</button>
</div>
</header>
<main>
<section className="hero-row">
<div>
<p className="eyebrow">
SYSTEM PULSE /{" "}
{lastRefresh ? lastRefresh.toLocaleTimeString() : "CONNECTING"}
</p>
<h1>Know what is moving.</h1>
<p className="hero-copy">
A live field view across the APIs that keep XTransit running.
</p>
</div>
<div className="pulse-stat">
<span>Healthy services</span>
<strong>
{healthyCount}
<small>/{configuredCount}</small>
</strong>
<em>{avgLatency} ms avg response</em>
</div>
</section>
<nav className="tabs">
<button
className={activeView === "overview" ? "active" : ""}
onClick={() => setActiveView("overview")}
>
Overview
</button>
<button
className={activeView === "catalog" ? "active" : ""}
onClick={() => setActiveView("catalog")}
>
Service catalog
</button>
<button
className={activeView === "docs" ? "active" : ""}
onClick={() => setActiveView("docs")}
>
Docs
</button>
<button
className={activeView === "incidents" ? "active" : ""}
onClick={() => setActiveView("incidents")}
>
Errors & bad requests <b>{errors.length || ""}</b>
</button>
<button
className={activeView === "tester" ? "active" : ""}
onClick={() => setActiveView("tester")}
>
Request lab
</button>
<button
className={activeView === "settings" ? "active" : ""}
onClick={() => setActiveView("settings")}
>
Connections
</button>
</nav>
{activeView === "overview" && (
<>
<section className="service-grid">
{Object.entries(serviceMeta).map(([key, meta]) => (
<ServiceCard
key={key}
serviceKey={key}
meta={meta}
health={health[key]}
history={history[key] || []}
/>
))}
</section>
<section className="lower-grid">
<div className="panel">
<div className="panel-heading">
<div>
<p className="eyebrow">DATA MAP</p>
<h2>What each service serves</h2>
</div>
<span className="panel-count">
{configuredCount} surfaces
</span>
</div>
<div className="data-list">
{Object.entries(serviceMeta).map(([key, meta]) => (
<div className="data-row" key={key}>
<span className={`service-dot ${meta.color}`} />
<strong>{meta.label}</strong>
<span>{meta.data}</span>
</div>
))}
</div>
</div>
<QuickNotes />
</section>
</>
)}
{activeView === "catalog" && <Catalog />}
{activeView === "docs" && <Docs />}
{activeView === "incidents" && (
<Incidents errors={errors} state={errorState} reload={loadErrors} />
)}
{activeView === "tester" && (
<RequestLab
tester={tester}
setTester={setTester}
result={testerResult}
run={runRequest}
busy={busy}
token={token}
/>
)}
{activeView === "settings" && (
<Connections
urls={urls}
token={token}
user={user}
login={login}
logout={logout}
save={saveSettings}
loginLoading={loginLoading}
/>
)}
</main>
</div>
);
}
function ServiceCard({ serviceKey, meta, health, history }) {
const status = !health
? "checking"
: health.ok
? "healthy"
: health.status === 0
? "offline"
: "degraded";
return (
<article className={`service-card ${meta.color}`}>
<div className="card-top">
<div>
<span className={`service-icon ${meta.color}`}>
{meta.label.slice(0, 2).toUpperCase()}
</span>
<h3>{meta.label}</h3>
<p>{meta.role}</p>
</div>
<span className={`status-pill ${status}`}>
<i />
{status}
</span>
</div>
<div className="metric-line">
<span>Response time</span>
<strong>{health ? `${health.latency} ms` : "--"}</strong>
</div>
<div className="sparkline">
{history.length > 1 ? (
history.map((value, index) => (
<span
key={`${value}-${index}`}
style={{ height: `${Math.max(10, Math.min(100, value / 4))}%` }}
/>
))
) : (
<em>Waiting for first reading</em>
)}
</div>
{health?.body && (
<details>
<summary>Health payload</summary>
<pre>{pretty(health.body)}</pre>
</details>
)}
</article>
);
}
function QuickNotes() {
return (
<div className="panel suggestion-panel">
<p className="eyebrow">OPERATOR NOTES</p>
<h2>Useful next signals</h2>
<ul>
<li>
<strong>Database reachability</strong>
<span>
Transit health already reports DB status. Employee health currently
only proves the process is reachable.
</span>
</li>
<li>
<strong>Auth-aware checks</strong>
<span>
Login with admin credentials to unlock /me and error history.
</span>
</li>
<li>
<strong>Safer testing</strong>
<span>
The request lab is intentionally allowlisted, with confirmation
before writes.
</span>
</li>
</ul>
</div>
);
}
function Docs() {
return (
<section className="panel docs-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">SERVICE DOCUMENTATION</p>
<h2>API Documentation & References</h2>
</div>
<span className="panel-count">
{Object.keys(serviceMeta).length} services
</span>
</div>
<div className="docs-grid">
{Object.entries(serviceMeta).map(([key, meta]) => (
<div className={`doc-card ${meta.color}`} key={key}>
<div className="doc-header">
<span className={`service-dot ${meta.color}`} />
<div>
<strong>{meta.label}</strong>
<p>{meta.role}</p>
</div>
</div>
<p className="doc-description">{meta.data}</p>
<div className="doc-links">
{meta.docs && (
<a
href={meta.docs}
target="_blank"
rel="noopener noreferrer"
className="doc-link"
>
View Documentation →
</a>
)}
<details className="routes-detail">
<summary>API Routes</summary>
<ul>
{meta.routes.map((route) => (
<li key={route}><code>{route}</code></li>
))}
</ul>
</details>
</div>
</div>
))}
</div>
</section>
);
}
function Catalog() {
return (
<section className="panel catalog-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">ENDPOINT ATLAS</p>
<h2>Known API surfaces</h2>
</div>
<span className="panel-count">read-only reference</span>
</div>
{Object.entries(serviceMeta).map(([key, meta]) => (
<div className="catalog-block" key={key}>
<div>
<span className={`service-dot ${meta.color}`} />
<strong>{meta.label}</strong>
<p>{meta.data}</p>
</div>
<div className="route-chips">
{meta.routes.map((route) => (
<code key={route}>{route}</code>
))}
</div>
</div>
))}
</section>
);
}
function Incidents({ errors, state, reload }) {
return (
<section className="panel incidents-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">CENTRAL ERROR LOG</p>
<h2>Errors & bad requests</h2>
</div>
<button className="ghost-button" onClick={reload}>
Reload log
</button>
</div>
{state === "missing-token" && (
<Empty
title="Admin token required"
text="Add a JWT in Connections to read the protected error log."
/>
)}
{state !== "missing-token" && state !== "ready" && state !== "idle" && (
<Empty title="Could not load error log" text={state} />
)}
{state === "ready" && !errors.length && (
<Empty
title="No incidents returned"
text="The error log is clear for the selected window."
/>
)}
{errors.length > 0 && (
<div className="incident-table">
<div className="table-head">
<span>Time</span>
<span>Service / route</span>
<span>Error</span>
</div>
{errors.map((item) => (
<div className="table-row" key={item.id}>
<time>{new Date(item.date).toLocaleString()}</time>
<strong>
{item.service}
<small>{item.partofservice || "unspecified route"}</small>
</strong>
<span>{item.error}</span>
</div>
))}
</div>
)}
</section>
);
}
function Empty({ title, text }) {
return (
<div className="empty">
<strong>{title}</strong>
<span>{text}</span>
</div>
);
}
function RequestLab({ tester, setTester, result, run, busy, token }) {
const matching = testerRoutes.filter(
(route) => route.service === tester.service,
);
return (
<section className="lab-grid">
<div className="panel request-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">CONTROLLED EXECUTION</p>
<h2>Live request lab</h2>
</div>
<span className="panel-count">
{token ? "token attached" : "anonymous"}
</span>
</div>
<label>
Service
<select
value={tester.service}
onChange={(event) => {
const route = testerRoutes.find(
(item) => item.service === event.target.value,
);
setTester({
...tester,
service: event.target.value,
method: route.method,
path: route.path,
});
}}
>
{Object.entries(serviceMeta)
.filter(([key]) => key !== "admin")
.map(([key, meta]) => (
<option key={key} value={key}>
{meta.label}
</option>
))}
</select>
</label>
<label>
Endpoint
<select
value={`${tester.method} ${tester.path}`}
onChange={(event) => {
const route = matching.find(
(item) => `${item.method} ${item.path}` === event.target.value,
);
setTester({ ...tester, method: route.method, path: route.path });
}}
>
{matching.map((route) => (
<option key={`${route.method} ${route.path}`}>
{route.method} {route.path}
</option>
))}
</select>
</label>
<label>
JSON body{" "}
<textarea
placeholder="Optional request body"
value={tester.body}
onChange={(event) =>
setTester({ ...tester, body: event.target.value })
}
/>
</label>
<button className="run-button" onClick={run} disabled={busy}>
{busy ? "Running..." : "Run request"}
</button>
<p className="helper">
Requests go directly from this browser to the configured service.
Write routes require a confirmation.
</p>
</div>
<div className="panel result-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">RESPONSE</p>
<h2>
{result
? `${result.status || "Network"} · ${result.latency} ms`
: "Awaiting a request"}
</h2>
</div>
{result && (
<span
className={`status-pill ${result.ok ? "healthy" : "degraded"}`}
>
<i />
{result.ok ? "success" : "failed"}
</span>
)}
</div>
{result ? (
<pre className="response-code">{pretty(result.body)}</pre>
) : (
<div className="result-placeholder">
Pick a known route and execute it to inspect the live response.
</div>
)}
</div>
</section>
);
}
function Connections({ urls, token, user, login, logout, save, loginLoading }) {
const [draft, setDraft] = useState(urls);
const [userCode, setUserCode] = useState("");
const [password, setPassword] = useState("");
const [loginError, setLoginError] = useState("");
const handleLogin = (e) => {
e.preventDefault();
setLoginError("");
login(userCode, password);
};
if (!token) {
return (
<section className="panel settings-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">RUNTIME CONFIGURATION</p>
<h2>Connections & Login</h2>
</div>
<span className="panel-count">stored locally</span>
</div>
<p className="settings-intro">
Point the dashboard at local Docker ports or deployed domains. Nothing
here is sent anywhere except the service you choose to call.
</p>
<div className="connection-grid">
{Object.entries(serviceMeta).map(([key, meta]) => (
<label key={key}>
{meta.label}
<input
value={draft[key]}
onChange={(event) =>
setDraft({ ...draft, [key]: event.target.value })
}
/>
</label>
))}
</div>
<div className="login-form">
<h3>Admin Login</h3>
<form onSubmit={handleLogin}>
<label>
User Code
<input
type="number"
value={userCode}
onChange={(e) => setUserCode(e.target.value)}
placeholder="1234"
required
/>
</label>
<label>
Password
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</label>
{loginError && <div className="login-error">{loginError}</div>}
<button type="submit" className="run-button save-button" disabled={loginLoading}>
{loginLoading ? "Logging in..." : "Login"}
</button>
</form>
</div>
<button
className="run-button save-button"
onClick={() => save(draft, "")}
>
Save connections
</button>
</section>
);
}
return (
<section className="panel settings-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">RUNTIME CONFIGURATION</p>
<h2>Connections</h2>
</div>
<span className="panel-count">stored locally</span>
</div>
<p className="settings-intro">
Point the dashboard at local Docker ports or deployed domains. Nothing
here is sent anywhere except the service you choose to call.
</p>
<div className="connection-grid">
{Object.entries(serviceMeta).map(([key, meta]) => (
<label key={key}>
{meta.label}
<input
value={draft[key]}
onChange={(event) =>
setDraft({ ...draft, [key]: event.target.value })
}
/>
</label>
))}
</div>
<div className="user-info">
<h3>Logged in as</h3>
<p><strong>User Code:</strong> {user?.userCode}</p>
<p><strong>Role:</strong> {user?.job}</p>
<p><strong>Admin:</strong> {user?.isAdmin ? "Yes" : "No"}</p>
<button className="ghost-button" onClick={logout}>Logout</button>
</div>
<button
className="run-button save-button"
onClick={() => save(draft, token)}
>
Save connections
</button>
</section>
);
}
export default App;