added docs tab and improved authentication on dash
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

This commit is contained in:
Sven laptop
2026-09-21 23:53:54 +02:00
parent 38782bca6d
commit da5c6b2839
2 changed files with 311 additions and 15 deletions
+199 -15
View File
@@ -16,7 +16,8 @@ const serviceMeta = {
role: "Identity & tokens",
color: "amber",
data: "Login, JWT sessions, current user profile",
routes: ["/login", "/me"],
routes: ["/login", "/me", "/health"],
docs: "https://github.com/your-org/auth_api/blob/main/README.md",
},
employee: {
label: "Employee API",
@@ -24,6 +25,7 @@ const serviceMeta = {
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",
@@ -36,6 +38,7 @@ const serviceMeta = {
"/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",
@@ -43,6 +46,7 @@ const serviceMeta = {
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",
},
};
@@ -53,6 +57,7 @@ const healthPaths = {
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",
@@ -103,6 +108,9 @@ function App() {
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([]);
@@ -118,6 +126,45 @@ function App() {
});
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) => {
@@ -332,6 +379,12 @@ function App() {
>
Service catalog
</button>
<button
className={activeView === "docs" ? "active" : ""}
onClick={() => setActiveView("docs")}
>
Docs
</button>
<button
className={activeView === "incidents" ? "active" : ""}
onClick={() => setActiveView("incidents")}
@@ -390,6 +443,7 @@ function App() {
</>
)}
{activeView === "catalog" && <Catalog />}
{activeView === "docs" && <Docs />}
{activeView === "incidents" && (
<Incidents errors={errors} state={errorState} reload={loadErrors} />
)}
@@ -404,7 +458,15 @@ function App() {
/>
)}
{activeView === "settings" && (
<Connections urls={urls} token={token} save={saveSettings} />
<Connections
urls={urls}
token={token}
user={user}
login={login}
logout={logout}
save={saveSettings}
loginLoading={loginLoading}
/>
)}
</main>
</div>
@@ -476,7 +538,7 @@ function QuickNotes() {
<li>
<strong>Auth-aware checks</strong>
<span>
Paste an admin JWT in Connections to unlock /me and error history.
Login with admin credentials to unlock /me and error history.
</span>
</li>
<li>
@@ -490,6 +552,55 @@ function QuickNotes() {
</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">
@@ -681,9 +792,84 @@ function RequestLab({ tester, setTester, result, run, busy, token }) {
</section>
);
}
function Connections({ urls, token, save }) {
function Connections({ urls, token, user, login, logout, save, loginLoading }) {
const [draft, setDraft] = useState(urls);
const [draftToken, setDraftToken] = useState(token);
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">
@@ -710,18 +896,16 @@ function Connections({ urls, token, save }) {
</label>
))}
</div>
<label>
Admin JWT token
<input
type="password"
value={draftToken}
onChange={(event) => setDraftToken(event.target.value)}
placeholder="eyJ..."
/>
</label>
<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, draftToken)}
onClick={() => save(draft, token)}
>
Save connections
</button>