diff --git a/monitoring_ui/src/App.jsx b/monitoring_ui/src/App.jsx index 88f7f8a..9bc2b3e 100644 --- a/monitoring_ui/src/App.jsx +++ b/monitoring_ui/src/App.jsx @@ -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 + + + + + + ); + } + return (
@@ -710,18 +896,16 @@ function Connections({ urls, token, save }) { ))}
- +
+

Logged in as

+

User Code: {user?.userCode}

+

Role: {user?.job}

+

Admin: {user?.isAdmin ? "Yes" : "No"}

+ +
diff --git a/monitoring_ui/src/styles.css b/monitoring_ui/src/styles.css index dc8e1d1..cd26cfb 100644 --- a/monitoring_ui/src/styles.css +++ b/monitoring_ui/src/styles.css @@ -603,6 +603,118 @@ textarea:focus { 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) { .app-shell { padding: 0 18px 40px;