feat: add employee API integration and request lab component
Build and Push Docker Images / detect-changes (push) Successful in 13s
Build and Push Docker Images / build-auth-api (push) Successful in 1m19s
Build and Push Docker Images / build-employee-api (push) Successful in 1m1s
Build and Push Docker Images / build-transit-api (push) Successful in 59s
Build and Push Docker Images / build-error-log-api (push) Successful in 57s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m14s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m12s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s

- Introduced EMPLOYEE_API endpoint in api.js for employee service.
- Added RequestLab component for testing API routes with a user-friendly interface.
- Updated monitoring_ui documentation to reflect changes in features and authentication.
- Removed unnecessary login functionality from monitoring_ui to maintain unauthenticated access.
- Enhanced styles for new components and improved overall UI consistency.
This commit is contained in:
Sven laptop
2026-10-05 20:03:53 +02:00
parent 30f4ca6701
commit 3c35999a84
9 changed files with 284 additions and 711 deletions
+5 -2
View File
@@ -7,7 +7,8 @@ import BusLinesManager from './components/BusLinesManager'
import FleetManager from './components/FleetManager'
import ScheduleMaker from './components/ScheduleMaker'
import ErrorLogViewer from './components/ErrorLogViewer'
import { IconAlert, IconBus, IconCalendar, IconGrid, IconLink, IconLogout, IconMapPin, IconRoute } from './components/icons'
import RequestLab from './components/RequestLab'
import { IconAlert, IconBus, IconCalendar, IconGrid, IconLink, IconLogout, IconMapPin, IconRoute, IconTerminal } from './components/icons'
const SECTIONS = [
{ id: 'overview', label: 'Overzicht', eyebrow: 'Operations', title: 'Dagelijks overzicht', desc: 'Vloot, netwerk en dienstrooster in één oogopslag.', icon: IconGrid },
@@ -17,13 +18,14 @@ const SECTIONS = [
{ id: 'fleet', label: 'Vloot', eyebrow: 'Vlootbeheer', title: 'Vloot', desc: 'Beheer de voertuigen die deel uitmaken van de vloot.', icon: IconBus },
{ id: 'schedules', label: 'Diensten', eyebrow: 'Planning', title: 'Dienstenplanning', desc: 'Plan wekelijkse schema’s en vaste diensten.', icon: IconCalendar },
{ id: 'logs', label: 'Foutmeldingen', eyebrow: 'Systeem', title: 'Centrale foutmeldingen', desc: 'Bekijk fouten die door alle diensten zijn gemeld.', icon: IconAlert },
{ id: 'lab', label: 'API-lab', eyebrow: 'Systeem', title: 'API-testomgeving', desc: 'Voer goedgekeurde verzoeken uit en inspecteer live antwoorden.', icon: IconTerminal },
]
const NAV_GROUPS = [
{ label: 'Algemeen', items: ['overview'] },
{ label: 'Netwerkbeheer', items: ['stops', 'lines', 'routes', 'fleet'] },
{ label: 'Planning', items: ['schedules'] },
{ label: 'Systeem', items: ['logs'] },
{ label: 'Systeem', items: ['logs', 'lab'] },
]
export default function App() {
@@ -110,6 +112,7 @@ export default function App() {
{section === 'fleet' && <FleetManager token={token} user={user} />}
{section === 'schedules' && <ScheduleMaker token={token} user={user} />}
{section === 'logs' && <ErrorLogViewer token={token} />}
{section === 'lab' && <RequestLab token={token} />}
</main>
</div>
</div>
+11
View File
@@ -25,11 +25,22 @@ const TRANSIT_API = normalizeApiBaseUrl(
RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API,
"https://transit.xtransit.testinstance.nl",
);
const EMPLOYEE_API = normalizeApiBaseUrl(
RUNTIME.EMPLOYEE_API || import.meta.env.VITE_EMPLOYEE_API,
"https://employees.xtransit.testinstance.nl",
);
const ERROR_LOG_API = normalizeApiBaseUrl(
RUNTIME.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API,
"https://error.xtransit.testinstance.nl",
);
export const API_BASE = {
auth: AUTH_API,
employee: EMPLOYEE_API,
transit: TRANSIT_API,
errors: ERROR_LOG_API,
};
const reportClientError = async ({ service, partofservice, error }) => {
try {
await fetch(`${ERROR_LOG_API}/error_log`, {
+164
View File
@@ -0,0 +1,164 @@
import React, { useState } from 'react'
import { API_BASE } from '../api'
import { IconTerminal } from './icons'
const SERVICE_OPTIONS = [
{ key: 'auth', label: 'Auth API' },
{ key: 'employee', label: 'Employee API' },
{ key: 'transit', label: 'Transit API' },
{ key: 'errors', label: 'Error Log API' },
]
const ROUTES = [
{ service: 'transit', method: 'GET', path: '/health', label: 'Transit health' },
{ service: 'transit', method: 'GET', path: '/bus/admin/stops', label: 'Haltes (admin)' },
{ service: 'transit', method: 'GET', path: '/bus/schedules', label: 'Dienstrooster-overzicht' },
{ service: 'transit', method: 'GET', path: '/bus/schedule/daily?date=2026-09-25', label: 'Dagelijkse dienstregeling' },
{ service: 'transit', method: 'GET', path: '/bus/public/schedules/1/progress', label: 'Openbare ritvoortgang' },
{ service: 'auth', method: 'GET', path: '/me', label: 'Huidige sessie' },
{ service: 'auth', method: 'POST', path: '/login', label: 'Login (userCode + wachtwoord)' },
{ service: 'employee', method: 'GET', path: '/health', label: 'Employee health' },
{ service: 'errors', method: 'GET', path: '/error_log?limit=50', label: 'Recente foutmeldingen' },
]
const pretty = (value) => {
try {
return JSON.stringify(value, null, 2)
} catch {
return String(value)
}
}
export default function RequestLab({ token }) {
const [route, setRoute] = useState(ROUTES[0])
const [body, setBody] = useState('')
const [result, setResult] = useState(null)
const [busy, setBusy] = useState(false)
const routesForService = ROUTES.filter((r) => r.service === route.service)
const pickService = (next) => {
const first = ROUTES.find((r) => r.service === next)
if (first) setRoute(first)
}
const run = async () => {
if (route.method !== 'GET' && !window.confirm('Deze aanvraag kan gegevens wijzigen. Doorgaan?')) return
setBusy(true)
setResult(null)
const started = performance.now()
try {
const response = await fetch(`${API_BASE[route.service]}${route.path}`, {
method: route.method,
headers: {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body || undefined,
})
const text = await response.text()
let parsed
try {
parsed = JSON.parse(text)
} catch {
parsed = text
}
setResult({
ok: response.ok,
status: response.status,
latency: Math.round(performance.now() - started),
body: parsed,
label: route.label,
})
} catch (err) {
setResult({
ok: false,
status: 0,
latency: Math.round(performance.now() - started),
body: { error: err.message },
label: route.label,
})
} finally {
setBusy(false)
}
}
return (
<div className="stack">
<section className="card">
<div className="section-header">
<div>
<h2>API-testomgeving</h2>
<p>Voer een van de goedgekeurde verzoeken uit om het live antwoord te bekijken.</p>
</div>
</div>
<div className="form-row">
<select value={route.service} onChange={(e) => pickService(e.target.value)} className="no-grow">
{SERVICE_OPTIONS.map((s) => (
<option key={s.key} value={s.key}>{s.label}</option>
))}
</select>
<select
value={`${route.method} ${route.path}`}
onChange={(e) => {
const found = routesForService.find((r) => `${r.method} ${r.path}` === e.target.value)
if (found) setRoute(found)
}}
className="flex-1"
>
{routesForService.map((r) => (
<option key={`${r.method} ${r.path}`} value={`${r.method} ${r.path}`}>
{r.method} {r.path} — {r.label}
</option>
))}
</select>
</div>
<div className="form-row">
<textarea
className="lab-body flex-1"
placeholder="Optionele request body (JSON)"
value={body}
onChange={(e) => setBody(e.target.value)}
/>
</div>
<div className="form-row">
<button className="no-grow" onClick={run} disabled={busy}>
{busy ? 'Uitvoeren…' : 'Verzoek uitvoeren'}
</button>
<span className="muted minw-0">
Verzoeken gaan rechtstreeks van deze browser naar de geconfigureerde service. Schrijfverzoeken vereisen een bevestiging.
</span>
</div>
</section>
<section className="card">
<div className="section-header">
<div>
<h2>Antwoord</h2>
<p>
{result
? `${result.label} · ${result.status || 'Netwerk'} · ${result.latency} ms`
: 'Nog geen verzoek uitgevoerd.'}
</p>
</div>
{result && (
<span className={`badge ${result.ok ? 'badge-success' : 'badge-danger'}`}>
{result.ok ? 'Succes' : 'Mislukt'}
</span>
)}
</div>
{result ? (
<pre className="lab-response">{pretty(result.body)}</pre>
) : (
<div className="lab-placeholder">
<IconTerminal size={28} />
<p>Kies een goedgekeurde route en voer deze uit om het live antwoord te inspecteren.</p>
</div>
)}
</section>
</div>
)
}
+7
View File
@@ -94,4 +94,11 @@ export const IconClock = (p) => (
<circle cx="12" cy="12" r="9" />
<path d="M12 7v5l3 2" />
</Svg>
)
export const IconTerminal = (p) => (
<Svg {...p}>
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="m7 9 3 3-3 3M13 15h4" />
</Svg>
)
+51
View File
@@ -840,6 +840,57 @@ button.block {
display: block;
}
/* ============================================================
Request lab
============================================================ */
.lab-body {
min-height: 90px;
resize: vertical;
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--text);
background: var(--surface);
border: 1px solid var(--border-strong);
border-radius: var(--radius-xs);
padding: 10px 12px;
}
.lab-body:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
}
.lab-response {
margin: 4px 0 0;
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 16px;
overflow: auto;
max-height: 480px;
font-family: var(--font-mono);
font-size: 0.82rem;
line-height: 1.5;
color: var(--text);
}
.lab-placeholder {
display: grid;
place-items: center;
text-align: center;
gap: 10px;
min-height: 220px;
color: var(--text-faint);
font-size: 0.9rem;
}
.lab-placeholder p {
margin: 0;
max-width: 340px;
}
/* ============================================================
Route stop board (drag & drop)
============================================================ */