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
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:
+199
-15
@@ -16,7 +16,8 @@ const serviceMeta = {
|
|||||||
role: "Identity & tokens",
|
role: "Identity & tokens",
|
||||||
color: "amber",
|
color: "amber",
|
||||||
data: "Login, JWT sessions, current user profile",
|
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: {
|
employee: {
|
||||||
label: "Employee API",
|
label: "Employee API",
|
||||||
@@ -24,6 +25,7 @@ const serviceMeta = {
|
|||||||
color: "teal",
|
color: "teal",
|
||||||
data: "Employee onboarding and employee records",
|
data: "Employee onboarding and employee records",
|
||||||
routes: ["/health", "/createemployee"],
|
routes: ["/health", "/createemployee"],
|
||||||
|
docs: "https://github.com/your-org/employee_api/blob/main/README.md",
|
||||||
},
|
},
|
||||||
transit: {
|
transit: {
|
||||||
label: "Transit API",
|
label: "Transit API",
|
||||||
@@ -36,6 +38,7 @@ const serviceMeta = {
|
|||||||
"/schedule/daily?date=YYYY-MM-DD",
|
"/schedule/daily?date=YYYY-MM-DD",
|
||||||
"/admin/stops",
|
"/admin/stops",
|
||||||
],
|
],
|
||||||
|
docs: "https://github.com/your-org/transit_api/blob/main/README.md",
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
label: "Error Log API",
|
label: "Error Log API",
|
||||||
@@ -43,6 +46,7 @@ const serviceMeta = {
|
|||||||
color: "coral",
|
color: "coral",
|
||||||
data: "Centralized bad requests and service errors",
|
data: "Centralized bad requests and service errors",
|
||||||
routes: ["/health", "/error_log?limit=100"],
|
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",
|
errors: "/health",
|
||||||
};
|
};
|
||||||
const testerRoutes = [
|
const testerRoutes = [
|
||||||
|
{ service: "auth", method: "POST", path: "/login", label: "Login (userCode + password)" },
|
||||||
{ service: "auth", method: "GET", path: "/me", label: "Current session" },
|
{ service: "auth", method: "GET", path: "/me", label: "Current session" },
|
||||||
{
|
{
|
||||||
service: "employee",
|
service: "employee",
|
||||||
@@ -103,6 +108,9 @@ function App() {
|
|||||||
const [token, setToken] = useState(
|
const [token, setToken] = useState(
|
||||||
() => localStorage.getItem("transit-monitor-token") || "",
|
() => localStorage.getItem("transit-monitor-token") || "",
|
||||||
);
|
);
|
||||||
|
const [user, setUser] = useState(
|
||||||
|
() => JSON.parse(localStorage.getItem("transit-monitor-user") || "null"),
|
||||||
|
);
|
||||||
const [health, setHealth] = useState({});
|
const [health, setHealth] = useState({});
|
||||||
const [history, setHistory] = useState({});
|
const [history, setHistory] = useState({});
|
||||||
const [errors, setErrors] = useState([]);
|
const [errors, setErrors] = useState([]);
|
||||||
@@ -118,6 +126,45 @@ function App() {
|
|||||||
});
|
});
|
||||||
const [testerResult, setTesterResult] = useState(null);
|
const [testerResult, setTesterResult] = useState(null);
|
||||||
const [busy, setBusy] = useState(false);
|
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(
|
const checkService = useCallback(
|
||||||
async (key) => {
|
async (key) => {
|
||||||
@@ -332,6 +379,12 @@ function App() {
|
|||||||
>
|
>
|
||||||
Service catalog
|
Service catalog
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={activeView === "docs" ? "active" : ""}
|
||||||
|
onClick={() => setActiveView("docs")}
|
||||||
|
>
|
||||||
|
Docs
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={activeView === "incidents" ? "active" : ""}
|
className={activeView === "incidents" ? "active" : ""}
|
||||||
onClick={() => setActiveView("incidents")}
|
onClick={() => setActiveView("incidents")}
|
||||||
@@ -390,6 +443,7 @@ function App() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeView === "catalog" && <Catalog />}
|
{activeView === "catalog" && <Catalog />}
|
||||||
|
{activeView === "docs" && <Docs />}
|
||||||
{activeView === "incidents" && (
|
{activeView === "incidents" && (
|
||||||
<Incidents errors={errors} state={errorState} reload={loadErrors} />
|
<Incidents errors={errors} state={errorState} reload={loadErrors} />
|
||||||
)}
|
)}
|
||||||
@@ -404,7 +458,15 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{activeView === "settings" && (
|
{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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
@@ -476,7 +538,7 @@ function QuickNotes() {
|
|||||||
<li>
|
<li>
|
||||||
<strong>Auth-aware checks</strong>
|
<strong>Auth-aware checks</strong>
|
||||||
<span>
|
<span>
|
||||||
Paste an admin JWT in Connections to unlock /me and error history.
|
Login with admin credentials to unlock /me and error history.
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -490,6 +552,55 @@ function QuickNotes() {
|
|||||||
</div>
|
</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() {
|
function Catalog() {
|
||||||
return (
|
return (
|
||||||
<section className="panel catalog-panel">
|
<section className="panel catalog-panel">
|
||||||
@@ -681,9 +792,84 @@ function RequestLab({ tester, setTester, result, run, busy, token }) {
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
function Connections({ urls, token, save }) {
|
function Connections({ urls, token, user, login, logout, save, loginLoading }) {
|
||||||
const [draft, setDraft] = useState(urls);
|
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 (
|
return (
|
||||||
<section className="panel settings-panel">
|
<section className="panel settings-panel">
|
||||||
<div className="panel-heading">
|
<div className="panel-heading">
|
||||||
@@ -710,18 +896,16 @@ function Connections({ urls, token, save }) {
|
|||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<label>
|
<div className="user-info">
|
||||||
Admin JWT token
|
<h3>Logged in as</h3>
|
||||||
<input
|
<p><strong>User Code:</strong> {user?.userCode}</p>
|
||||||
type="password"
|
<p><strong>Role:</strong> {user?.job}</p>
|
||||||
value={draftToken}
|
<p><strong>Admin:</strong> {user?.isAdmin ? "Yes" : "No"}</p>
|
||||||
onChange={(event) => setDraftToken(event.target.value)}
|
<button className="ghost-button" onClick={logout}>Logout</button>
|
||||||
placeholder="eyJ..."
|
</div>
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<button
|
<button
|
||||||
className="run-button save-button"
|
className="run-button save-button"
|
||||||
onClick={() => save(draft, draftToken)}
|
onClick={() => save(draft, token)}
|
||||||
>
|
>
|
||||||
Save connections
|
Save connections
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -603,6 +603,118 @@ textarea:focus {
|
|||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.login-form {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.login-form h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
}
|
||||||
|
.login-form label {
|
||||||
|
display: grid;
|
||||||
|
gap: 7px;
|
||||||
|
margin: 12px 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font: 10px "DM Mono", monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.login-form input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.login-error {
|
||||||
|
color: var(--coral);
|
||||||
|
font-size: 11px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.user-info {
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.user-info h3 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.user-info p {
|
||||||
|
margin: 6px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.user-info button {
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.docs-panel {
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
.docs-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.doc-card {
|
||||||
|
background: rgba(23, 32, 34, 0.88);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 20px;
|
||||||
|
border-top: 2px solid var(--card-color);
|
||||||
|
}
|
||||||
|
.doc-card.amber { --card-color: var(--amber); }
|
||||||
|
.doc-card.teal { --card-color: var(--mint); }
|
||||||
|
.doc-card.blue { --card-color: var(--blue); }
|
||||||
|
.doc-card.coral { --card-color: var(--coral); }
|
||||||
|
.doc-header {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.doc-header .service-dot {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.doc-header strong { display: block; font-size: 14px; }
|
||||||
|
.doc-header p { margin: 3px 0 0; color: var(--muted); font-size: 11px; }
|
||||||
|
.doc-description {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.doc-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.doc-link {
|
||||||
|
color: var(--mint);
|
||||||
|
text-decoration: none;
|
||||||
|
font: 500 11px "DM Mono", monospace;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.doc-link:hover { text-decoration: underline; }
|
||||||
|
.routes-detail summary {
|
||||||
|
color: var(--muted);
|
||||||
|
font: 500 10px "DM Mono", monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.routes-detail ul {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #c6d5d3;
|
||||||
|
}
|
||||||
|
.routes-detail li { margin: 4px 0; }
|
||||||
|
.routes-detail code {
|
||||||
|
font: 11px "DM Mono", monospace;
|
||||||
|
background: #111a1c;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
padding: 0 18px 40px;
|
padding: 0 18px 40px;
|
||||||
|
|||||||
Reference in New Issue
Block a user