Compare commits
7 Commits
main
...
a17125bdee
| Author | SHA1 | Date | |
|---|---|---|---|
| a17125bdee | |||
| 217dfd3abc | |||
| 635a130f0b | |||
| f935bbeb8c | |||
| 1154d1d658 | |||
| 3f0a02aa04 | |||
| 9d1a37e248 |
@@ -0,0 +1,536 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Command Center | XTransit</title>
|
||||
<link rel="stylesheet" href="./theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-root">
|
||||
<header class="preview-header">
|
||||
<div class="preview-header__brand">
|
||||
<span class="preview-header__logo">XT</span>
|
||||
<span>XTransit Operations</span>
|
||||
</div>
|
||||
<div class="preview-header__actions">
|
||||
<button
|
||||
class="preview-btn preview-btn--ghost"
|
||||
aria-label="Search services"
|
||||
>
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="6.5" stroke-width="1.8"></circle>
|
||||
<path
|
||||
d="m16 16 4.5 4.5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Search</span>
|
||||
</button>
|
||||
<button
|
||||
class="preview-btn preview-btn--ghost"
|
||||
aria-label="View notifications"
|
||||
>
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18 9a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9Z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M10 21h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="preview-badge preview-badge--warning">2</span>
|
||||
</button>
|
||||
<span class="preview-avatar" aria-label="Signed in as Ava Morgan"
|
||||
>AM</span
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="preview-nav" aria-label="Primary navigation">
|
||||
<a class="preview-nav__link preview-nav__link--active" href="#">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect
|
||||
x="3.5"
|
||||
y="3.5"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="13.5"
|
||||
y="3.5"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="3.5"
|
||||
y="13.5"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="13.5"
|
||||
y="13.5"
|
||||
width="7"
|
||||
height="7"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
</svg>
|
||||
<span>Command Center</span>
|
||||
</a>
|
||||
<a class="preview-nav__link" href="./service-catalog.html">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M4 6.5h16M4 12h16M4 17.5h16"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
<circle
|
||||
cx="7"
|
||||
cy="6.5"
|
||||
r="1.4"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
></circle>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="1.4"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
></circle>
|
||||
<circle
|
||||
cx="17"
|
||||
cy="17.5"
|
||||
r="1.4"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
></circle>
|
||||
</svg>
|
||||
<span>Service Catalog</span>
|
||||
</a>
|
||||
<a class="preview-nav__link" href="./request-lab.html">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M5 4.5h14v15H5z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M8 8h8M8 12h8M8 16h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Request Lab</span>
|
||||
</a>
|
||||
<a class="preview-nav__link" href="./incident-log.html">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 3.5 20 7v5c0 4.2-3.2 7.4-8 8.8C7.2 19.4 4 16.2 4 12V7l8-3.5Z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M12 8v4M12 15.5v.1"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Incident Log</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="preview-shell">
|
||||
<aside class="preview-sidebar" aria-label="Command center sections">
|
||||
<div class="preview-sidebar__section">Monitor</div>
|
||||
<a
|
||||
class="preview-sidebar__item preview-sidebar__item--active"
|
||||
href="#"
|
||||
>
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M4 17.5h16M6.5 14V9M12 14V5M17.5 14v-3"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
<path
|
||||
d="M4 19.5h16"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Overview</span>
|
||||
</a>
|
||||
<a class="preview-sidebar__item" href="./service-catalog.html">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<rect
|
||||
x="4"
|
||||
y="4"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="14"
|
||||
y="4"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="4"
|
||||
y="14"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="14"
|
||||
y="14"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
</svg>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
<a class="preview-sidebar__item" href="./incident-log.html">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M5 4.5h14v15H5z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M8 8h8M8 12h6M8 16h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Activity</span>
|
||||
</a>
|
||||
<div class="preview-sidebar__section">Workspace</div>
|
||||
<a class="preview-sidebar__item" href="./request-lab.html">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="m8 8-4 4 4 4M16 8l4 4-4 4M14 5l-4 14"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Request Lab</span>
|
||||
</a>
|
||||
<a class="preview-sidebar__item" href="./incident-log.html">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M12 4.5v8l5 3"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<circle cx="12" cy="12" r="8.5" stroke-width="1.8"></circle>
|
||||
</svg>
|
||||
<span>Incident Log</span>
|
||||
</a>
|
||||
</aside>
|
||||
|
||||
<main class="preview-main">
|
||||
<section class="preview-hero" aria-labelledby="page-title">
|
||||
<div class="preview-hero__mesh"></div>
|
||||
<div class="preview-hero__eyebrow">Operations / Live overview</div>
|
||||
<h1 class="preview-hero__title" id="page-title">Command Center</h1>
|
||||
<p class="preview-hero__subtitle">
|
||||
Current health, polling freshness, and important service signals
|
||||
across the XTransit platform.
|
||||
</p>
|
||||
<div class="preview-hero__actions">
|
||||
<button class="preview-btn preview-btn--primary">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 4v8l4 2"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M19.5 12a7.5 7.5 0 1 1-2.2-5.3"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
<path
|
||||
d="M17 4h3v3"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Run health check</span>
|
||||
</button>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
><span class="preview-badge__dot"></span>Polling every 60
|
||||
seconds</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="preview-kpi-row" aria-label="Platform health summary">
|
||||
<article class="preview-kpi">
|
||||
<svg
|
||||
class="preview-icon preview-icon--lg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 19.5h16M6.5 16V8M12 16V4.5M17.5 16v-5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="preview-kpi__label">Monitored services</span>
|
||||
<strong class="preview-kpi__value">4</strong>
|
||||
<span class="preview-kpi__delta">Across APIs and admin UI</span>
|
||||
</article>
|
||||
<article class="preview-kpi">
|
||||
<svg
|
||||
class="preview-icon preview-icon--lg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="m5 12 4.5 4.5L19 7"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<circle cx="12" cy="12" r="9" stroke-width="1.8"></circle>
|
||||
</svg>
|
||||
<span class="preview-kpi__label">Operational</span>
|
||||
<strong class="preview-kpi__value">3</strong>
|
||||
<span class="preview-kpi__delta preview-kpi__delta--up"
|
||||
>75% of monitored services</span
|
||||
>
|
||||
</article>
|
||||
<article class="preview-kpi">
|
||||
<svg
|
||||
class="preview-icon preview-icon--lg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 4.5 20 19H4L12 4.5Z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M12 10v4M12 16.5v.1"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="preview-kpi__label">Degraded</span>
|
||||
<strong class="preview-kpi__value">1</strong>
|
||||
<span class="preview-kpi__delta"
|
||||
>Error Log API needs attention</span
|
||||
>
|
||||
</article>
|
||||
<article class="preview-kpi">
|
||||
<svg
|
||||
class="preview-icon preview-icon--lg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="8.5" stroke-width="1.8"></circle>
|
||||
<path
|
||||
d="M12 7v5l3.5 2"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="preview-kpi__label">Last poll</span>
|
||||
<strong class="preview-kpi__value">18 sec ago</strong>
|
||||
<span class="preview-kpi__delta">Next poll in 42 seconds</span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="preview-two-column" aria-label="Service signals">
|
||||
<div>
|
||||
<div class="preview-section-title">
|
||||
<h2 class="preview-section-title__h">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 12h4l2-7 4 14 2-7h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Service health
|
||||
</h2>
|
||||
<span class="preview-section-title__hint"
|
||||
>Live response checks</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-table-wrap">
|
||||
<table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Latency</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Transit API</strong><br /><span
|
||||
class="preview-section-title__hint"
|
||||
>/health</span
|
||||
>
|
||||
</td>
|
||||
<td>184 ms</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
><span class="preview-badge__dot"></span
|
||||
>Operational</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Employee API</strong><br /><span
|
||||
class="preview-section-title__hint"
|
||||
>/health</span
|
||||
>
|
||||
</td>
|
||||
<td>96 ms</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
><span class="preview-badge__dot"></span
|
||||
>Operational</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Error Log API</strong><br /><span
|
||||
class="preview-section-title__hint"
|
||||
>/health</span
|
||||
>
|
||||
</td>
|
||||
<td>121 ms</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--warning"
|
||||
><span class="preview-badge__dot"></span
|
||||
>Degraded</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong>Admin UI</strong><br /><span
|
||||
class="preview-section-title__hint"
|
||||
>http://localhost:5173</span
|
||||
>
|
||||
</td>
|
||||
<td>42 ms</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
><span class="preview-badge__dot"></span
|
||||
>Operational</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="preview-section-title">
|
||||
<h2 class="preview-section-title__h">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4.5 5.5h15v13h-15z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M8 9h8M8 13h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
Recent poll activity
|
||||
</h2>
|
||||
<span class="preview-section-title__hint">Updating</span>
|
||||
</div>
|
||||
<div
|
||||
class="preview-card"
|
||||
aria-label="Recent poll activity loading"
|
||||
>
|
||||
<div class="preview-skeleton preview-skeleton--title"></div>
|
||||
<div class="preview-skeleton preview-skeleton--line"></div>
|
||||
<div class="preview-skeleton preview-skeleton--line"></div>
|
||||
<div class="preview-skeleton preview-skeleton--line"></div>
|
||||
<div class="preview-skeleton preview-skeleton--block"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="preview-action-bar">
|
||||
<span class="preview-section-title__hint"
|
||||
>Signed in as Ava Morgan · Operator access</span
|
||||
>
|
||||
<button class="preview-btn preview-btn--secondary">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M4 12a8 8 0 1 0 2.3-5.7"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
<path
|
||||
d="M4 5v5h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Refresh overview</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,479 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Incident Log | Transit Operations</title>
|
||||
<link rel="stylesheet" href="./theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-root">
|
||||
<header class="preview-header">
|
||||
<div class="preview-header__brand">
|
||||
<span class="preview-header__logo">TO</span>
|
||||
<span>Transit Operations</span>
|
||||
</div>
|
||||
<div class="preview-header__actions">
|
||||
<label class="preview-form__input" aria-label="Search console">
|
||||
<svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="11" cy="11" r="6.5" stroke-width="1.8"></circle>
|
||||
<path
|
||||
d="m16 16 5 5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search console"
|
||||
aria-label="Search console"
|
||||
style="
|
||||
border: 0;
|
||||
background: transparent;
|
||||
outline: 0;
|
||||
font: inherit;
|
||||
width: 135px;
|
||||
color: inherit;
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<a
|
||||
class="preview-header__action"
|
||||
href="#notifications"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18 9a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9ZM10 21h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<span class="preview-avatar" aria-label="Signed in as Maya Chen"
|
||||
>MC</span
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="preview-nav" aria-label="Primary navigation">
|
||||
<a class="preview-nav__link" href="./command-center.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="4" y="4" width="6" height="6" stroke-width="1.8"></rect>
|
||||
<rect x="14" y="4" width="6" height="6" stroke-width="1.8"></rect>
|
||||
<rect x="4" y="14" width="6" height="6" stroke-width="1.8"></rect>
|
||||
<rect
|
||||
x="14"
|
||||
y="14"
|
||||
width="6"
|
||||
height="6"
|
||||
stroke-width="1.8"
|
||||
></rect></svg
|
||||
>Command center</a
|
||||
>
|
||||
<a class="preview-nav__link" href="./service-catalog.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 20V5l8-2 8 2v15M4 20h16M8 8h2m4 0h2m-8 4h2m4 0h2m-8 4h2m4 0h2"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Service catalog</a
|
||||
>
|
||||
<a class="preview-nav__link" href="./request-lab.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="m8 8-4 4 4 4M16 8l4 4-4 4M14 4l-4 16"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Request lab</a
|
||||
>
|
||||
<a
|
||||
class="preview-nav__link preview-nav__link--active"
|
||||
href="#"
|
||||
aria-current="page"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Incident log</a
|
||||
>
|
||||
</nav>
|
||||
|
||||
<div class="preview-shell">
|
||||
<aside class="preview-sidebar" aria-label="Incident log navigation">
|
||||
<div class="preview-sidebar__section">Monitor</div>
|
||||
<a class="preview-sidebar__item" href="./command-center.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 13h4l2-7 4 12 2-5h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Command center</a
|
||||
>
|
||||
<a
|
||||
class="preview-sidebar__item preview-sidebar__item--active"
|
||||
href="#"
|
||||
aria-current="page"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Incident log</a
|
||||
>
|
||||
<div class="preview-sidebar__section">Services</div>
|
||||
<a class="preview-sidebar__item" href="./service-catalog.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 20V5l8-2 8 2v15M4 20h16M8 8h2m4 0h2m-8 4h2m4 0h2"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Service catalog</a
|
||||
>
|
||||
<a class="preview-sidebar__item" href="./request-lab.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="m8 8-4 4 4 4M16 8l4 4-4 4M14 4l-4 16"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Request lab</a
|
||||
>
|
||||
</aside>
|
||||
|
||||
<main class="preview-main">
|
||||
<div class="preview-section-title">
|
||||
<div>
|
||||
<div class="preview-card__meta">MONITORING / INCIDENTS</div>
|
||||
<h1
|
||||
class="preview-hero__title"
|
||||
style="color: var(--color-text); margin-top: 4px"
|
||||
>
|
||||
Incident log
|
||||
</h1>
|
||||
</div>
|
||||
<a
|
||||
class="preview-btn preview-btn--primary"
|
||||
href="#refresh-incidents"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M20 11a8 8 0 1 0 1 4M20 5v6h-6"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Refresh incidents</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="preview-action-bar"
|
||||
style="justify-content: flex-start; padding: 0; border-top: 0"
|
||||
>
|
||||
<label class="preview-form__input"
|
||||
><span class="preview-card__meta">Service</span
|
||||
><select aria-label="Filter by service">
|
||||
<option>All services</option>
|
||||
<option>Auth API</option>
|
||||
<option>Employee API</option>
|
||||
<option>Transit API</option>
|
||||
<option>Error Log API</option>
|
||||
<option>Admin UI</option>
|
||||
</select></label
|
||||
>
|
||||
<label class="preview-form__input"
|
||||
><span class="preview-card__meta">Severity</span
|
||||
><select aria-label="Filter by severity">
|
||||
<option>All severities</option>
|
||||
<option>Critical</option>
|
||||
<option>Error</option>
|
||||
<option>Warning</option>
|
||||
<option>Info</option>
|
||||
</select></label
|
||||
>
|
||||
<label class="preview-form__input"
|
||||
><span class="preview-card__meta">Window</span
|
||||
><select aria-label="Filter by time window">
|
||||
<option>Last 24 hours</option>
|
||||
<option>Last 7 days</option>
|
||||
<option>Last 30 days</option>
|
||||
</select></label
|
||||
>
|
||||
<span class="preview-card__meta"
|
||||
>3 incidents in the selected window</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="preview-tabs" role="tablist" aria-label="Incident views">
|
||||
<a
|
||||
class="preview-tabs__tab preview-tabs__tab--active"
|
||||
href="#all-incidents"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
>All incidents</a
|
||||
>
|
||||
<a
|
||||
class="preview-tabs__tab"
|
||||
href="#delivery-status"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
>Delivery status</a
|
||||
>
|
||||
</div>
|
||||
|
||||
<section id="all-incidents" aria-labelledby="all-incidents-heading">
|
||||
<div class="preview-section-title">
|
||||
<h2 class="preview-section-title__h" id="all-incidents-heading">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Recent activity
|
||||
</h2>
|
||||
<span class="preview-section-title__hint"
|
||||
>Normalized from five connected services</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-table-wrap">
|
||||
<table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Service</th>
|
||||
<th>Code</th>
|
||||
<th>Message</th>
|
||||
<th>Severity</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>09:42:18</strong></td>
|
||||
<td>Error Log API</td>
|
||||
<td><code>BAD_REQUEST</code></td>
|
||||
<td>Missing route identifier</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--warning"
|
||||
><span class="preview-badge__dot"></span>Warning</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>09:37:04</strong></td>
|
||||
<td>Transit API</td>
|
||||
<td><code>FORBIDDEN</code></td>
|
||||
<td>Admin role required</td>
|
||||
<td>
|
||||
<span class="preview-badge"
|
||||
><span class="preview-badge__dot"></span>Info</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>09:18:51</strong></td>
|
||||
<td>Employee API</td>
|
||||
<td><code>INTERNAL_ERROR</code></td>
|
||||
<td>Employee lookup timed out</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--danger"
|
||||
><span class="preview-badge__dot"></span>Critical</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="preview-card-list" aria-labelledby="severity-heading">
|
||||
<div class="preview-section-title">
|
||||
<h2 class="preview-section-title__h" id="severity-heading">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3 3 20h18L12 3ZM12 9v5m0 3h.01"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Severity summary
|
||||
</h2>
|
||||
<span class="preview-section-title__hint">Current window</span>
|
||||
</div>
|
||||
<div class="preview-card-grid">
|
||||
<article class="preview-card">
|
||||
<div class="preview-card__title">
|
||||
<svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3 3 20h18L12 3Zm0 6v5m0 3h.01"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Warning
|
||||
</div>
|
||||
<strong class="preview-kpi__value">1</strong>
|
||||
<p class="preview-card__body">
|
||||
Requires review before the next dispatch cycle.
|
||||
</p>
|
||||
</article>
|
||||
<article class="preview-card">
|
||||
<div class="preview-card__title">
|
||||
<svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="8.5" stroke-width="1.8"></circle>
|
||||
<path
|
||||
d="M12 8v4l2.5 2"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
Info
|
||||
</div>
|
||||
<strong class="preview-kpi__value">1</strong>
|
||||
<p class="preview-card__body">
|
||||
Access policy event recorded from Transit API.
|
||||
</p>
|
||||
</article>
|
||||
<article class="preview-card">
|
||||
<div class="preview-card__title">
|
||||
<svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3 3 20h18L12 3Zm0 6v5m0 3h.01"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Critical
|
||||
</div>
|
||||
<strong class="preview-kpi__value">1</strong>
|
||||
<p class="preview-card__body">
|
||||
Employee lookup timeout needs immediate attention.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="delivery-status"
|
||||
class="preview-error"
|
||||
aria-labelledby="delivery-heading"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon preview-icon--lg preview-error__icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3 3 20h18L12 3Zm0 6v5m0 3h.01"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="preview-error__body">
|
||||
<strong class="preview-error__title" id="delivery-heading"
|
||||
>Error log API delayed</strong
|
||||
><span class="preview-error__text"
|
||||
>Last successful pull was 3 minutes ago</span
|
||||
>
|
||||
</div>
|
||||
<a class="preview-btn preview-btn--secondary" href="#retry-delivery"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M20 11a8 8 0 1 0 1 4M20 5v6h-6"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Retry</a
|
||||
>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"generatedAt": "2026-09-20T00:00:00.000Z",
|
||||
"previewStatus": "generating",
|
||||
"pages": [
|
||||
{
|
||||
"slug": "command-center",
|
||||
"title": "Command Center",
|
||||
"route": "/",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"slug": "service-catalog",
|
||||
"title": "Service Catalog",
|
||||
"route": "/services",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"slug": "request-lab",
|
||||
"title": "Request Lab",
|
||||
"route": "/requests",
|
||||
"status": "pending"
|
||||
},
|
||||
{
|
||||
"slug": "incident-log",
|
||||
"title": "Incident Log",
|
||||
"route": "/incidents",
|
||||
"status": "pending"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,495 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Request Lab | XTransit Operations</title>
|
||||
<link rel="stylesheet" href="./theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-root">
|
||||
<header class="preview-header">
|
||||
<div class="preview-header__brand">
|
||||
<span class="preview-header__logo" aria-hidden="true">XT</span>
|
||||
<span>XTransit Operations</span>
|
||||
</div>
|
||||
<div class="preview-header__actions">
|
||||
<label class="preview-header__search" aria-label="Search operations">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="6.5" stroke-width="1.8"></circle>
|
||||
<path
|
||||
d="m16 16 4.2 4.2"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<input
|
||||
class="preview-form__input"
|
||||
type="search"
|
||||
placeholder="Search operations"
|
||||
value=""
|
||||
/>
|
||||
</label>
|
||||
<a class="preview-header__action" href="#" aria-label="Notifications">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18 9a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<span class="preview-avatar" aria-label="Signed in as Maya Chen"
|
||||
>MC</span
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="preview-nav" aria-label="Primary navigation">
|
||||
<a class="preview-nav__link" href="./command-center.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect
|
||||
x="4"
|
||||
y="4"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="14"
|
||||
y="4"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="4"
|
||||
y="14"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect>
|
||||
<rect
|
||||
x="14"
|
||||
y="14"
|
||||
width="6"
|
||||
height="6"
|
||||
rx="1"
|
||||
stroke-width="1.8"
|
||||
></rect></svg
|
||||
>Command Center</a
|
||||
>
|
||||
<a class="preview-nav__link" href="./service-catalog.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 5h14v14H5zM8 9h8M8 13h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Service Catalog</a
|
||||
>
|
||||
<a
|
||||
class="preview-nav__link preview-nav__link--active"
|
||||
href="#"
|
||||
aria-current="page"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 6h16M4 12h10M4 18h16"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
<circle cx="17" cy="12" r="2" stroke-width="1.8"></circle></svg
|
||||
>Request Lab</a
|
||||
>
|
||||
<a class="preview-nav__link" href="./incident-log.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 4 21 20H3L12 4Z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M12 9v5M12 17h.01"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path></svg
|
||||
>Incident Log</a
|
||||
>
|
||||
</nav>
|
||||
|
||||
<div class="preview-shell">
|
||||
<aside class="preview-sidebar" aria-label="Operations sections">
|
||||
<div class="preview-sidebar__section">Workspace</div>
|
||||
<a class="preview-sidebar__item" href="./command-center.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 13h6V4H4v9ZM14 20h6v-9h-6v9ZM14 8h6V4h-6v4ZM4 20h6v-3H4v3Z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Command Center</a
|
||||
>
|
||||
<a class="preview-sidebar__item" href="./service-catalog.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M7 4h10v4H7zM4 10h16v10H4zM8 14h8M8 17h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
stroke-linecap="round"
|
||||
></path></svg
|
||||
>Service Catalog</a
|
||||
>
|
||||
<a
|
||||
class="preview-sidebar__item preview-sidebar__item--active"
|
||||
href="#"
|
||||
aria-current="page"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 5h14v14H5zM8 9h8M8 13h5M8 17h8"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Request Lab</a
|
||||
>
|
||||
<a class="preview-sidebar__item" href="./incident-log.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 4 21 20H3L12 4Z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M12 9v5M12 17h.01"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path></svg
|
||||
>Incident Log</a
|
||||
>
|
||||
<div class="preview-sidebar__section">Tools</div>
|
||||
<a class="preview-sidebar__item" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
<circle cx="12" cy="12" r="3.5" stroke-width="1.8"></circle></svg
|
||||
>Environment</a
|
||||
>
|
||||
</aside>
|
||||
|
||||
<main class="preview-main">
|
||||
<div class="preview-section-title">
|
||||
<div>
|
||||
<div class="preview-section-title__h">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 5h16v14H4zM8 9h8M8 13h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Request Lab
|
||||
</div>
|
||||
<div class="preview-section-title__hint">
|
||||
Run a bounded request against an approved upstream route and
|
||||
inspect timing.
|
||||
</div>
|
||||
</div>
|
||||
<span class="preview-badge"
|
||||
><span class="preview-badge__dot"></span>Authenticated
|
||||
session</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="preview-split">
|
||||
<form class="preview-form" aria-label="Request builder">
|
||||
<div class="preview-section-title">
|
||||
<div class="preview-section-title__h">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 6h14M5 12h9M5 18h14"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
<circle cx="17" cy="12" r="2" stroke-width="1.8"></circle>
|
||||
</svg>
|
||||
Request builder
|
||||
</div>
|
||||
<span class="preview-section-title__hint"
|
||||
>Allowlisted routes only</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-form__field">
|
||||
<label class="preview-form__label" for="preset">Preset</label>
|
||||
<select class="preview-form__select" id="preset">
|
||||
<option>
|
||||
Transit API GET /schedules/overview Schedule summary
|
||||
</option>
|
||||
<option>Transit API GET /admin/drivers Driver roster</option>
|
||||
<option>Employee API GET /health Service health</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="preview-two-column">
|
||||
<div class="preview-form__field">
|
||||
<label class="preview-form__label" for="service"
|
||||
>Service</label
|
||||
>
|
||||
<select class="preview-form__select" id="service">
|
||||
<option>Transit API</option>
|
||||
<option>Employee API</option>
|
||||
<option>Auth API</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="preview-form__field">
|
||||
<label class="preview-form__label" for="method">Method</label>
|
||||
<select class="preview-form__select" id="method">
|
||||
<option>GET</option>
|
||||
<option>POST</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-form__field">
|
||||
<label class="preview-form__label" for="path">Path</label>
|
||||
<input
|
||||
class="preview-form__input"
|
||||
id="path"
|
||||
type="text"
|
||||
value="/schedules/overview"
|
||||
/>
|
||||
</div>
|
||||
<div class="preview-two-column">
|
||||
<div class="preview-form__field">
|
||||
<label class="preview-form__label" for="timeout"
|
||||
>Timeout</label
|
||||
>
|
||||
<input
|
||||
class="preview-form__input"
|
||||
id="timeout"
|
||||
type="number"
|
||||
value="5000"
|
||||
/>
|
||||
<span class="preview-form__hint">Milliseconds</span>
|
||||
</div>
|
||||
<div class="preview-form__field">
|
||||
<label class="preview-form__label" for="query"
|
||||
>Query fields</label
|
||||
>
|
||||
<input
|
||||
class="preview-form__input"
|
||||
id="query"
|
||||
type="text"
|
||||
value="date=2026-09-20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-form__field">
|
||||
<label class="preview-form__label" for="body">Body</label>
|
||||
<textarea
|
||||
class="preview-form__textarea"
|
||||
id="body"
|
||||
placeholder="No request body for this GET route"
|
||||
></textarea>
|
||||
<span class="preview-form__hint"
|
||||
>Safe limit: 10 requests per minute, 5-second timeout.</span
|
||||
>
|
||||
</div>
|
||||
<section class="preview-error" aria-label="Request response">
|
||||
<svg
|
||||
class="preview-icon preview-icon--lg preview-error__icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 4 21 20H3L12 4Z"
|
||||
stroke-width="1.8"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
<path
|
||||
d="M12 9v5M12 17h.01"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<div class="preview-error__body">
|
||||
<div class="preview-error__title">Request blocked</div>
|
||||
<div class="preview-error__text">
|
||||
Admin role required for /admin/drivers
|
||||
</div>
|
||||
<button
|
||||
class="preview-btn preview-btn--secondary"
|
||||
type="button"
|
||||
>
|
||||
Retry request
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<section class="preview-table-wrap" aria-labelledby="history-title">
|
||||
<div
|
||||
class="preview-section-title"
|
||||
style="padding: 20px 20px 12px"
|
||||
>
|
||||
<div class="preview-section-title__h" id="history-title">
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 5h16v15H4zM8 9h8M8 13h5M8 17h8"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
Request history
|
||||
</div>
|
||||
<span class="preview-section-title__hint"
|
||||
>Today, 09:00–12:00 UTC</span
|
||||
>
|
||||
</div>
|
||||
<table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Request</th>
|
||||
<th>Status</th>
|
||||
<th>Duration</th>
|
||||
<th>Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>GET</strong> /schedules/overview</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>200 OK</span
|
||||
>
|
||||
</td>
|
||||
<td>214 ms</td>
|
||||
<td>11:42:18</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>GET</strong> /admin/drivers</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--danger"
|
||||
>403</span
|
||||
>
|
||||
</td>
|
||||
<td>88 ms</td>
|
||||
<td>11:36:02</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>GET</strong> /health</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>200 OK</span
|
||||
>
|
||||
</td>
|
||||
<td>42 ms</td>
|
||||
<td>11:20:44</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>GET</strong> /schedules/overview</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>200 OK</span
|
||||
>
|
||||
</td>
|
||||
<td>197 ms</td>
|
||||
<td>10:58:09</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>GET</strong> /admin/drivers</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--danger"
|
||||
>403</span
|
||||
>
|
||||
</td>
|
||||
<td>91 ms</td>
|
||||
<td>10:41:27</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="preview-action-bar">
|
||||
<span class="preview-form__hint"
|
||||
>Requests are logged to the operations audit trail.</span
|
||||
>
|
||||
<button class="preview-btn preview-btn--secondary" type="button">
|
||||
Clear
|
||||
</button>
|
||||
<button class="preview-btn preview-btn--primary" type="submit">
|
||||
<svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="m5 12 5 5L20 7"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Run request
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,608 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Service Catalog | Transit Operations</title>
|
||||
<link rel="stylesheet" href="./theme.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="preview-root">
|
||||
<header class="preview-header">
|
||||
<div class="preview-header__brand">
|
||||
<div class="preview-header__logo" aria-hidden="true">TO</div>
|
||||
<span>Transit Operations</span>
|
||||
</div>
|
||||
<div class="preview-header__actions">
|
||||
<label
|
||||
class="preview-form__input"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
min-width: 220px;
|
||||
"
|
||||
aria-label="Search services"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="11" cy="11" r="6.5" stroke-width="1.8"></circle>
|
||||
<path
|
||||
d="m16 16 4.5 4.5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
></path>
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search services"
|
||||
style="
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
font: inherit;
|
||||
"
|
||||
/>
|
||||
</label>
|
||||
<a class="preview-header__action" href="#" aria-label="Notifications">
|
||||
<svg class="preview-icon" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M18 9a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</a>
|
||||
<div class="preview-avatar" aria-label="Signed in as Olivia Moore">
|
||||
OM
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="preview-nav" aria-label="Primary navigation">
|
||||
<a class="preview-nav__link" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 13h6V4H4v9Zm10 7h6v-9h-6v9ZM4 20h6v-3H4v3Zm10-12h6V4h-6v4Z"
|
||||
stroke-width="1.7"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Command Center</a
|
||||
>
|
||||
<a
|
||||
class="preview-nav__link preview-nav__link--active"
|
||||
href="./service-catalog.html"
|
||||
aria-current="page"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 5h14v14H5zM8 9h8M8 13h5M8 17h3"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Service Catalog</a
|
||||
>
|
||||
<a class="preview-nav__link" href="./request-lab.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Request Lab</a
|
||||
>
|
||||
<a class="preview-nav__link" href="./incident-log.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="m12 4 8 16H4L12 4Zm0 5v5M12 17v.5"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Incident Log</a
|
||||
>
|
||||
</nav>
|
||||
|
||||
<div class="preview-shell">
|
||||
<aside class="preview-sidebar" aria-label="Service catalog navigation">
|
||||
<div class="preview-sidebar__section">Monitor</div>
|
||||
<a class="preview-sidebar__item" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 13h6V4H4v9Zm10 7h6v-9h-6v9ZM4 20h6v-3H4v3Zm10-12h6V4h-6v4Z"
|
||||
stroke-width="1.7"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Command Center</a
|
||||
>
|
||||
<a
|
||||
class="preview-sidebar__item preview-sidebar__item--active"
|
||||
href="./service-catalog.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 5h14v14H5zM8 9h8M8 13h5M8 17h3"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Service Catalog</a
|
||||
>
|
||||
<a class="preview-sidebar__item" href="./request-lab.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 4h14v16H5zM8 8h8M8 12h8M8 16h5"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Request Lab</a
|
||||
>
|
||||
<a class="preview-sidebar__item" href="./incident-log.html"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="m12 4 8 16H4L12 4Zm0 5v5M12 17v.5"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Incident Log</a
|
||||
>
|
||||
<div class="preview-sidebar__section">Workspace</div>
|
||||
<a class="preview-sidebar__item" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
></path></svg
|
||||
>Route mappings</a
|
||||
>
|
||||
<a class="preview-sidebar__item" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 3 4 7v5c0 4.5 3.4 7.6 8 9 4.6-1.4 8-4.5 8-9V7l-8-4Zm0 5v4m0 4v.5"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Access policies</a
|
||||
>
|
||||
</aside>
|
||||
|
||||
<main class="preview-main">
|
||||
<div class="preview-section-title">
|
||||
<div>
|
||||
<div
|
||||
class="preview-section-title__h"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M6 4h12v16H6zM9 8h6M9 12h6M9 16h3"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Service Catalog
|
||||
</div>
|
||||
<p class="preview-card__body">
|
||||
Inspect upstream capabilities and representative route coverage
|
||||
across the transit platform.
|
||||
</p>
|
||||
</div>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
><span class="preview-badge__dot"></span>5 services
|
||||
monitored</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="preview-tabs" role="tablist" aria-label="Catalog views">
|
||||
<a
|
||||
class="preview-tabs__tab preview-tabs__tab--active"
|
||||
href="#catalog"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
>Catalog</a
|
||||
>
|
||||
<a
|
||||
class="preview-tabs__tab"
|
||||
href="#unmapped"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
>Unmapped routes <span class="preview-badge">0</span></a
|
||||
>
|
||||
</div>
|
||||
|
||||
<section id="catalog" aria-label="Service catalog">
|
||||
<div class="preview-section-title" style="margin-bottom: 12px">
|
||||
<h2
|
||||
class="preview-section-title__h"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 6h16v12H4zM8 10h8M8 14h5"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Monitored services
|
||||
</h2>
|
||||
<span class="preview-section-title__hint"
|
||||
>Last inventory sync 09:42 UTC</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-card-list">
|
||||
<article class="preview-card">
|
||||
<div class="preview-section-title">
|
||||
<h3
|
||||
class="preview-card__title"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 5h14v14H5zM8 9h8M8 13h5M8 17h3"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Auth API
|
||||
</h3>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Healthy</span
|
||||
>
|
||||
</div>
|
||||
<p class="preview-card__body">
|
||||
Identity and session access for operations staff.
|
||||
</p>
|
||||
<p class="preview-card__meta">4 endpoints · 2 route mappings</p>
|
||||
</article>
|
||||
<article class="preview-card">
|
||||
<div class="preview-section-title">
|
||||
<h3
|
||||
class="preview-card__title"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8Zm-7 8a7 7 0 0 1 14 0"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
></path></svg
|
||||
>Employee API
|
||||
</h3>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Healthy</span
|
||||
>
|
||||
</div>
|
||||
<p class="preview-card__body">
|
||||
Staff records, roles, and workforce administration.
|
||||
</p>
|
||||
<p class="preview-card__meta">7 endpoints · 1 route mapping</p>
|
||||
</article>
|
||||
<article class="preview-card">
|
||||
<div class="preview-section-title">
|
||||
<h3
|
||||
class="preview-card__title"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 17h16M5 17V8h14v9M8 8V5h8v3M8 20h8"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Transit API
|
||||
</h3>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Healthy</span
|
||||
>
|
||||
</div>
|
||||
<p class="preview-card__body">
|
||||
Routes, stops, fleet data, and daily schedules.
|
||||
</p>
|
||||
<p class="preview-card__meta">
|
||||
18 endpoints · 2 route mappings
|
||||
</p>
|
||||
</article>
|
||||
<article class="preview-card">
|
||||
<div class="preview-section-title">
|
||||
<h3
|
||||
class="preview-card__title"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 4h14v16H5zM8 8h8M8 12h5M8 16h3"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Error Log API
|
||||
</h3>
|
||||
<span class="preview-badge preview-badge--warning"
|
||||
>Degraded</span
|
||||
>
|
||||
</div>
|
||||
<p class="preview-card__body">
|
||||
Centralized application errors and diagnostic events.
|
||||
</p>
|
||||
<p class="preview-card__meta">3 endpoints · 1 route mapping</p>
|
||||
</article>
|
||||
<article class="preview-card">
|
||||
<div class="preview-section-title">
|
||||
<h3
|
||||
class="preview-card__title"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 5h16v14H4zM8 9h8M8 13h5M8 17h3"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Admin UI
|
||||
</h3>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Healthy</span
|
||||
>
|
||||
</div>
|
||||
<p class="preview-card__body">
|
||||
Signed-in administration workspace for transit teams.
|
||||
</p>
|
||||
<p class="preview-card__meta">12 screens · 0 route mappings</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="coverage-title">
|
||||
<div class="preview-section-title" style="margin-bottom: 12px">
|
||||
<h2
|
||||
id="coverage-title"
|
||||
class="preview-section-title__h"
|
||||
style="display: flex; align-items: center; gap: 8px"
|
||||
>
|
||||
<svg
|
||||
class="preview-icon"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M5 5h14v14H5zM8 9h8M8 13h8M8 17h4"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Representative route coverage
|
||||
</h2>
|
||||
<span class="preview-section-title__hint"
|
||||
>4 mapped capabilities</span
|
||||
>
|
||||
</div>
|
||||
<div class="preview-table-wrap">
|
||||
<table class="preview-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Service</th>
|
||||
<th>Capability</th>
|
||||
<th>Route</th>
|
||||
<th>Access</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Transit API</strong></td>
|
||||
<td>Daily schedules</td>
|
||||
<td><code>/schedule/daily</code></td>
|
||||
<td><span class="preview-badge">Authenticated</span></td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Mapped</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Transit API</strong></td>
|
||||
<td>Route detail</td>
|
||||
<td><code>/routes/:routeId</code></td>
|
||||
<td><span class="preview-badge">Authenticated</span></td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Mapped</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Employee API</strong></td>
|
||||
<td>Create employee</td>
|
||||
<td><code>/createemployee</code></td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--warning"
|
||||
>Admin</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Mapped</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Error Log API</strong></td>
|
||||
<td>Read error log</td>
|
||||
<td><code>/error_log</code></td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--warning"
|
||||
>Admin</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<span class="preview-badge preview-badge--success"
|
||||
>Mapped</span
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
id="unmapped"
|
||||
class="preview-empty"
|
||||
aria-label="Unmapped routes empty state"
|
||||
>
|
||||
<div class="preview-empty__icon">
|
||||
<svg
|
||||
class="preview-icon preview-icon--lg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M4 5h11v14H4zM8 9h3M8 13h3M17 15l4 4M15 13a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z"
|
||||
stroke-width="1.7"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="preview-empty__title">No unmapped routes</div>
|
||||
<div class="preview-empty__body">
|
||||
All discovered upstream capabilities are currently represented in
|
||||
the catalog.
|
||||
</div>
|
||||
<a class="preview-btn preview-btn--primary" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 5v14M5 12h14"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
></path></svg
|
||||
>Add route mapping</a
|
||||
>
|
||||
</section>
|
||||
|
||||
<div class="preview-action-bar">
|
||||
<span class="preview-section-title__hint"
|
||||
>Catalog source: route inventory and service manifests</span
|
||||
>
|
||||
<a class="preview-btn preview-btn--secondary" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 4v11m0 0 4-4m-4 4-4-4M5 19h14"
|
||||
stroke-width="1.8"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
></path></svg
|
||||
>Export catalog</a
|
||||
>
|
||||
<a class="preview-btn preview-btn--primary" href="#"
|
||||
><svg
|
||||
class="preview-icon preview-icon--sm"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M12 5v14M5 12h14"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
></path></svg
|
||||
>Add route mapping</a
|
||||
>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<footer class="preview-footer">
|
||||
<span>Transit Operations Console</span
|
||||
><span>Environment: Production · Read-only catalog</span>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,592 @@
|
||||
:root {
|
||||
--color-primary: #155eef;
|
||||
--color-on-primary: #ffffff;
|
||||
--color-accent: #f59e0b;
|
||||
--color-on-accent: #172033;
|
||||
--color-surface: #f6f8fb;
|
||||
--color-surface-raised: #ffffff;
|
||||
--color-surface-sunken: #eef2f7;
|
||||
--color-text: #172033;
|
||||
--color-muted: #667085;
|
||||
--color-border: #d9e0ea;
|
||||
--color-success: #16803c;
|
||||
--color-warning: #b54708;
|
||||
--color-danger: #d92d20;
|
||||
--font-body: "Segoe UI Variable", "Segoe UI", sans-serif;
|
||||
--font-heading: var(--font-body);
|
||||
--text-xs: 11px;
|
||||
--text-sm: 13px;
|
||||
--text-base: 14px;
|
||||
--text-lg: 17px;
|
||||
--text-xl: 22px;
|
||||
--text-2xl: 30px;
|
||||
--text-3xl: 38px;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-pill: 9999px;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-5: 20px;
|
||||
--space-6: 28px;
|
||||
--space-7: 40px;
|
||||
--space-8: 56px;
|
||||
--shadow-sm: 0 1px 2px rgba(23, 32, 51, 0.06);
|
||||
--shadow-md: 0 5px 16px rgba(23, 32, 51, 0.1);
|
||||
--shadow-lg: 0 16px 32px rgba(23, 32, 51, 0.14);
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-body);
|
||||
font-size: var(--text-base);
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-heading);
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
.preview-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.preview-shell {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.preview-main {
|
||||
flex: 1;
|
||||
padding: var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
min-width: 0;
|
||||
}
|
||||
.preview-split,
|
||||
.preview-two-column {
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
.preview-split {
|
||||
grid-template-columns: 1fr 2fr;
|
||||
}
|
||||
.preview-two-column {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.preview-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
background: var(--color-surface-raised);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.preview-header__brand,
|
||||
.preview-header__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.preview-header__brand {
|
||||
font-weight: 700;
|
||||
}
|
||||
.preview-header__logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-header__action {
|
||||
color: var(--color-muted);
|
||||
display: inline-flex;
|
||||
}
|
||||
.preview-nav {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-2) var(--space-6);
|
||||
background: var(--color-surface-raised);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.preview-nav__link,
|
||||
.preview-sidebar__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
color: var(--color-muted);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.preview-nav__link {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-nav__link--active,
|
||||
.preview-sidebar__item--active {
|
||||
color: var(--color-primary);
|
||||
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-sidebar {
|
||||
width: 220px;
|
||||
flex-shrink: 0;
|
||||
padding: var(--space-4) var(--space-3);
|
||||
background: var(--color-surface-sunken);
|
||||
border-right: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
.preview-sidebar__section {
|
||||
padding: var(--space-3) var(--space-2) var(--space-1);
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.preview-sidebar__item {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: var(--space-6);
|
||||
border-radius: var(--radius-md);
|
||||
background: linear-gradient(135deg, #155eef, #174ea6);
|
||||
color: var(--color-on-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.preview-hero__mesh {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.35;
|
||||
background:
|
||||
radial-gradient(60% 80% at 100% 0%, #7aa7ff, transparent 70%),
|
||||
radial-gradient(60% 80% at 0% 100%, #0e3b8e, transparent 70%);
|
||||
}
|
||||
.preview-hero > *:not(.preview-hero__mesh) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.preview-hero__eyebrow {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.preview-hero__title {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
max-width: 28ch;
|
||||
}
|
||||
.preview-hero__subtitle {
|
||||
max-width: 62ch;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.preview-hero__actions,
|
||||
.preview-action-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview-kpi-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.preview-kpi,
|
||||
.preview-card,
|
||||
.preview-form,
|
||||
.preview-empty,
|
||||
.preview-table-wrap {
|
||||
background: var(--color-surface-raised);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.preview-kpi {
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.preview-kpi__label {
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.preview-kpi__value {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
}
|
||||
.preview-kpi__delta {
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.preview-kpi__delta--up {
|
||||
color: var(--color-success);
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-section-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.preview-section-title__h {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
}
|
||||
.preview-section-title__hint {
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.preview-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
.preview-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.preview-card {
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
transition:
|
||||
transform 160ms ease,
|
||||
box-shadow 160ms ease;
|
||||
}
|
||||
.preview-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
.preview-card__title {
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-card__body,
|
||||
.preview-card__meta {
|
||||
margin: 0;
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-card__meta {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.preview-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
padding: 2px var(--space-2);
|
||||
border-radius: var(--radius-pill);
|
||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||
color: var(--color-primary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-badge--success {
|
||||
background: #e7f6ec;
|
||||
color: var(--color-success);
|
||||
}
|
||||
.preview-badge--warning {
|
||||
background: #fff4e5;
|
||||
color: var(--color-warning);
|
||||
}
|
||||
.preview-badge--danger {
|
||||
background: #feeceb;
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.preview-badge__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
.preview-form {
|
||||
padding: var(--space-5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.preview-form__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
.preview-form__label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-form__hint {
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
.preview-form__input,
|
||||
.preview-form__textarea,
|
||||
.preview-form__select {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
}
|
||||
.preview-form__textarea {
|
||||
min-height: 96px;
|
||||
resize: vertical;
|
||||
}
|
||||
.preview-form__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.preview-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
font: inherit;
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-btn--primary {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.preview-btn--secondary {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
.preview-btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
}
|
||||
.preview-table-wrap {
|
||||
overflow: hidden;
|
||||
}
|
||||
.preview-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.preview-table th,
|
||||
.preview-table td {
|
||||
text-align: left;
|
||||
padding: var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-table th {
|
||||
background: var(--color-surface-sunken);
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.preview-table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.preview-action-bar {
|
||||
justify-content: flex-end;
|
||||
padding-top: var(--space-3);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.preview-tabs {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
.preview-tabs__tab {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-tabs__tab--active {
|
||||
color: var(--color-primary);
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.preview-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-7) var(--space-4);
|
||||
text-align: center;
|
||||
}
|
||||
.preview-empty__icon {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: var(--radius-pill);
|
||||
background: #e6eeff;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.preview-empty__title {
|
||||
font-weight: 700;
|
||||
}
|
||||
.preview-empty__body {
|
||||
max-width: 40ch;
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-error {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-4);
|
||||
border: 1px solid #f4b8b4;
|
||||
border-radius: var(--radius-md);
|
||||
background: #fff4f3;
|
||||
}
|
||||
.preview-error__icon {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.preview-error__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
.preview-error__title {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 700;
|
||||
}
|
||||
.preview-error__text {
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.preview-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-3) var(--space-6);
|
||||
color: var(--color-muted);
|
||||
font-size: var(--text-xs);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
.preview-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
stroke: currentColor;
|
||||
fill: none;
|
||||
}
|
||||
.preview-icon--sm {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
.preview-icon--lg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.preview-avatar {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--color-primary);
|
||||
background: #e6eeff;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
.preview-skeleton {
|
||||
background: linear-gradient(90deg, #e8edf4 25%, #f7f9fc 37%, #e8edf4 63%);
|
||||
background-size: 400% 100%;
|
||||
border-radius: var(--radius-sm);
|
||||
animation: preview-shimmer 1.4s ease infinite;
|
||||
}
|
||||
.preview-skeleton--line {
|
||||
height: 12px;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.preview-skeleton--title {
|
||||
height: 18px;
|
||||
width: 40%;
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
.preview-skeleton--block {
|
||||
height: 96px;
|
||||
}
|
||||
@keyframes preview-shimmer {
|
||||
0% {
|
||||
background-position: 100% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.preview-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.preview-main {
|
||||
padding: var(--space-4);
|
||||
}
|
||||
.preview-nav {
|
||||
overflow-x: auto;
|
||||
padding: var(--space-2) var(--space-4);
|
||||
}
|
||||
.preview-split,
|
||||
.preview-two-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.preview-header {
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
.preview-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.preview-card,
|
||||
.preview-skeleton {
|
||||
transition: none;
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"schemaVersion": "2",
|
||||
"generatedAt": "2026-09-20T00:00:00.000Z",
|
||||
"mode": "NEW",
|
||||
"summary": "A new React + Vite monitoring dashboard for the existing XTransit APIs and admin UI. It will aggregate health, service capabilities, error activity, and a guarded API request tester without adding a new datastore.",
|
||||
"workspaceSignals": {
|
||||
"detectedFiles": [
|
||||
"README.md",
|
||||
"docker-compose.yml",
|
||||
"auth_api/index.js",
|
||||
"auth_api/package.json",
|
||||
"employee_api/index.js",
|
||||
"employee_api/package.json",
|
||||
"transit_api/index.js",
|
||||
"transit_api/package.json",
|
||||
"error_log_api/index.js",
|
||||
"error_log_api/package.json",
|
||||
"admin_ui/package.json",
|
||||
"admin_ui/src/api.js",
|
||||
"admin_ui/src/App.jsx",
|
||||
"DOMAINS_README.md",
|
||||
"admin_ui/README.md",
|
||||
"admin_ui/SCHEDULE_API.md",
|
||||
"transit_api/README.md"
|
||||
],
|
||||
"decision": "NEW",
|
||||
"decisionReason": "The repository contains the existing services and admin UI, but no monitoring dashboard; the requested dashboard is a new project folder within this workspace."
|
||||
},
|
||||
"services": [
|
||||
{
|
||||
"id": "transit-monitor-dashboard",
|
||||
"label": "Transit Monitor Dashboard",
|
||||
"role": "frontend",
|
||||
"root": "./monitoring_dashboard"
|
||||
},
|
||||
{
|
||||
"id": "monitoring-proxy-api",
|
||||
"label": "Monitoring Proxy API",
|
||||
"role": "backend",
|
||||
"root": "./monitoring_dashboard/server"
|
||||
}
|
||||
],
|
||||
"questions": [
|
||||
{
|
||||
"id": "transit-monitor-dashboard:language",
|
||||
"category": "service",
|
||||
"question": "Which programming language for Transit Monitor Dashboard?",
|
||||
"header": "Language",
|
||||
"answer": "TypeScript",
|
||||
"status": "confirmed",
|
||||
"rationale": "The requested framework is explicitly React + Vite, but the language was not specified; TypeScript is recommended for the dashboard's endpoint catalog and response-shape handling.",
|
||||
"options": [
|
||||
{
|
||||
"label": "TypeScript",
|
||||
"description": "Type-safe React application code"
|
||||
},
|
||||
{
|
||||
"label": "JavaScript",
|
||||
"description": "Plain JavaScript matching the existing admin UI"
|
||||
}
|
||||
],
|
||||
"recommendedChoice": "TypeScript",
|
||||
"multiSelect": false,
|
||||
"allowFreeformInput": false,
|
||||
"serviceId": "transit-monitor-dashboard"
|
||||
},
|
||||
{
|
||||
"id": "transit-monitor-dashboard:framework",
|
||||
"category": "service",
|
||||
"question": "Which frontend framework for Transit Monitor Dashboard?",
|
||||
"header": "Framework",
|
||||
"answer": "React + Vite",
|
||||
"status": "confirmed",
|
||||
"rationale": "React + Vite is explicitly requested.",
|
||||
"options": [
|
||||
{
|
||||
"label": "React + Vite",
|
||||
"description": "React SPA with Vite, as requested"
|
||||
},
|
||||
{
|
||||
"label": "Vue + Vite",
|
||||
"description": "Vue SPA with Vite"
|
||||
},
|
||||
{
|
||||
"label": "Angular",
|
||||
"description": "Angular application"
|
||||
},
|
||||
{
|
||||
"label": "Svelte",
|
||||
"description": "Svelte application with Vite"
|
||||
}
|
||||
],
|
||||
"recommendedChoice": "React + Vite",
|
||||
"multiSelect": false,
|
||||
"allowFreeformInput": true,
|
||||
"serviceId": "transit-monitor-dashboard"
|
||||
},
|
||||
{
|
||||
"id": "transit-monitor-dashboard:features",
|
||||
"category": "service",
|
||||
"question": "Describe the features or API routes for Transit Monitor Dashboard.",
|
||||
"header": "Features",
|
||||
"answer": "Service health overview for auth_api, employee_api, transit_api, error_log_api, and admin_ui; service capability catalog; error and bad-request views; guarded live request tester; configurable environments; polling controls; response timing and status history.",
|
||||
"status": "confirmed",
|
||||
"rationale": "Distilled from the requested monitoring, service-data, error, request-testing, and health-check workflows.",
|
||||
"recommendedChoice": "Service health overview for auth_api, employee_api, transit_api, error_log_api, and admin_ui; service capability catalog; error and bad-request views; guarded live request tester; configurable environments; polling controls; response timing and status history.",
|
||||
"multiSelect": false,
|
||||
"serviceId": "transit-monitor-dashboard"
|
||||
},
|
||||
{
|
||||
"id": "monitoring-proxy-api:language",
|
||||
"category": "service",
|
||||
"question": "Which programming language for Monitoring Proxy API?",
|
||||
"header": "Language",
|
||||
"answer": "TypeScript",
|
||||
"status": "confirmed",
|
||||
"rationale": "The workspace already uses Node.js services and the proxy needs shared typed endpoint metadata and response normalization; TypeScript is recommended, but the user did not explicitly choose the proxy language.",
|
||||
"options": [
|
||||
{
|
||||
"label": "TypeScript",
|
||||
"description": "Node.js + TypeScript proxy service"
|
||||
},
|
||||
{
|
||||
"label": "Python",
|
||||
"description": "Python proxy service"
|
||||
},
|
||||
{
|
||||
"label": "C# (.NET)",
|
||||
"description": ".NET proxy service"
|
||||
}
|
||||
],
|
||||
"recommendedChoice": "TypeScript",
|
||||
"multiSelect": false,
|
||||
"allowFreeformInput": false,
|
||||
"serviceId": "monitoring-proxy-api"
|
||||
},
|
||||
{
|
||||
"id": "monitoring-proxy-api:framework",
|
||||
"category": "service",
|
||||
"question": "Which backend framework for Monitoring Proxy API?",
|
||||
"header": "Framework",
|
||||
"answer": "Express",
|
||||
"status": "confirmed",
|
||||
"rationale": "Express matches the existing API services and is sufficient for an allowlisted proxy, health aggregation, and request execution endpoints.",
|
||||
"options": [
|
||||
{
|
||||
"label": "Express",
|
||||
"description": "Small Node.js HTTP proxy consistent with the existing APIs"
|
||||
},
|
||||
{
|
||||
"label": "Fastify",
|
||||
"description": "Typed, low-overhead Node.js HTTP service"
|
||||
},
|
||||
{
|
||||
"label": "Azure Functions",
|
||||
"description": "Serverless HTTP functions"
|
||||
}
|
||||
],
|
||||
"recommendedChoice": "Express",
|
||||
"multiSelect": false,
|
||||
"allowFreeformInput": true,
|
||||
"serviceId": "monitoring-proxy-api"
|
||||
},
|
||||
{
|
||||
"id": "monitoring-proxy-api:features",
|
||||
"category": "service",
|
||||
"question": "Describe the features or API routes for Monitoring Proxy API.",
|
||||
"header": "Features",
|
||||
"answer": "Environment-configured upstream base URLs; aggregated health checks including admin_ui; allowlisted endpoint catalog; authenticated request execution with safe method/body/timeout limits; error-log read proxy; redacted structured results; CORS and rate limiting.",
|
||||
"status": "confirmed",
|
||||
"rationale": "The proxy is planned to centralize cross-origin access and protect administrative/tester capabilities while preserving the existing APIs unchanged.",
|
||||
"recommendedChoice": "Environment-configured upstream base URLs; aggregated health checks including admin_ui; allowlisted endpoint catalog; authenticated request execution with safe method/body/timeout limits; error-log read proxy; redacted structured results; CORS and rate limiting.",
|
||||
"multiSelect": false,
|
||||
"serviceId": "monitoring-proxy-api"
|
||||
},
|
||||
{
|
||||
"id": "dataStores",
|
||||
"category": "data",
|
||||
"question": "Which data stores does your app need?",
|
||||
"header": "Data Stores",
|
||||
"answer": [
|
||||
"No datastore required"
|
||||
],
|
||||
"status": "confirmed",
|
||||
"rationale": "The dashboard monitors and queries the existing APIs and error-log database; it needs only bounded in-memory polling state and does not introduce persistence, file storage, queues, or caches.",
|
||||
"options": [
|
||||
{
|
||||
"label": "No datastore required",
|
||||
"description": "The dashboard reads existing APIs and does not persist new data",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"label": "Blob Storage",
|
||||
"description": "Store files and images"
|
||||
},
|
||||
{
|
||||
"label": "Queue Storage",
|
||||
"description": "Async message queue"
|
||||
},
|
||||
{
|
||||
"label": "PostgreSQL",
|
||||
"description": "Relational database"
|
||||
},
|
||||
{
|
||||
"label": "CosmosDB",
|
||||
"description": "NoSQL document database"
|
||||
},
|
||||
{
|
||||
"label": "Redis",
|
||||
"description": "In-memory cache"
|
||||
},
|
||||
{
|
||||
"label": "Azure SQL",
|
||||
"description": "Managed SQL Server"
|
||||
}
|
||||
],
|
||||
"recommendedChoice": [
|
||||
"No datastore required"
|
||||
],
|
||||
"multiSelect": true,
|
||||
"allowFreeformInput": false
|
||||
},
|
||||
{
|
||||
"id": "auth",
|
||||
"category": "auth",
|
||||
"question": "Does this app need user login?",
|
||||
"header": "API Login",
|
||||
"answer": "Yes",
|
||||
"status": "confirmed",
|
||||
"rationale": "Reading centralized errors and exercising authenticated transit/admin endpoints must be restricted to authorized dashboard users.",
|
||||
"options": [
|
||||
{
|
||||
"label": "Yes",
|
||||
"description": "Add a user sign-in flow and authenticated API access"
|
||||
},
|
||||
{
|
||||
"label": "No",
|
||||
"description": "No user sign-in or authenticated API access"
|
||||
}
|
||||
],
|
||||
"recommendedChoice": "Yes",
|
||||
"multiSelect": false,
|
||||
"allowFreeformInput": false
|
||||
}
|
||||
],
|
||||
"executionMode": "guided"
|
||||
}
|
||||
@@ -3,18 +3,18 @@ name: Build and Push Docker Images
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
- develop
|
||||
paths:
|
||||
- "auth_api/**"
|
||||
- "employee_api/**"
|
||||
- "transit_api/**"
|
||||
- "error_log_api/**"
|
||||
- "admin_ui/**"
|
||||
- "docker-compose.yml"
|
||||
- ".gitea/workflows/docker-build.yml"
|
||||
- auth-api
|
||||
- employee-api
|
||||
- transit-api
|
||||
- error-log-api
|
||||
- admin-ui
|
||||
- monitoring-ui
|
||||
pull_request:
|
||||
branches:
|
||||
- dev
|
||||
- main
|
||||
- develop
|
||||
|
||||
@@ -23,23 +23,55 @@ env:
|
||||
IMAGE_NAMESPACE: sven/the-transit-project
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: auth_api
|
||||
context: ./auth_api
|
||||
- name: employee_api
|
||||
context: ./employee_api
|
||||
- name: transit_api
|
||||
context: ./transit_api
|
||||
- name: error_log_api
|
||||
context: ./error_log_api
|
||||
- name: admin_ui
|
||||
context: ./admin_ui
|
||||
outputs:
|
||||
auth_api: ${{ steps.filter.outputs.auth_api }}
|
||||
employee_api: ${{ steps.filter.outputs.employee_api }}
|
||||
transit_api: ${{ steps.filter.outputs.transit_api }}
|
||||
error_log_api: ${{ steps.filter.outputs.error_log_api }}
|
||||
admin_ui: ${{ steps.filter.outputs.admin_ui }}
|
||||
monitoring_ui: ${{ steps.filter.outputs.monitoring_ui }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed paths
|
||||
id: filter
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
auth_api:
|
||||
- 'auth_api/**'
|
||||
- 'docker-compose.yml'
|
||||
- '.gitea/workflows/docker-build.yml'
|
||||
employee_api:
|
||||
- 'employee_api/**'
|
||||
- 'docker-compose.yml'
|
||||
- '.gitea/workflows/docker-build.yml'
|
||||
transit_api:
|
||||
- 'transit_api/**'
|
||||
- 'docker-compose.yml'
|
||||
- '.gitea/workflows/docker-build.yml'
|
||||
error_log_api:
|
||||
- 'error_log_api/**'
|
||||
- 'docker-compose.yml'
|
||||
- '.gitea/workflows/docker-build.yml'
|
||||
admin_ui:
|
||||
- 'admin_ui/**'
|
||||
- 'docker-compose.yml'
|
||||
- '.gitea/workflows/docker-build.yml'
|
||||
monitoring_ui:
|
||||
- 'monitoring_ui/**'
|
||||
- 'docker-compose.yml'
|
||||
- '.gitea/workflows/docker-build.yml'
|
||||
|
||||
build-auth-api:
|
||||
needs: detect-changes
|
||||
if: ${{ (gitea.ref_name == 'auth-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.auth_api == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -58,29 +90,224 @@ jobs:
|
||||
id: tags
|
||||
shell: bash
|
||||
run: |
|
||||
image_base="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.name }}"
|
||||
image_tags="${image_base}:${{ gitea.sha }}"
|
||||
|
||||
if [[ "${{ gitea.event_name }}" == "push" && "${{ gitea.ref_name }}" == "main" ]]; then
|
||||
image_tags+=$'\n'"${image_base}:latest"
|
||||
image_base="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/auth_api"
|
||||
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
|
||||
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
{
|
||||
echo 'tags<<EOF'
|
||||
echo "$image_tags"
|
||||
echo 'EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push
|
||||
- name: Build and push auth_api
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
context: ./auth_api
|
||||
push: ${{ gitea.event_name == 'push' }}
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
build-employee-api:
|
||||
needs: detect-changes
|
||||
if: ${{ (gitea.ref_name == 'employee-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.employee_api == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Set image tags
|
||||
id: tags
|
||||
shell: bash
|
||||
run: |
|
||||
image_base="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/employee_api"
|
||||
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
|
||||
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push employee_api
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./employee_api
|
||||
push: ${{ gitea.event_name == 'push' }}
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
build-transit-api:
|
||||
needs: detect-changes
|
||||
if: ${{ (gitea.ref_name == 'transit-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.transit_api == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Set image tags
|
||||
id: tags
|
||||
shell: bash
|
||||
run: |
|
||||
image_base="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/transit_api"
|
||||
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
|
||||
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push transit_api
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./transit_api
|
||||
push: ${{ gitea.event_name == 'push' }}
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
build-error-log-api:
|
||||
needs: detect-changes
|
||||
if: ${{ (gitea.ref_name == 'error-log-api' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.error_log_api == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Set image tags
|
||||
id: tags
|
||||
shell: bash
|
||||
run: |
|
||||
image_base="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/error_log_api"
|
||||
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
|
||||
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push error_log_api
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./error_log_api
|
||||
push: ${{ gitea.event_name == 'push' }}
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
build-admin-ui:
|
||||
needs: detect-changes
|
||||
if: ${{ (gitea.ref_name == 'admin-ui' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.admin_ui == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Set image tags
|
||||
id: tags
|
||||
shell: bash
|
||||
run: |
|
||||
image_base="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/admin_ui"
|
||||
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
|
||||
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push admin_ui
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./admin_ui
|
||||
push: ${{ gitea.event_name == 'push' }}
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
build-monitoring-ui:
|
||||
needs: detect-changes
|
||||
if: ${{ (gitea.ref_name == 'monitoring-ui' || gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop') && needs.detect-changes.outputs.monitoring_ui == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Gitea Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
|
||||
- name: Set image tags
|
||||
id: tags
|
||||
shell: bash
|
||||
run: |
|
||||
image_base="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/monitoring_ui"
|
||||
echo "tags<<EOF" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.ref_name }}" >> "$GITHUB_OUTPUT"
|
||||
echo "${image_base}:${{ gitea.sha }}" >> "$GITHUB_OUTPUT"
|
||||
if [[ "${{ gitea.ref_name }}" == "dev" || "${{ gitea.ref_name }}" == "main" || "${{ gitea.ref_name }}" == "develop" ]]; then
|
||||
echo "${image_base}:latest" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Build and push monitoring_ui
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./monitoring_ui
|
||||
push: ${{ gitea.event_name == 'push' }}
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
|
||||
deploy-test-node:
|
||||
needs: build-and-push
|
||||
if: ${{ gitea.event_name == 'push' && gitea.ref_name == 'main' }}
|
||||
needs:
|
||||
[
|
||||
detect-changes,
|
||||
build-auth-api,
|
||||
build-employee-api,
|
||||
build-transit-api,
|
||||
build-error-log-api,
|
||||
build-admin-ui,
|
||||
build-monitoring-ui,
|
||||
]
|
||||
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && gitea.event_name == 'push' && (gitea.ref_name == 'dev' || gitea.ref_name == 'main') }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
@@ -1,23 +1,63 @@
|
||||
# Environment and secrets
|
||||
.env
|
||||
/*.env
|
||||
transit_api/.env
|
||||
error_log_api/.env
|
||||
.env.*
|
||||
!.env.example
|
||||
**/.env
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
|
||||
# Local build / node_modules
|
||||
# Node.js
|
||||
node_modules/
|
||||
admin_ui/node_modules/
|
||||
admin_ui/dist/
|
||||
monitoring_ui/node_modules/
|
||||
monitoring_ui/dist/
|
||||
coverage/
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Sensitive docs to keep private
|
||||
ADMIN_DEPLOYMENT_SUMMARY.md
|
||||
CI_CD_PORTAINER_GUIDE.md
|
||||
DEPLOYMENT.md
|
||||
GETTING_STARTED.md
|
||||
handoff.md
|
||||
# Flutter / Dart
|
||||
**/.dart_tool/
|
||||
**/.pub-cache/
|
||||
**/.pub/
|
||||
**/build/
|
||||
**/.flutter-plugins
|
||||
**/.flutter-plugins-dependencies
|
||||
|
||||
# Database and local artifacts
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
*.db
|
||||
dump.sql
|
||||
|
||||
# Certificates and keys
|
||||
*.pem
|
||||
*.key
|
||||
*.crt
|
||||
*.p12
|
||||
|
||||
# OS/editor files
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Sensitive project docs & AI Agent Map
|
||||
PROJECT_MAP.md
|
||||
monitoring_ui/docs/ADMIN_DEPLOYMENT_SUMMARY.md
|
||||
monitoring_ui/docs/CI_CD_PORTAINER_GUIDE.md
|
||||
monitoring_ui/docs/DEPLOYMENT.md
|
||||
monitoring_ui/docs/GETTING_STARTED.md
|
||||
monitoring_ui/docs/handoff.md
|
||||
INTERNAL_*.md
|
||||
SETUP_*.md
|
||||
DEPLOY_*.md
|
||||
|
||||
# Custom instruction files (private)
|
||||
# Private instructions and tooling files
|
||||
.instructions.md
|
||||
.prompt.md
|
||||
.agent.md
|
||||
@@ -25,15 +65,30 @@ SKILL.md
|
||||
copilot-instructions.md
|
||||
AGENTS.md
|
||||
|
||||
# Private scripts
|
||||
# Local/private scripts
|
||||
scripts/private/
|
||||
scripts/*.local.*
|
||||
scripts/.env
|
||||
|
||||
# Private timetable note
|
||||
transit_api/LOCAL_BUS_TIMETABLE_README.md
|
||||
# Monitoring UI local configuration
|
||||
monitoring_ui/.env.local
|
||||
monitoring_ui/.env.*.local
|
||||
|
||||
# Runtime files
|
||||
*.log
|
||||
*.sqlite3
|
||||
.DS_Store
|
||||
# Project-specific private notes
|
||||
monitoring_ui/docs/transit_api/LOCAL_BUS_TIMETABLE_README.md
|
||||
|
||||
# Deprecated app backups and legacy artifacts
|
||||
deprecated/build/
|
||||
deprecated/.dart_tool/
|
||||
deprecated/.pub-cache/
|
||||
deprecated/.idea/
|
||||
deprecated/.vscode/
|
||||
|
||||
# Runtime logs
|
||||
auth_api.log
|
||||
transit_api.log
|
||||
error_log_api.log
|
||||
|
||||
# Temporary files
|
||||
tmp/
|
||||
temp/
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
# GitHub Actions → Docker Registry → Portainer Deployment
|
||||
|
||||
Automatisch Docker images bouwen via GitHub en deployen op Portainer.
|
||||
|
||||
## 📋 Setup Stappen
|
||||
|
||||
### 1. GitHub Repository voorbereiding
|
||||
|
||||
```bash
|
||||
# Push je code naar GitHub (met .github/workflows/docker-build.yml)
|
||||
git add .
|
||||
git commit -m "Add Docker build workflow"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
**Controleer:** https://github.com/YOUR_USERNAME/YOUR_REPO/actions
|
||||
|
||||
### 2. GitHub Container Registry inschakelen
|
||||
|
||||
✅ Automatisch als je private repo hebt. Voor public repos:
|
||||
|
||||
1. Ga naar GitHub repo → **Settings → Packages and registries**
|
||||
2. Of gewoon eerste push doen → workflow doet dit automatisch
|
||||
|
||||
### 3. GitHub Token configureren (optioneel)
|
||||
|
||||
Standaard gebruikt GitHub Actions je `GITHUB_TOKEN` (automatisch).
|
||||
|
||||
Voor meer controle, maak Personal Access Token (PAT):
|
||||
1. GitHub → **Settings → Developer settings → Personal access tokens**
|
||||
2. New token → Scopes: `write:packages`, `read:packages`
|
||||
3. Token kopieren
|
||||
|
||||
### 4. Docker Login op Portainer Server
|
||||
|
||||
```bash
|
||||
# Op je remote server waar Portainer draait
|
||||
docker login ghcr.io
|
||||
# Username: je GitHub username
|
||||
# Password: GITHUB_TOKEN of PAT token
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Image Bouwen & Pushen
|
||||
|
||||
### Automatisch (via GitHub Actions)
|
||||
|
||||
1. **Je code pushen naar main/develop:**
|
||||
```bash
|
||||
git push origin main
|
||||
```
|
||||
|
||||
2. **GitHub Actions bouwt automatisch:**
|
||||
- ✅ Checkout code
|
||||
- ✅ Build Docker images
|
||||
- ✅ Push naar ghcr.io
|
||||
- ✅ Tags: `latest`, branch name, git SHA
|
||||
|
||||
3. **Bekijk workflow:**
|
||||
- GitHub → **Actions tab** → Zie voortgang
|
||||
- Logs klikken voor debug info
|
||||
|
||||
### Images checken in registry
|
||||
|
||||
```bash
|
||||
# List available images/tags
|
||||
docker pull ghcr.io/YOUR_USERNAME/YOUR_REPO/auth_api:latest --dry-run
|
||||
|
||||
# of via GitHub CLI:
|
||||
gh api user/packages --jq '.[] | select(.package_type=="container")'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐳 Portainer Deployment
|
||||
|
||||
### Methode 1: Via Docker Compose (aanbevolen)
|
||||
|
||||
**Op je Portainer server:**
|
||||
|
||||
1. **Clone repo of kopie bestand:**
|
||||
```bash
|
||||
git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git
|
||||
cd YOUR_REPO
|
||||
```
|
||||
|
||||
2. **Login bij registry:**
|
||||
```bash
|
||||
docker login ghcr.io
|
||||
# Credentials: GitHub username + token
|
||||
```
|
||||
|
||||
3. **Start via docker-compose:**
|
||||
```bash
|
||||
docker-compose -f docker-compose.portainer.yml up -d
|
||||
```
|
||||
|
||||
### Methode 2: Via Portainer UI
|
||||
|
||||
1. **Portainer Web Dashboard → Stacks**
|
||||
2. **"Add Stack" → "Compose"**
|
||||
3. **Paste inhoud van `docker-compose.portainer.yml`**
|
||||
4. **Voeg environment vars toe:**
|
||||
- `POSTGRES_PASSWORD=...`
|
||||
- `JWT_SECRET=...`
|
||||
5. **Deploy**
|
||||
|
||||
### Methode 3: Private Registry in Portainer
|
||||
|
||||
1. **Portainer → Registries → Add Registry**
|
||||
- **Provider:** GitHub Container Registry
|
||||
- **URL:** `ghcr.io`
|
||||
- **Username:** je GitHub username
|
||||
- **Password:** GITHUB_TOKEN
|
||||
|
||||
2. **In docker-compose, images gebruiken:**
|
||||
```yaml
|
||||
image: ghcr.io/YOUR_USERNAME/YOUR_REPO/auth_api:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Workflow Overzicht
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ Push to GitHub │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ GitHub Actions Workflow: │
|
||||
│ - Checkout │
|
||||
│ - Build images │
|
||||
│ - Push to ghcr.io │
|
||||
└────────┬─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ ghcr.io Registry │
|
||||
│ - auth_api:latest │
|
||||
│ - employee_api:latest │
|
||||
│ - transit_api:latest │
|
||||
└────────┬─────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ Portainer (Remote Server) │
|
||||
│ docker-compose pull & up │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Image Tags Automatisering
|
||||
|
||||
GitHub Actions genereert automatisch tags:
|
||||
|
||||
| Event | Tag |
|
||||
|-------|-----|
|
||||
| Push naar main | `latest`, `main-{SHA}` |
|
||||
| Push naar develop | `develop-{SHA}` |
|
||||
| Release/Tag | `v1.0.0`, `1.0`, `1` |
|
||||
|
||||
**Voorbeeld:**
|
||||
```
|
||||
ghcr.io/sven/transit_project/auth_api:latest
|
||||
ghcr.io/sven/transit_project/auth_api:main-a1b2c3d
|
||||
ghcr.io/sven/transit_project/auth_api:v1.0.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuratie
|
||||
|
||||
### docker-compose.portainer.yml aanpassen
|
||||
|
||||
1. **Vervang dit:**
|
||||
```
|
||||
ghcr.io/YOUR_USERNAME/YOUR_REPO/auth_api:latest
|
||||
```
|
||||
|
||||
2. **Met jouw gegevens:**
|
||||
```
|
||||
ghcr.io/sven/transit_project/auth_api:latest
|
||||
```
|
||||
|
||||
3. **Voor private image, pull policy toevoegen:**
|
||||
```yaml
|
||||
auth_api:
|
||||
image: ghcr.io/sven/transit_project/auth_api:latest
|
||||
pull_policy: always
|
||||
```
|
||||
|
||||
### Environment variabelen
|
||||
|
||||
**Maak .env file op Portainer server:**
|
||||
```bash
|
||||
POSTGRES_DB=transit_db
|
||||
POSTGRES_USER=transit_user
|
||||
POSTGRES_PASSWORD=super_secure_password
|
||||
JWT_SECRET=your_secret_key_here
|
||||
```
|
||||
|
||||
**Dan starten:**
|
||||
```bash
|
||||
docker-compose --env-file .env -f docker-compose.portainer.yml up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Veiligheid
|
||||
|
||||
### GitHub Token limieten
|
||||
- Standaard `GITHUB_TOKEN` werkt goed
|
||||
- Verloopt na 60 minuten (genoeg voor build)
|
||||
|
||||
### Private Registry
|
||||
Als je Portainer server geen internet heeft:
|
||||
- Use **Portainer Registry Mirror**
|
||||
- Of **Private Registry** (Harbor, Nexus)
|
||||
|
||||
### Credentials in Portainer
|
||||
**Nooit credentials in docker-compose zetten!**
|
||||
|
||||
✅ Juist: Environment file
|
||||
```bash
|
||||
docker-compose --env-file .env up -d
|
||||
```
|
||||
|
||||
❌ Fout:
|
||||
```yaml
|
||||
environment:
|
||||
POSTGRES_PASSWORD: password123 # DON'T DO THIS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### "Image not found"
|
||||
```bash
|
||||
# Check of image in registry staat
|
||||
docker pull ghcr.io/USERNAME/REPO/auth_api:latest
|
||||
|
||||
# Kijk in GitHub Actions logs
|
||||
# Check je IMAGE naam in workflow
|
||||
```
|
||||
|
||||
### "Unauthorized: authentication required"
|
||||
```bash
|
||||
# Login opnieuw
|
||||
docker login ghcr.io
|
||||
|
||||
# Check token permissies (scope: write:packages)
|
||||
```
|
||||
|
||||
### "Connection refused" naar database
|
||||
```bash
|
||||
# Check Docker network
|
||||
docker network ls
|
||||
docker network inspect YOUR_NETWORK
|
||||
|
||||
# Check database running
|
||||
docker ps | grep postgres
|
||||
```
|
||||
|
||||
### Logs bekijken
|
||||
```bash
|
||||
# Via docker
|
||||
docker logs auth_api
|
||||
|
||||
# Via Portainer: Containers → Select → Logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Nuttige Commando's
|
||||
|
||||
```bash
|
||||
# Check beschikbare images/tags
|
||||
curl -s -H "Authorization: Bearer TOKEN" \
|
||||
https://ghcr.io/v2/USERNAME/REPO/auth_api/tags/list | jq
|
||||
|
||||
# Manual image build (als je workflow debug)
|
||||
docker build -t ghcr.io/USERNAME/REPO/auth_api:test ./auth_api
|
||||
docker push ghcr.io/USERNAME/REPO/auth_api:test
|
||||
|
||||
# Pull latest images
|
||||
docker-compose -f docker-compose.portainer.yml pull
|
||||
|
||||
# Recreate containers (na pull)
|
||||
docker-compose -f docker-compose.portainer.yml up -d --force-recreate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Samenvatting
|
||||
|
||||
| Stap | Wat | Waar |
|
||||
|------|-----|------|
|
||||
| 1 | Code pushen | GitHub |
|
||||
| 2 | Images bouwen | GitHub Actions |
|
||||
| 3 | Images pushen | ghcr.io registry |
|
||||
| 4 | Images pullen | Portainer server |
|
||||
| 5 | Containers starten | Docker Compose |
|
||||
|
||||
**Voordelen:**
|
||||
- ✅ Volledig geautomatiseerd
|
||||
- ✅ Versiebeheer via git tags
|
||||
- ✅ Makkelijk rollback
|
||||
- ✅ Geen lokale builds nodig
|
||||
- ✅ CI/CD best practices
|
||||
|
||||
@@ -1,240 +0,0 @@
|
||||
# Docker Setup voor Transit Project APIs
|
||||
|
||||
Dit project bevat Docker-configuratie voor alle APIs, Admin Control Center en PostgreSQL database.
|
||||
|
||||
## Bestanden
|
||||
|
||||
- **auth_api/Dockerfile** - Image voor Authentication API (port 5000)
|
||||
- **employee_api/Dockerfile** - Image voor Employee API (port 4000)
|
||||
- **transit_api/Dockerfile** - Image voor Transit API (port 5001)
|
||||
- **admin_ui/Dockerfile** - Image voor Admin Control Center (port 5174)
|
||||
- **docker-compose.yml** - Orchestratie van alle services
|
||||
- **.dockerignore** - Bestanden die niet in Docker images opgenomen worden
|
||||
- **.env.example** - Template voor environment variabelen
|
||||
- **DEPLOYMENT.md** - Gedetailleerde deployment guide
|
||||
- **scripts/deploy.sh** - Deployment helper script
|
||||
- **scripts/build-and-push.sh** - Build en push script voor registry
|
||||
|
||||
## Vereisten
|
||||
|
||||
- Docker (>=20.10)
|
||||
- Docker Compose (>=2.0)
|
||||
|
||||
## Installatie
|
||||
|
||||
### 1. Environment variabelen configureren
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Bewerk .env met je eigen waarden
|
||||
```
|
||||
|
||||
### 2. Services starten
|
||||
|
||||
```bash
|
||||
# Start alle services (APIs + Admin UI)
|
||||
docker-compose up -d
|
||||
|
||||
# Of build en start tegelijk (als je code hebt aangepast)
|
||||
docker-compose up -d --build
|
||||
|
||||
# Quick start met script
|
||||
./scripts/deploy.sh start
|
||||
```
|
||||
|
||||
### 3. Logs bekijken
|
||||
|
||||
```bash
|
||||
# Alle logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Logs van specifieke service
|
||||
docker-compose logs -f auth_api
|
||||
docker-compose logs -f transit_api
|
||||
docker-compose logs -f admin_ui
|
||||
|
||||
# Via script
|
||||
./scripts/deploy.sh logs admin_ui
|
||||
```
|
||||
|
||||
## Beschikbare Services
|
||||
|
||||
| Service | Port | Beschrijving |
|
||||
|---------|------|--------------|
|
||||
| admin_ui | 5174 | Admin Control Center (React + Nginx) |
|
||||
| auth_api | 5000 | Authentication endpoints |
|
||||
| employee_api | 4000 | Employee management endpoints |
|
||||
| transit_api | 5001 | Transit management endpoints |
|
||||
|
||||
## Commando's
|
||||
|
||||
### Services beheren (Quick way)
|
||||
|
||||
```bash
|
||||
# Via helper script
|
||||
./scripts/deploy.sh start # Start alle services
|
||||
./scripts/deploy.sh stop # Stop alle services
|
||||
./scripts/deploy.sh restart # Herstart alle services
|
||||
./scripts/deploy.sh status # Show status
|
||||
./scripts/deploy.sh logs # Bekijk logs admin_ui
|
||||
./scripts/deploy.sh logs transit_api # Bekijk logs specifieke service
|
||||
```
|
||||
|
||||
### Services beheren (Docker compose)
|
||||
|
||||
```bash
|
||||
# Start services
|
||||
docker-compose up -d
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
|
||||
# Herstart services
|
||||
docker-compose restart
|
||||
|
||||
# Verwijder containers en volumes
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Individuele services
|
||||
|
||||
```bash
|
||||
# Herstart specifieke service
|
||||
docker-compose restart auth_api
|
||||
docker-compose restart admin_ui
|
||||
|
||||
# Build specifieke image
|
||||
docker-compose build auth_api
|
||||
|
||||
# Logs van specifieke service
|
||||
docker-compose logs -f transit_api
|
||||
```
|
||||
|
||||
### Admin UI
|
||||
|
||||
```bash
|
||||
# Bekijk Admin UI
|
||||
http://localhost:5174
|
||||
|
||||
# Logs
|
||||
docker-compose logs -f admin_ui
|
||||
|
||||
# Shell in container
|
||||
docker-compose exec admin_ui sh
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Port al in gebruik
|
||||
```bash
|
||||
# Check welke container de port gebruikt
|
||||
docker ps
|
||||
|
||||
# Verander port in docker-compose.yml (e.g., "5000:5000" naar "5010:5000")
|
||||
```
|
||||
|
||||
### Database connectie fout
|
||||
```bash
|
||||
# Check database status
|
||||
docker-compose logs db
|
||||
|
||||
# Herstart database
|
||||
docker-compose restart db
|
||||
```
|
||||
|
||||
### API start probleem
|
||||
```bash
|
||||
# Check logs
|
||||
docker-compose logs auth_api
|
||||
|
||||
# Controleer environment variabelen in .env
|
||||
# Controleer database connection string
|
||||
```
|
||||
|
||||
### Admin UI niet bereikbaar
|
||||
```bash
|
||||
# Check Admin UI container
|
||||
docker-compose logs admin_ui
|
||||
|
||||
# Zorg ervoor dat poort 5174 niet in gebruik is
|
||||
lsof -i :5174
|
||||
```
|
||||
|
||||
## Admin Control Center
|
||||
|
||||
De Admin UI is een modern React + Vite applicatie die alle admin functies biedt.
|
||||
|
||||
### Features
|
||||
|
||||
- **Authenticatie**: JWT-based login met dev token helper
|
||||
- **Stops Management**: Create en beheer transit stops met geo-coördinaten
|
||||
- **Bus Lines**: Define bus routes met start/end stops en reistijd
|
||||
- **Fleet Management**: Monitor fleet vehicles
|
||||
- **Stop Linking**: Assign stops aan bus lines met volgorde en ETA
|
||||
|
||||
### Toegang
|
||||
|
||||
```
|
||||
URL: http://localhost:5174
|
||||
```
|
||||
|
||||
### Deployment Configuratie
|
||||
|
||||
Environment variabelen in docker-compose.yml:
|
||||
- `VITE_AUTH_API`: URL naar auth_api (standaard: http://auth_api:5000)
|
||||
- `VITE_TRANSIT_API`: URL naar transit_api (standaard: http://transit_api:5001)
|
||||
|
||||
Voor externe toegang, update de URLs naar je deployment domain.
|
||||
|
||||
### Build en Deployment
|
||||
|
||||
```bash
|
||||
# Build Admin UI image
|
||||
docker-compose build admin_ui
|
||||
|
||||
# Push naar registry
|
||||
./scripts/build-and-push.sh ghcr.io/your-org/xtransit latest
|
||||
|
||||
# Deploy met updated image
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Wijzigingen doorgevoerd in code
|
||||
|
||||
```bash
|
||||
# Rebuild container
|
||||
docker-compose build auth_api
|
||||
|
||||
# Herstart service
|
||||
docker-compose restart auth_api
|
||||
```
|
||||
|
||||
### Code reloading (hot reload)
|
||||
|
||||
Voor hot reload kunt u volumes toevoegen aan docker-compose.yml:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./auth_api:/app
|
||||
```
|
||||
|
||||
Dit kan performance beïnvloeden. Alleen gebruiken tijdens development.
|
||||
|
||||
## Productie Tips
|
||||
|
||||
1. **Veiligheid**: Wijzig alle wachtwoorden en JWT_SECRET in .env
|
||||
2. **Database**: Gebruik een managed database service in productie
|
||||
3. **Logging**: Configureer centralized logging (ELK stack, Splunk, etc.)
|
||||
4. **Backups**: Stel automatische backups in voor de database
|
||||
5. **Health Checks**: Monitoreer de health endpoints van de APIs
|
||||
6. **SSL/TLS**: Gebruik reverse proxy (nginx) voor HTTPS
|
||||
|
||||
## Netwerking
|
||||
|
||||
De services kunnen elkaar bereiken via:
|
||||
- auth_api: `http://auth_api:5000`
|
||||
- employee_api: `http://employee_api:4000`
|
||||
- transit_api: `http://transit_api:5001`
|
||||
- database: `db:5432`
|
||||
@@ -1,157 +0,0 @@
|
||||
# Quick Start Guide - XTransit Admin Control Center
|
||||
|
||||
## 🚀 5-Minute Setup
|
||||
|
||||
### Step 1: Initial Setup
|
||||
```bash
|
||||
# From the project root
|
||||
./scripts/init.sh
|
||||
```
|
||||
|
||||
This will:
|
||||
- Check Docker and Docker Compose installation
|
||||
- Create `.env` file from template
|
||||
- Build all Docker images
|
||||
- Prompt you to configure database credentials
|
||||
|
||||
### Step 2: Configure Database Connection
|
||||
|
||||
Edit `.env` with your database details:
|
||||
```env
|
||||
POSTGRES_HOST=your-db-host.com
|
||||
POSTGRES_USER_AUTH=transitAUTH
|
||||
POSTGRES_PASSWORD_AUTH=your_password
|
||||
POSTGRES_USER_TRANSIT=transitAPI
|
||||
POSTGRES_PASSWORD_TRANSIT=your_password
|
||||
JWT_SECRET=your-secure-secret
|
||||
# ADMIN_USER_CODES should be set in auth_api/.env (comma-separated admin user codes)
|
||||
```
|
||||
|
||||
### Step 3: Start Services
|
||||
```bash
|
||||
./scripts/deploy.sh start
|
||||
```
|
||||
|
||||
### Step 4: Access Admin UI
|
||||
```
|
||||
http://localhost:5174
|
||||
```
|
||||
|
||||
Log in with your real user code and password.
|
||||
|
||||
## 📱 Admin UI Features
|
||||
|
||||
### Stops Management
|
||||
- Create new transit stops
|
||||
- Set geolocation (latitude, longitude)
|
||||
- View all stops
|
||||
- Link stops to bus lines
|
||||
|
||||
### Bus Lines
|
||||
- Create new bus routes
|
||||
- Define start and end stops
|
||||
- Set estimated travel time
|
||||
- Assign stops to lines with order
|
||||
|
||||
### Fleet
|
||||
- View fleet vehicles (read-only)
|
||||
- See vehicle details
|
||||
|
||||
## 🐳 Docker Services
|
||||
|
||||
| Port | Service | URL |
|
||||
|------|---------|-----|
|
||||
| 5174 | Admin UI | http://localhost:5174 |
|
||||
| 5000 | Auth API | http://localhost:5000 |
|
||||
| 5001 | Transit API | http://localhost:5001 |
|
||||
| 4000 | Employee API | http://localhost:4000 |
|
||||
|
||||
## 🔧 Common Commands
|
||||
|
||||
```bash
|
||||
# View service status
|
||||
./scripts/deploy.sh status
|
||||
|
||||
# Restart all services
|
||||
./scripts/deploy.sh restart
|
||||
|
||||
# View logs
|
||||
./scripts/deploy.sh logs admin_ui
|
||||
./scripts/deploy.sh logs transit_api
|
||||
|
||||
# Stop services
|
||||
./scripts/deploy.sh stop
|
||||
|
||||
# Clean up containers and volumes
|
||||
./scripts/deploy.sh clean
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
Use the real login flow with user code and password.
|
||||
|
||||
### Production
|
||||
- Keep `DEV_BYPASS=0` and `DEV_MOCK=0`
|
||||
- Configure proper admin user codes in `ADMIN_USER_CODES`
|
||||
- Ensure JWTs include the `userCode` claim
|
||||
|
||||
## 📝 Configuration Files
|
||||
|
||||
- **docker-compose.yml** - All service definitions
|
||||
- **.env** - Database and API credentials
|
||||
- **DEPLOYMENT.md** - Comprehensive deployment guide
|
||||
- **DOCKER_README.md** - Docker-specific documentation
|
||||
|
||||
## ⚠️ Troubleshooting
|
||||
|
||||
### "Address already in use"
|
||||
```bash
|
||||
# Find and stop the process using the port
|
||||
lsof -i :5174 # Find process on port 5174
|
||||
kill -9 <PID> # Kill the process
|
||||
```
|
||||
|
||||
### "Cannot connect to database"
|
||||
```bash
|
||||
# Check database credentials in .env
|
||||
# Verify database is accessible from your machine
|
||||
# Ensure database user exists and has proper permissions
|
||||
```
|
||||
|
||||
### "Admin UI not loading"
|
||||
```bash
|
||||
# Check container is running
|
||||
docker-compose ps admin_ui
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f admin_ui
|
||||
|
||||
# Restart service
|
||||
docker-compose restart admin_ui
|
||||
```
|
||||
|
||||
### "Permission denied" errors
|
||||
The transit API user needs proper database permissions:
|
||||
```sql
|
||||
-- Grant permissions to transitAPI user
|
||||
GRANT CONNECT ON DATABASE "XTransit" TO transitAPI;
|
||||
GRANT USAGE ON SCHEMA public TO transitAPI;
|
||||
GRANT SELECT, INSERT, UPDATE ON all tables IN SCHEMA public TO transitAPI;
|
||||
```
|
||||
|
||||
## 📚 For More Information
|
||||
|
||||
- **DEPLOYMENT.md** - Full deployment guide for production
|
||||
- **DOCKER_README.md** - Docker and container information
|
||||
- **admin_ui/README.md** - Admin UI specific details
|
||||
|
||||
## 🎯 Next Steps
|
||||
|
||||
1. ✅ Run initial setup script
|
||||
2. ✅ Configure database credentials
|
||||
3. ✅ Start services
|
||||
4. ✅ Access Admin UI at http://localhost:5174
|
||||
5. ✅ Create test stops and bus lines
|
||||
6. ✅ Test the admin interface
|
||||
|
||||
For production deployment, see **DEPLOYMENT.md** for security and configuration best practices.
|
||||
@@ -1,21 +1,37 @@
|
||||
# Transit Project
|
||||
|
||||
Dit repository bevat een suite van drie Node.js microservices (APIs) en bijbehorende tooling:
|
||||
This repository contains a suite of Node.js microservices, a modern React administration console, an operational monitoring dashboard, and a legacy client retained for compatibility reference.
|
||||
|
||||
- `auth_api` — Authenticatie en token-uitgifte (standaard port 5000)
|
||||
- `employee_api` — Employee onboarding / management (standaard port 4000)
|
||||
- `transit_api` — Transit/schedule endpoints (standaard port 5001)
|
||||
- `error_log_api` — Centrale foutlogging en viewer (standaard port 5003)
|
||||
- `auth_api` — Authentication and token issuance (default port 5000)
|
||||
- `employee_api` — Employee onboarding and management (default port 4000)
|
||||
- `transit_api` — Routes, schedules, fleet, GPS, and transit administration (default port 5001)
|
||||
- `error_log_api` — Centralized error logging and viewer (default port 5003)
|
||||
|
||||
Er is ook een GitHub Actions workflow aanwezig die Docker images bouwt en pusht naar GitHub Container Registry (GHCR).
|
||||
The error API is available at `https://error.xtransit.testinstance.nl`.
|
||||
|
||||
Belangrijk: deze README bevat geen wachtwoorden of andere gevoelige gegevens. Sla secrets altijd buiten versiebeheer op (environment variables, secret managers, Portainer secrets, etc.).
|
||||
- `admin_ui` — React + Vite administration interface
|
||||
|
||||
The admin panel is available at `https://admin.xtransit.testinstance.nl`.
|
||||
|
||||
- `monitoring_ui` — React + Vite service monitoring dashboard
|
||||
|
||||
The monitoring dashboard is available at host port 5175 (`https://monitor.xtransit.testinstance.nl` or local port 5173).
|
||||
|
||||
- `windows_xp_driver_ui/` — Native Win32 driver client targeting Windows XP-era systems
|
||||
- `deprecated/` — Legacy Flutter bus terminal, retained for reference while moving the client to a Windows XP-era build path
|
||||
|
||||
The repository also includes CI workflows that build Docker images and push them to GitHub Container Registry (GHCR).
|
||||
|
||||
See [monitoring_ui/docs/DOMAINS_README.md](monitoring_ui/docs/DOMAINS_README.md) for the central list of all public domains and their functions.
|
||||
The consolidated documentation hub is available in [monitoring_ui/docs/README.md](monitoring_ui/docs/README.md).
|
||||
|
||||
Important: this README does not contain any passwords or other sensitive information. Always store secrets outside of version control (environment variables, secret managers, Portainer secrets, etc.).
|
||||
|
||||
---
|
||||
|
||||
## Security & Private Files
|
||||
|
||||
**⚠️ Important:** This repository excludes private documentation and setup instructions from Git and Docker builds. See [SECURITY_PRIVATE_FILES.md](SECURITY_PRIVATE_FILES.md) for details on:
|
||||
**⚠️ Important:** This repository excludes private documentation and setup instructions from Git and Docker builds. See [monitoring_ui/docs/SECURITY_PRIVATE_FILES.md](monitoring_ui/docs/SECURITY_PRIVATE_FILES.md) for details on:
|
||||
|
||||
- What files are kept private and why
|
||||
- How `.gitignore` and `.dockerignore` protect sensitive information
|
||||
@@ -23,6 +39,7 @@ Belangrijk: deze README bevat geen wachtwoorden of andere gevoelige gegevens. Sl
|
||||
- Guidelines for adding new private files
|
||||
|
||||
**Private files are excluded** (not in git/docker):
|
||||
|
||||
- `GETTING_STARTED.md` - Internal setup guide
|
||||
- `ADMIN_DEPLOYMENT_SUMMARY.md` - Deployment status
|
||||
- `CI_CD_PORTAINER_GUIDE.md` - Infrastructure docs
|
||||
@@ -31,47 +48,52 @@ Belangrijk: deze README bevat geen wachtwoorden of andere gevoelige gegevens. Sl
|
||||
|
||||
---
|
||||
|
||||
## Overzicht van inhoud
|
||||
## Table of Contents / Directory Overview
|
||||
|
||||
- `auth_api/` — source voor auth service
|
||||
- `employee_api/` — source voor employee service
|
||||
- `transit_api/` — source voor transit service
|
||||
- `.github/workflows/docker-build.yml` — CI: bouw en push images naar GHCR
|
||||
- `docker-compose.yml` — compose file die (in deze workspace-versie) images trekt van `ghcr.io/...` en verwacht dat je een externe PostgreSQL hebt draaien
|
||||
- `.env.example` — voorbeeld environment variabelen
|
||||
- `DOCKER_README.md` & `CI_CD_PORTAINER_GUIDE.md` — extra docs voor Docker/CI
|
||||
- `auth_api/` — Source code for the authentication service
|
||||
- `employee_api/` — Source code for the employee service
|
||||
- `transit_api/` — Source code for the transit service
|
||||
- `error_log_api/` — Source code for centralized error collection
|
||||
- `admin_ui/` — Source code for React admin panel
|
||||
- `monitoring_ui/` — Source code for React monitoring dashboard and documentation hub
|
||||
- `windows_xp_driver_ui/` — Native driver interface
|
||||
- `deprecated/` — Legacy client reference
|
||||
- `.github/workflows/docker-build.yml` — CI: build and push images to GHCR
|
||||
- `docker-compose.yml` — Compose file that pulls images from GHCR or builds locally and connects to an external PostgreSQL database
|
||||
- `.env.example` — Example environment variables
|
||||
- `monitoring_ui/docs/` — Consolidated documentation hub
|
||||
|
||||
---
|
||||
|
||||
## Vereisten
|
||||
## Prerequisites
|
||||
|
||||
- Git
|
||||
- Docker (20.10+)
|
||||
- Docker Compose (v2+ of `docker compose`)
|
||||
- (Optioneel) een remote omgeving met Portainer
|
||||
- (Optioneel) Een GitHub account met rechten om images naar GHCR te publiceren
|
||||
- Docker Compose (v2+ or `docker compose`)
|
||||
- (Optional) A remote environment running Portainer
|
||||
- (Optional) A GitHub account with permissions to publish images to GHCR
|
||||
|
||||
---
|
||||
|
||||
## Veiligheid — belangrijke reminders
|
||||
## Security — Important Reminders
|
||||
|
||||
- Plaats geen secrets in git. Gebruik een `.env`-bestand lokaal (dat je in `.gitignore` houdt) of een secret manager.
|
||||
- JWT secrets, database wachtwoorden en andere credentials moeten via environment variables of Portainer secrets geleverd worden.
|
||||
- Never commit secrets into git. Use a local `.env` file (kept in `.gitignore`) or a secret manager.
|
||||
- JWT secrets, database passwords, and other credentials must be supplied via environment variables or Portainer secrets.
|
||||
|
||||
---
|
||||
|
||||
## Environment variabelen (voorbeeld)
|
||||
## Environment Variables (Example)
|
||||
|
||||
Je `docker-compose.yml` verwacht environment variabelen die de database-verbinding en JWT secret definiëren.
|
||||
Maak een local `.env` bestand in de projectroot (NIET commiten). Gebruik deze variabelen als leidraad:
|
||||
Your `docker-compose.yml` expects environment variables defining the database connection and JWT secret.
|
||||
Create a local `.env` file in the project root (DO NOT commit). Use these variables as a guide:
|
||||
|
||||
```
|
||||
# Database (externe Postgres)
|
||||
```env
|
||||
# Database (external Postgres)
|
||||
POSTGRES_HOST=your.db.host.or.ip
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_DB=transit_db
|
||||
|
||||
# Per-service DB-credentials (optioneel: kun ook één DB-user gebruiken per DB)
|
||||
# Per-service DB credentials (optional: you can also use a single DB user per DB)
|
||||
POSTGRES_USER_AUTH=auth_user
|
||||
POSTGRES_PASSWORD_AUTH=strong_password_auth
|
||||
POSTGRES_USER_EMPLOYEE=employee_user
|
||||
@@ -81,29 +103,29 @@ POSTGRES_PASSWORD_TRANSIT=strong_password_transit
|
||||
POSTGRES_USER_ERROR_LOG=error_logger
|
||||
POSTGRES_PASSWORD_ERROR_LOG=strong_password_error_log
|
||||
|
||||
# JWT secret voor services die JWT gebruiken
|
||||
# JWT secret for services using JWT
|
||||
JWT_SECRET=super_secret_jwt_key_here
|
||||
|
||||
# Optioneel: centraal error log endpoint voor clients/services
|
||||
ERROR_LOG_API_URL=http://error_log_api:5003/error_log
|
||||
# Optional: central error log endpoint for clients/services
|
||||
ERROR_LOG_API_URL=https://error.xtransit.testinstance.nl/error_log
|
||||
```
|
||||
|
||||
Opmerking: de exacte variabelen worden gelezen door `docker-compose.yml`. Pas de namen/waarden aan naar jouw omgeving.
|
||||
Note: The exact variables are read by `docker-compose.yml`. Adjust names and values according to your environment.
|
||||
|
||||
---
|
||||
|
||||
## Lokale ontwikkeling (zonder Docker)
|
||||
## Local Development (Without Docker)
|
||||
|
||||
1. Zorg dat je een PostgreSQL instance hebt draaien (lokal of remote).
|
||||
2. Kopieer per API een `.env` of export de benodigde vars.
|
||||
1. Ensure you have a running PostgreSQL instance (local or remote).
|
||||
2. Copy a `.env` file for each API or export the required environment variables.
|
||||
|
||||
Voorbeeld: run de `auth_api` lokaal
|
||||
Example: running `auth_api` locally:
|
||||
|
||||
```bash
|
||||
cd auth_api
|
||||
# installeer dependencies
|
||||
# Install dependencies
|
||||
npm install
|
||||
# export env vars (voorbeeld):
|
||||
# Export env vars (example):
|
||||
export DB_HOST=your.db.host
|
||||
export DB_PORT=5432
|
||||
export DB_NAME=transit_db
|
||||
@@ -114,93 +136,97 @@ export JWT_SECRET=...
|
||||
npm start
|
||||
```
|
||||
|
||||
Herhaal voor `employee_api` en `transit_api` met de juiste env keys/poorten.
|
||||
Repeat for `employee_api`, `transit_api`, and `error_log_api` with the appropriate env keys and ports.
|
||||
|
||||
Tip: tijdens development kun je één Postgres instance gebruiken en schema/rollen configureren.
|
||||
Tip: During development, you can use a single Postgres instance and configure schemas and roles.
|
||||
|
||||
---
|
||||
|
||||
## Docker / Portainer — deployment (met externe Postgres)
|
||||
## Docker / Portainer — Deployment (With External Postgres)
|
||||
|
||||
Deze repository bevat `docker-compose.yml` die images vanuit GHCR trekt. De compose is aangepast om een externe Postgres te gebruiken.
|
||||
This repository includes a `docker-compose.yml` file that pulls images from GHCR. The compose setup is configured to use an external Postgres instance.
|
||||
|
||||
Voordat je start:
|
||||
- Zorg dat je `.env` met database credentials en `JWT_SECRET` klaar staat op de server.
|
||||
- Zorg dat de host waar je compose draait netwerk-connectiviteit heeft naar de Postgres host.
|
||||
Before starting:
|
||||
|
||||
Stappen om te deployen (server/Portainer host):
|
||||
- Ensure your `.env` file with database credentials and `JWT_SECRET` is prepared on the server.
|
||||
- Ensure the host running compose has network connectivity to the Postgres host.
|
||||
|
||||
Deployment steps (server / Portainer host):
|
||||
|
||||
```bash
|
||||
# 1. Zet .env op server (NIET in git)
|
||||
# 1. Place .env on the server (NOT in git)
|
||||
scp .env user@server:/path/to/project/.env
|
||||
# of maak lokaal op de server
|
||||
# or create it directly on the server
|
||||
|
||||
# 2. Login bij GHCR (om private images te kunnen pullen)
|
||||
# Username = je GitHub-gebruikersnaam
|
||||
# Password = PAT met 'write:packages'/'read:packages' scopes of je GITHUB_TOKEN
|
||||
# 2. Login to GHCR (to pull private images)
|
||||
# Username = your GitHub username
|
||||
# Password = PAT with 'write:packages'/'read:packages' scopes or your GITHUB_TOKEN
|
||||
docker login ghcr.io
|
||||
|
||||
# 3. Start stack
|
||||
docker-compose --env-file .env up -d
|
||||
# of met compose V2:
|
||||
# or with compose V2:
|
||||
docker compose --env-file .env up -d
|
||||
```
|
||||
|
||||
Als je Portainer gebruikt kun je `docker-compose.yml` ook als "Stack" in de Portainer UI plakken en deployment via de UI uitvoeren. Voeg de environment variabelen in Portainer toe of verwijs naar een server-side `.env`.
|
||||
If using Portainer, you can also paste `docker-compose.yml` as a "Stack" in the Portainer UI and deploy via the UI. Add environment variables in Portainer or reference a server-side `.env`.
|
||||
|
||||
Opmerking over `depends_on` in `docker-compose.yml`: omdat je een externe DB gebruikt, verwijst de compose soms nog naar een non-existent `db` service. Je kunt die `depends_on` regels veilig verwijderen of negeren, zolang je database beschikbaar is voordat de services de verbinding proberen te maken.
|
||||
Note on `depends_on` in `docker-compose.yml`: because you are using an external DB, compose configs sometimes reference a non-existent `db` service. You can safely remove or ignore those `depends_on` rules, as long as your database is available before the services attempt to connect.
|
||||
|
||||
---
|
||||
|
||||
## GitHub Actions & Container Registry (CI)
|
||||
|
||||
Het project bevat een workflow in `.github/workflows/docker-build.yml` die bij push naar `main`/`develop` de volgende stappen uitvoert:
|
||||
The project includes a CI workflow in `.github/workflows/docker-build.yml` (and `.gitea/workflows/docker-build.yml`) that executes the following steps on push to `main`/`dev`:
|
||||
|
||||
- Checkout
|
||||
- Build Docker images per API (met BuildKit/Buildx)
|
||||
- Login naar de Container Registry (GHCR) en push images
|
||||
- Genereert tags (`latest`, branch/sha, semver als aanwezig)
|
||||
- Checkout repository
|
||||
- Build Docker images for each API (using BuildKit / Buildx)
|
||||
- Login to the Container Registry (GHCR) and push images
|
||||
- Generate tags (`latest`, branch/sha, semver if present)
|
||||
|
||||
Om GHCR te gebruiken op je server:
|
||||
1. Maak een Personal Access Token (PAT) met `read:packages` (en `write:packages` indien je zelf ook wilt pushen).
|
||||
2. Login op de server: `docker login ghcr.io` (gebruik PAT als wachtwoord).
|
||||
To use GHCR on your server:
|
||||
|
||||
1. Create a Personal Access Token (PAT) with `read:packages` (and `write:packages` if you also wish to push).
|
||||
2. Login on the server: `docker login ghcr.io` (use PAT as password).
|
||||
|
||||
Example image name:
|
||||
|
||||
Image naam voorbeeld:
|
||||
```
|
||||
ghcr.io/<github-username>/<repo>/auth_api:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Health checks, endpoints en debugging
|
||||
## Health Checks, Endpoints, and Debugging
|
||||
|
||||
Enkele nuttige endpoints (voorbeeld):
|
||||
Useful endpoints (example):
|
||||
|
||||
- `GET /health` — (beschikbaar in `employee_api`) controleert reachability
|
||||
- `POST /login` — (`auth_api`) authenticatie
|
||||
- `GET /schedule/daily?date=YYYY-MM-DD` — (`transit_api`) vereist auth
|
||||
- `GET /health` — (available in `employee_api`, `transit_api`, `error_log_api`) checks reachability
|
||||
- `POST /login` — (`auth_api`) authentication
|
||||
- `GET /schedule/daily?date=YYYY-MM-DD` — (`transit_api`) requires authentication
|
||||
|
||||
Logs bekijken:
|
||||
Viewing logs:
|
||||
|
||||
```bash
|
||||
# Logs van service
|
||||
# Logs from a service
|
||||
docker-compose logs -f auth_api
|
||||
|
||||
# Of via container id
|
||||
# Or via container ID
|
||||
docker logs -f auth_api
|
||||
```
|
||||
|
||||
Problemen met DB-verbinding:
|
||||
- Controleer netwerkconnectiviteit naar `POSTGRES_HOST`
|
||||
- Controleer users/permissions in Postgres
|
||||
- Controleer dat de juiste schema (zoals `workers`) bestaat als de queries die verwachten
|
||||
Troubleshooting DB connection:
|
||||
|
||||
- Verify network connectivity to `POSTGRES_HOST`
|
||||
- Check users and permissions in Postgres
|
||||
- Ensure the expected schema (such as `workers` or `transit`) exists if queries expect it
|
||||
|
||||
---
|
||||
|
||||
## Veelvoorkomende taken & commando's
|
||||
## Common Tasks & Commands
|
||||
|
||||
```bash
|
||||
# Build & run (pull images van GHCR)
|
||||
# Build & run (pull images from GHCR)
|
||||
docker-compose --env-file .env up -d
|
||||
|
||||
# Pull latest images
|
||||
@@ -218,29 +244,30 @@ docker login ghcr.io
|
||||
|
||||
---
|
||||
|
||||
## Migraties & Database schema
|
||||
## Migrations & Database Schema
|
||||
|
||||
Database schema en migraties zitten niet in deze repository. Zorg dat je:
|
||||
- de juiste schema's (`workers`, `routes`, etc.) aanmaakt
|
||||
- gebruikers en rechten configureert voor de verschillende services
|
||||
Database schemas and migrations are maintained separately. Ensure you:
|
||||
|
||||
### Error logging schema
|
||||
- Create the necessary schemas (`workers`, `transit`, `data`, etc.)
|
||||
- Configure users and privileges for the respective services
|
||||
|
||||
Voor de centrale foutlogging maak je schema `data` en tabel `data.errors` aan:
|
||||
### Error Logging Schema
|
||||
|
||||
For centralized error logging, create schema `data` and table `data.errors`:
|
||||
|
||||
```sql
|
||||
CREATE SCHEMA IF NOT EXISTS data;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS data.errors (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
"date" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"service" TEXT NOT NULL,
|
||||
"partOfService" TEXT,
|
||||
"error" TEXT NOT NULL
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
"date" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
"service" TEXT NOT NULL,
|
||||
partofservice TEXT,
|
||||
error TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Gebruik een aparte gebruiker met minimale rechten, bijvoorbeeld `error_logger`:
|
||||
Use a dedicated user with minimal privileges, e.g. `error_logger`:
|
||||
|
||||
```sql
|
||||
CREATE ROLE error_logger LOGIN PASSWORD 'strong_password_error_log';
|
||||
@@ -248,38 +275,39 @@ GRANT USAGE ON SCHEMA data TO error_logger;
|
||||
GRANT INSERT, SELECT ON data.errors TO error_logger;
|
||||
```
|
||||
|
||||
Als je nog geen database hebt, kun je lokaal een Postgres container gebruiken (development):
|
||||
If you do not have a database yet, you can run a local PostgreSQL container for development:
|
||||
|
||||
```bash
|
||||
docker run --name transit-db -e POSTGRES_DB=transit_db -e POSTGRES_USER=transit_user -e POSTGRES_PASSWORD=pass -p 5432:5432 -d postgres:15-alpine
|
||||
```
|
||||
|
||||
Maar voor je production/Portainer setup volg je je bestaande beheersbeleid en gebruik je een managed DB of beveiligde host.
|
||||
For production / Portainer setups, follow your infrastructure policy and use a managed database or secured host.
|
||||
|
||||
---
|
||||
|
||||
## Deployment tips
|
||||
## Deployment Tips
|
||||
|
||||
- Gebruik verschillende database users per service met minimale rechten
|
||||
- Gebruik secrets in Portainer of Docker swarm/kubernetes secrets in productie
|
||||
- Gebruik een reverse proxy (nginx, Traefik) voor TLS/HTTPS
|
||||
- Stel log aggregatie en monitoring in (Prometheus/Grafana, ELK, etc.)
|
||||
- Use distinct database users per service with minimal privileges
|
||||
- Use secrets in Portainer, Docker swarm, or Kubernetes in production
|
||||
- Use a reverse proxy (Nginx, Traefik) for TLS/HTTPS
|
||||
- Set up log aggregation and monitoring (Prometheus/Grafana, ELK, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Contributie
|
||||
## Contributing
|
||||
|
||||
- Maak feature branches en open pull requests
|
||||
- Push naar `develop` of `main` afhankelijk van je branching policy
|
||||
- CI zal automatisch images bouwen en pushen voor `main`/`develop`
|
||||
- Create feature branches and open pull requests
|
||||
- Push to `dev` or `main` depending on your branching policy
|
||||
- CI will automatically build and push images for `main`/`dev`
|
||||
|
||||
---
|
||||
|
||||
## Waar te vinden
|
||||
## Where to Find Everything
|
||||
|
||||
- API code: `auth_api/`, `employee_api/`, `transit_api/`
|
||||
- CI workflow: `.github/workflows/docker-build.yml`
|
||||
- Extra docs: `DOCKER_README.md`, `CI_CD_PORTAINER_GUIDE.md`
|
||||
- API code: `auth_api/`, `employee_api/`, `transit_api/`, `error_log_api/`
|
||||
- Frontend UI: `admin_ui/`, `monitoring_ui/`
|
||||
- Documentation hub: `monitoring_ui/docs/`
|
||||
- CI workflows: `.github/workflows/docker-build.yml`, `.gitea/workflows/docker-build.yml`
|
||||
|
||||
---
|
||||
|
||||
@@ -314,7 +342,6 @@ admin_ui/
|
||||
│ └── styles.css # Responsive UI styling
|
||||
├── docker-entrypoint.sh # Runtime env config
|
||||
└── Dockerfile # Multi-stage build
|
||||
|
||||
```
|
||||
|
||||
### Building the Admin UI
|
||||
@@ -323,7 +350,7 @@ admin_ui/
|
||||
cd admin_ui
|
||||
npm install
|
||||
npm run build # Production build
|
||||
npm run dev # Development (Vite)
|
||||
npm run dev # Development (Vite)
|
||||
```
|
||||
|
||||
### Docker Deployment
|
||||
@@ -340,25 +367,28 @@ docker-compose up admin_ui
|
||||
The Transit API exposes public (unauthenticated) schedule endpoints:
|
||||
|
||||
**Endpoints:**
|
||||
|
||||
- `GET /schedules?date=YYYY-MM-DD&lineId=ID` — Get schedules for a date/line
|
||||
- `GET /schedules/:scheduleId` — Get schedule details
|
||||
- `GET /schedules/:scheduleId/stops` — Get stops on a schedule's line
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
curl http://localhost:5001/schedules?date=2026-05-06
|
||||
curl https://transit.xtransit.testinstance.nl/schedules?date=2026-05-06
|
||||
```
|
||||
|
||||
**TimetableViewer Component:**
|
||||
```jsx
|
||||
import TimetableViewer from './components/TimetableViewer'
|
||||
|
||||
<TimetableViewer apiBaseUrl="http://api.transit.local:5001" />
|
||||
```jsx
|
||||
import TimetableViewer from "./components/TimetableViewer";
|
||||
|
||||
<TimetableViewer apiBaseUrl="http://api.transit.local:5001" />;
|
||||
```
|
||||
|
||||
### Schedule Maker Documentation
|
||||
|
||||
See [admin_ui/SCHEDULE_API.md](admin_ui/SCHEDULE_API.md) for detailed API documentation, usage examples, and UI features.
|
||||
See [monitoring_ui/docs/admin_ui/SCHEDULE_API.md](monitoring_ui/docs/admin_ui/SCHEDULE_API.md) for detailed API documentation, usage examples, and UI features.
|
||||
|
||||
---
|
||||
|
||||
@@ -393,7 +423,3 @@ ADMIN_USER_CODES=42 DEV_MOCK=1 JWT_SECRET=devsecret npm --prefix ./auth_api star
|
||||
### Production Mode
|
||||
|
||||
Set up real PostgreSQL database and provide credentials via environment variables.
|
||||
|
||||
---
|
||||
|
||||
Als je wilt, kan ik deze README nog aanpassen met specifieke voorbeelden van database schema's of exacte environment-variabele namen voor lokaal draaien. Zeg welke details je wil toevoegen (bijv. example SQL voor `workers` schema).
|
||||
@@ -7,6 +7,7 @@ RUN npm run build
|
||||
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY docker-entrypoint.sh /docker-entrypoint.sh
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
EXPOSE 80
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
version: '3.8'
|
||||
version: "3.8"
|
||||
services:
|
||||
admin-ui:
|
||||
build: .
|
||||
ports:
|
||||
- "5174:80"
|
||||
environment:
|
||||
VITE_AUTH_API: ${VITE_AUTH_API:-http://localhost:5000}
|
||||
VITE_TRANSIT_API: ${VITE_TRANSIT_API:-http://localhost:5001}
|
||||
VITE_AUTH_API: ${VITE_AUTH_API:-https://auth.xtransit.testinstance.nl}
|
||||
VITE_TRANSIT_API: ${VITE_TRANSIT_API:-https://transit.xtransit.testinstance.nl}
|
||||
|
||||
@@ -3,9 +3,9 @@ set -e
|
||||
|
||||
cat > /usr/share/nginx/html/env-config.js <<EOF
|
||||
window.__ENV__ = {
|
||||
AUTH_API: "${AUTH_API:-http://localhost:5000}",
|
||||
TRANSIT_API: "${TRANSIT_API:-http://localhost:5001}",
|
||||
ERROR_LOG_API: "${ERROR_LOG_API:-http://localhost:5003}"
|
||||
AUTH_API: "${AUTH_API:-https://auth.xtransit.testinstance.nl}",
|
||||
TRANSIT_API: "${TRANSIT_API:-https://transit.xtransit.testinstance.nl}",
|
||||
ERROR_LOG_API: "${ERROR_LOG_API:-https://error.xtransit.testinstance.nl}"
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
server {
|
||||
listen 80;
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = /health {
|
||||
default_type application/json;
|
||||
add_header Cache-Control "no-store";
|
||||
return 200 '{"status":"ok","service":"admin_ui"}';
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,106 @@
|
||||
import axios from 'axios'
|
||||
import axios from "axios";
|
||||
|
||||
const RUNTIME = typeof window !== 'undefined' && window.__ENV__ ? window.__ENV__ : {}
|
||||
const RUNTIME =
|
||||
typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {};
|
||||
|
||||
const normalizeApiBaseUrl = (value, fallbackPath) => {
|
||||
const rawValue = value || ''
|
||||
if (!rawValue) return fallbackPath
|
||||
if (/^https?:\/\//i.test(rawValue) || rawValue.startsWith('//') || rawValue.startsWith('/')) {
|
||||
return rawValue
|
||||
const rawValue = value || "";
|
||||
if (!rawValue) return fallbackPath;
|
||||
if (
|
||||
/^https?:\/\//i.test(rawValue) ||
|
||||
rawValue.startsWith("//") ||
|
||||
rawValue.startsWith("/")
|
||||
) {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
return `${window.location.protocol}//${rawValue}`
|
||||
}
|
||||
return `${window.location.protocol}//${rawValue}`;
|
||||
};
|
||||
|
||||
const AUTH_API = normalizeApiBaseUrl(RUNTIME.AUTH_API || import.meta.env.VITE_AUTH_API, 'http://localhost:5000')
|
||||
const TRANSIT_API = normalizeApiBaseUrl(RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API, 'http://localhost:5001')
|
||||
const ERROR_LOG_API = normalizeApiBaseUrl(RUNTIME.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API, 'http://localhost:5003')
|
||||
const AUTH_API = normalizeApiBaseUrl(
|
||||
RUNTIME.AUTH_API || import.meta.env.VITE_AUTH_API,
|
||||
"https://auth.xtransit.testinstance.nl",
|
||||
);
|
||||
const TRANSIT_API = normalizeApiBaseUrl(
|
||||
RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API,
|
||||
"https://transit.xtransit.testinstance.nl",
|
||||
);
|
||||
const ERROR_LOG_API = normalizeApiBaseUrl(
|
||||
RUNTIME.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API,
|
||||
"https://error.xtransit.testinstance.nl",
|
||||
);
|
||||
|
||||
const reportClientError = async ({ service, partOfService, error }) => {
|
||||
const reportClientError = async ({ service, partofservice, error }) => {
|
||||
try {
|
||||
await fetch(`${ERROR_LOG_API}/error_log`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
service,
|
||||
partOfService,
|
||||
error: typeof error === 'string' ? error : JSON.stringify(error),
|
||||
partofservice,
|
||||
error: typeof error === "string" ? error : JSON.stringify(error),
|
||||
}),
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore logging failures
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const authClient = axios.create({ baseURL: AUTH_API })
|
||||
export const authClient = axios.create({ baseURL: AUTH_API });
|
||||
|
||||
authClient.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
(response) => response,
|
||||
(error) => {
|
||||
void reportClientError({
|
||||
service: 'admin_ui',
|
||||
partOfService: 'authClient',
|
||||
error: error?.response?.data?.error || error?.message || 'authClient_error',
|
||||
})
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
service: "admin_ui",
|
||||
partofservice: "authClient",
|
||||
error:
|
||||
error?.response?.data?.error || error?.message || "authClient_error",
|
||||
});
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export const transitClient = (token) => {
|
||||
const client = axios.create({
|
||||
baseURL: TRANSIT_API,
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
})
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
// Auto-logout on unauthorized responses to avoid delayed failures
|
||||
client.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
(response) => response,
|
||||
(error) => {
|
||||
try {
|
||||
const status = error?.response?.status
|
||||
const status = error?.response?.status;
|
||||
if (status === 401 || status === 403) {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem("token");
|
||||
localStorage.removeItem("user");
|
||||
// reload to let App show the Login screen
|
||||
if (typeof window !== 'undefined') window.location.reload()
|
||||
if (typeof window !== "undefined") window.location.reload();
|
||||
}
|
||||
void reportClientError({
|
||||
service: 'admin_ui',
|
||||
partOfService: 'transitClient',
|
||||
error: error?.response?.data?.error || error?.message || 'transitClient_error',
|
||||
})
|
||||
service: "admin_ui",
|
||||
partofservice: "transitClient",
|
||||
error:
|
||||
error?.response?.data?.error ||
|
||||
error?.message ||
|
||||
"transitClient_error",
|
||||
});
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
return client
|
||||
}
|
||||
return client;
|
||||
};
|
||||
|
||||
export const errorLogClient = (token) => axios.create({
|
||||
baseURL: ERROR_LOG_API,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
export const errorLogClient = (token) =>
|
||||
axios.create({
|
||||
baseURL: ERROR_LOG_API,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
export { reportClientError }
|
||||
export { reportClientError };
|
||||
|
||||
@@ -1,42 +1,44 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { errorLogClient } from '../api'
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { errorLogClient } from "../api";
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) return '—'
|
||||
if (!value) return "—";
|
||||
try {
|
||||
return new Date(value).toLocaleString()
|
||||
return new Date(value).toLocaleString();
|
||||
} catch (_) {
|
||||
return String(value)
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function ErrorLogViewer({ token }) {
|
||||
const client = useMemo(() => errorLogClient(token), [token])
|
||||
const [logs, setLogs] = useState([])
|
||||
const [service, setService] = useState('')
|
||||
const [partOfService, setPartOfService] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState(null)
|
||||
const client = useMemo(() => errorLogClient(token), [token]);
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [service, setService] = useState("");
|
||||
const [partofservice, setPartofservice] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
const loadLogs = async () => {
|
||||
setLoading(true)
|
||||
setMessage(null)
|
||||
setLoading(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
const params = { limit: 100 }
|
||||
if (service) params.service = service
|
||||
if (partOfService) params.partOfService = partOfService
|
||||
const res = await client.get('/error_log', { params })
|
||||
setLogs(res.data.errors || [])
|
||||
const params = { limit: 100 };
|
||||
if (service) params.service = service;
|
||||
if (partofservice) params.partofservice = partofservice;
|
||||
const res = await client.get("/error_log", { params });
|
||||
setLogs(res.data.errors || []);
|
||||
} catch (err) {
|
||||
setMessage(err.response?.data?.error || err.message || 'Kon fouten niet laden')
|
||||
setMessage(
|
||||
err.response?.data?.error || err.message || "Kon fouten niet laden",
|
||||
);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs()
|
||||
}, [service, partOfService])
|
||||
loadLogs();
|
||||
}, [service, partofservice]);
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
@@ -46,29 +48,41 @@ export default function ErrorLogViewer({ token }) {
|
||||
<p>Laatste foutmeldingen uit de centrale logging service.</p>
|
||||
</div>
|
||||
<button className="secondary" onClick={loadLogs} disabled={loading}>
|
||||
{loading ? 'Laden...' : 'Ververs'}
|
||||
{loading ? "Laden..." : "Ververs"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<input placeholder="Service filter" value={service} onChange={e => setService(e.target.value)} />
|
||||
<input placeholder="Part of service filter" value={partOfService} onChange={e => setPartOfService(e.target.value)} />
|
||||
<input
|
||||
placeholder="Service filter"
|
||||
value={service}
|
||||
onChange={(e) => setService(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
placeholder="Part of service filter"
|
||||
value={partofservice}
|
||||
onChange={(e) => setPartofservice(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{message && <div className="message">{message}</div>}
|
||||
|
||||
<div className="list-grid">
|
||||
{logs.map(log => (
|
||||
{logs.map((log) => (
|
||||
<article className="mini-card" key={log.id}>
|
||||
<strong>{log.service}</strong>
|
||||
<div>{formatDate(log.date)}</div>
|
||||
<div className="muted" style={{ marginTop: 6 }}>{log.partOfService || '—'}</div>
|
||||
<div className="muted" style={{ marginTop: 6 }}>
|
||||
{log.partofservice || "—"}
|
||||
</div>
|
||||
<div style={{ marginTop: 8 }}>{log.error}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!logs.length && !message && <div className="muted">Nog geen foutmeldingen gevonden.</div>}
|
||||
{!logs.length && !message && (
|
||||
<div className="muted">Nog geen foutmeldingen gevonden.</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,93 +1,61 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { transitClient } from '../api'
|
||||
import TimetableDisplay from './TimetableDisplay'
|
||||
import FixedScheduleEditor from './FixedScheduleEditor'
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { transitClient } from "../api";
|
||||
import WeeklyScheduleEditor from "./WeeklyScheduleEditor";
|
||||
|
||||
export default function ScheduleMaker({ token, user }) {
|
||||
const client = transitClient(token)
|
||||
const [scheduleTemplates, setScheduleTemplates] = useState([])
|
||||
const [busLines, setBusLines] = useState([])
|
||||
const [buses, setBuses] = useState([])
|
||||
const [drivers, setDrivers] = useState([])
|
||||
const [selectedDriverFilter, setSelectedDriverFilter] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [isAdmin] = useState(Boolean(user?.isAdmin))
|
||||
const [overviewDate, setOverviewDate] = useState(new Date().toISOString().split('T')[0])
|
||||
export default function ScheduleMaker({ token }) {
|
||||
const client = transitClient(token);
|
||||
const [schedules, setSchedules] = useState([]);
|
||||
const [busLines, setBusLines] = useState([]);
|
||||
const [drivers, setDrivers] = useState([]);
|
||||
const [message, setMessage] = useState(null);
|
||||
|
||||
const loadScheduleTemplates = async () => {
|
||||
const loadSchedules = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/schedules?templates=1')
|
||||
setScheduleTemplates((res.data.schedules || []).filter(schedule => Array.isArray(schedule.weekdays) && schedule.weekdays.length > 0))
|
||||
const res = await client.get("/admin/weekly-schedules");
|
||||
setSchedules(res.data.schedules || []);
|
||||
} catch (err) {
|
||||
setMessage('Error loading fixed schedules: ' + (err.response?.data?.error || err.message))
|
||||
setMessage("Could not load weekly schedules.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadBusLines = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/bus-lines')
|
||||
setBusLines(res.data.busLines || [])
|
||||
const res = await client.get("/admin/bus-lines");
|
||||
setBusLines(res.data.busLines || []);
|
||||
} catch (err) {
|
||||
setMessage('Error loading bus lines: ' + (err.response?.data?.error || err.message))
|
||||
setMessage("Could not load bus lines.");
|
||||
}
|
||||
}
|
||||
|
||||
const loadBuses = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/fleet/buses')
|
||||
setBuses(res.data.buses || [])
|
||||
} catch (err) {
|
||||
setMessage('Error loading buses: ' + (err.response?.data?.error || err.message))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loadDrivers = async () => {
|
||||
try {
|
||||
const res = await client.get('/admin/drivers')
|
||||
setDrivers(res.data.drivers || [])
|
||||
const res = await client.get("/admin/shift-drivers");
|
||||
setDrivers(res.data.drivers || []);
|
||||
} catch (err) {
|
||||
setMessage('Error loading drivers: ' + (err.response?.data?.error || err.message))
|
||||
setMessage("Could not load drivers.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadScheduleTemplates()
|
||||
loadBusLines()
|
||||
loadBuses()
|
||||
loadDrivers()
|
||||
}, [])
|
||||
loadSchedules();
|
||||
loadBusLines();
|
||||
loadDrivers();
|
||||
}, []);
|
||||
|
||||
const handleScheduleChange = async () => {
|
||||
await loadScheduleTemplates()
|
||||
}
|
||||
await loadSchedules();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Timetable widget - compact daily view */}
|
||||
<TimetableDisplay
|
||||
<WeeklyScheduleEditor
|
||||
client={client}
|
||||
overviewDate={overviewDate}
|
||||
setOverviewDate={setOverviewDate}
|
||||
selectedDriverFilter={selectedDriverFilter}
|
||||
setSelectedDriverFilter={setSelectedDriverFilter}
|
||||
busLines={busLines}
|
||||
drivers={drivers}
|
||||
schedules={schedules}
|
||||
onChange={handleScheduleChange}
|
||||
/>
|
||||
|
||||
{/* Fixed service editor - separate widget */}
|
||||
{isAdmin && (
|
||||
<FixedScheduleEditor
|
||||
client={client}
|
||||
token={token}
|
||||
user={user}
|
||||
scheduleTemplates={scheduleTemplates}
|
||||
busLines={busLines}
|
||||
buses={buses}
|
||||
drivers={drivers}
|
||||
message={message}
|
||||
setMessage={setMessage}
|
||||
onScheduleChange={handleScheduleChange}
|
||||
/>
|
||||
)}
|
||||
{message && <div className="message">{message}</div>}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,72 +1,85 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }) {
|
||||
const [schedules, setSchedules] = useState([])
|
||||
const [summary, setSummary] = useState(null)
|
||||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0])
|
||||
const [selectedLineId, setSelectedLineId] = useState('')
|
||||
const [lines, setLines] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
export default function TimetableViewer({
|
||||
apiBaseUrl = "https://transit.xtransit.testinstance.nl",
|
||||
}) {
|
||||
const [schedules, setSchedules] = useState([]);
|
||||
const [summary, setSummary] = useState(null);
|
||||
const [selectedDate, setSelectedDate] = useState(
|
||||
new Date().toISOString().split("T")[0],
|
||||
);
|
||||
const [selectedLineId, setSelectedLineId] = useState("");
|
||||
const [lines, setLines] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
// Fetch available bus lines
|
||||
useEffect(() => {
|
||||
const fetchLines = async () => {
|
||||
try {
|
||||
const res = await fetch(`${apiBaseUrl}/admin/bus-lines`)
|
||||
const data = await res.json()
|
||||
setLines(data.busLines || [])
|
||||
const res = await fetch(`${apiBaseUrl}/admin/bus-lines`);
|
||||
const data = await res.json();
|
||||
setLines(data.busLines || []);
|
||||
} catch (err) {
|
||||
console.error('Failed to load lines:', err)
|
||||
console.error("Failed to load lines:", err);
|
||||
}
|
||||
}
|
||||
fetchLines()
|
||||
}, [apiBaseUrl])
|
||||
};
|
||||
fetchLines();
|
||||
}, [apiBaseUrl]);
|
||||
|
||||
// Fetch schedules based on date and line filter
|
||||
useEffect(() => {
|
||||
const fetchSchedules = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
let url = `${apiBaseUrl}/schedules/overview?date=${selectedDate}`
|
||||
let url = `${apiBaseUrl}/schedules/overview?date=${selectedDate}`;
|
||||
if (selectedLineId) {
|
||||
url += `&lineId=${selectedLineId}`
|
||||
url += `&lineId=${selectedLineId}`;
|
||||
}
|
||||
const res = await fetch(url)
|
||||
const data = await res.json()
|
||||
setSchedules(data.schedules || [])
|
||||
setSummary(data.summary || null)
|
||||
const res = await fetch(url);
|
||||
const data = await res.json();
|
||||
setSchedules(data.schedules || []);
|
||||
setSummary(data.summary || null);
|
||||
} catch (err) {
|
||||
setError('Failed to load schedules: ' + err.message)
|
||||
setError("Failed to load schedules: " + err.message);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
fetchSchedules()
|
||||
}, [selectedDate, selectedLineId, apiBaseUrl])
|
||||
};
|
||||
fetchSchedules();
|
||||
}, [selectedDate, selectedLineId, apiBaseUrl]);
|
||||
|
||||
const timeSlots = generateTimeSlots()
|
||||
const schedulesBySlot = groupSchedulesByTimeSlot(schedules, timeSlots)
|
||||
const timeSlots = generateTimeSlots();
|
||||
const schedulesBySlot = groupSchedulesByTimeSlot(schedules, timeSlots);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
|
||||
<div style={{ padding: "20px", maxWidth: "1200px", margin: "0 auto" }}>
|
||||
<h1>Public Transit Timetable</h1>
|
||||
|
||||
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
|
||||
|
||||
<div style={{ marginBottom: "20px", display: "flex", gap: "10px" }}>
|
||||
<input
|
||||
type="date"
|
||||
value={selectedDate}
|
||||
onChange={e => setSelectedDate(e.target.value)}
|
||||
style={{ padding: '8px', borderRadius: '6px', border: '1px solid #ccc' }}
|
||||
onChange={(e) => setSelectedDate(e.target.value)}
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid #ccc",
|
||||
}}
|
||||
/>
|
||||
<select
|
||||
value={selectedLineId}
|
||||
onChange={e => setSelectedLineId(e.target.value)}
|
||||
style={{ padding: '8px', borderRadius: '6px', border: '1px solid #ccc', minWidth: '200px' }}
|
||||
onChange={(e) => setSelectedLineId(e.target.value)}
|
||||
style={{
|
||||
padding: "8px",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid #ccc",
|
||||
minWidth: "200px",
|
||||
}}
|
||||
>
|
||||
<option value="">All lines</option>
|
||||
{lines.map(line => (
|
||||
{lines.map((line) => (
|
||||
<option key={line.id} value={line.id}>
|
||||
Line {line.lineNumber} ({line.startStop} → {line.endStop})
|
||||
</option>
|
||||
@@ -75,101 +88,191 @@ export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }
|
||||
</div>
|
||||
|
||||
{summary && (
|
||||
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#eef2ff', color: '#3730a3' }}>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: "16px",
|
||||
display: "flex",
|
||||
gap: "12px",
|
||||
flexWrap: "wrap",
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: "999px",
|
||||
backgroundColor: "#eef2ff",
|
||||
color: "#3730a3",
|
||||
}}
|
||||
>
|
||||
Total: {summary.total}
|
||||
</span>
|
||||
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#ecfeff', color: '#155e75' }}>
|
||||
<span
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: "999px",
|
||||
backgroundColor: "#ecfeff",
|
||||
color: "#155e75",
|
||||
}}
|
||||
>
|
||||
Vast: {summary.recurring}
|
||||
</span>
|
||||
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#f8fafc', color: '#334155' }}>
|
||||
<span
|
||||
style={{
|
||||
padding: "6px 10px",
|
||||
borderRadius: "999px",
|
||||
backgroundColor: "#f8fafc",
|
||||
color: "#334155",
|
||||
}}
|
||||
>
|
||||
Eenmalig: {summary.oneTime}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && <p>Loading schedules...</p>}
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
{error && <p style={{ color: "red" }}>{error}</p>}
|
||||
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '20px' }}>
|
||||
<thead style={{ backgroundColor: '#f1f5f9' }}>
|
||||
<table
|
||||
style={{ width: "100%", borderCollapse: "collapse", marginTop: "20px" }}
|
||||
>
|
||||
<thead style={{ backgroundColor: "#f1f5f9" }}>
|
||||
<tr>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Time</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Line</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Vehicle</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Day status</th>
|
||||
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Delay</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px",
|
||||
textAlign: "left",
|
||||
borderBottom: "2px solid #ddd",
|
||||
}}
|
||||
>
|
||||
Time
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px",
|
||||
textAlign: "left",
|
||||
borderBottom: "2px solid #ddd",
|
||||
}}
|
||||
>
|
||||
Line
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px",
|
||||
textAlign: "left",
|
||||
borderBottom: "2px solid #ddd",
|
||||
}}
|
||||
>
|
||||
Vehicle
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px",
|
||||
textAlign: "left",
|
||||
borderBottom: "2px solid #ddd",
|
||||
}}
|
||||
>
|
||||
Day status
|
||||
</th>
|
||||
<th
|
||||
style={{
|
||||
padding: "12px",
|
||||
textAlign: "left",
|
||||
borderBottom: "2px solid #ddd",
|
||||
}}
|
||||
>
|
||||
Delay
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{timeSlots.map((slot, idx) => {
|
||||
const slotSchedules = schedulesBySlot[slot] || []
|
||||
if (slotSchedules.length === 0) return null
|
||||
const slotSchedules = schedulesBySlot[slot] || [];
|
||||
if (slotSchedules.length === 0) return null;
|
||||
return slotSchedules.map((schedule, sidx) => (
|
||||
<tr key={`${idx}-${sidx}`} style={{ borderBottom: '1px solid #eee' }}>
|
||||
<td style={{ padding: '12px' }}>
|
||||
{sidx === 0 ? slot : ''}
|
||||
</td>
|
||||
<td style={{ padding: '12px', fontWeight: 'bold', color: '#4f46e5' }}>
|
||||
<tr
|
||||
key={`${idx}-${sidx}`}
|
||||
style={{ borderBottom: "1px solid #eee" }}
|
||||
>
|
||||
<td style={{ padding: "12px" }}>{sidx === 0 ? slot : ""}</td>
|
||||
<td
|
||||
style={{
|
||||
padding: "12px",
|
||||
fontWeight: "bold",
|
||||
color: "#4f46e5",
|
||||
}}
|
||||
>
|
||||
Line {schedule.lineNumber}
|
||||
</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<td style={{ padding: "12px" }}>
|
||||
{schedule.busName} ({schedule.licensePlate})
|
||||
</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
<td style={{ padding: "12px" }}>
|
||||
<span
|
||||
style={{
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: schedule.status === 'in_progress' ? '#dbeafe' : schedule.status === 'completed' ? '#dcfce7' : '#f3f4f6',
|
||||
color: schedule.status === 'in_progress' ? '#1d4ed8' : schedule.status === 'completed' ? '#166534' : '#374151',
|
||||
fontSize: '0.9rem',
|
||||
padding: "4px 8px",
|
||||
borderRadius: "4px",
|
||||
backgroundColor:
|
||||
schedule.status === "in_progress"
|
||||
? "#dbeafe"
|
||||
: schedule.status === "completed"
|
||||
? "#dcfce7"
|
||||
: "#f3f4f6",
|
||||
color:
|
||||
schedule.status === "in_progress"
|
||||
? "#1d4ed8"
|
||||
: schedule.status === "completed"
|
||||
? "#166534"
|
||||
: "#374151",
|
||||
fontSize: "0.9rem",
|
||||
}}
|
||||
>
|
||||
{schedule.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '12px' }}>
|
||||
{typeof schedule.currentDelayMinutes === 'number' && schedule.currentDelayMinutes !== null
|
||||
? `${schedule.currentDelayMinutes > 0 ? '+' : ''}${schedule.currentDelayMinutes} min`
|
||||
: '—'}
|
||||
<td style={{ padding: "12px" }}>
|
||||
{typeof schedule.currentDelayMinutes === "number" &&
|
||||
schedule.currentDelayMinutes !== null
|
||||
? `${schedule.currentDelayMinutes > 0 ? "+" : ""}${schedule.currentDelayMinutes} min`
|
||||
: "—"}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
));
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{schedules.length === 0 && !loading && (
|
||||
<p style={{ textAlign: 'center', color: '#999', marginTop: '40px' }}>
|
||||
<p style={{ textAlign: "center", color: "#999", marginTop: "40px" }}>
|
||||
No schedules found for the selected date and line.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function generateTimeSlots() {
|
||||
const slots = []
|
||||
const slots = [];
|
||||
for (let hour = 0; hour < 24; hour++) {
|
||||
slots.push(`${String(hour).padStart(2, '0')}:00-${String((hour + 1) % 24).padStart(2, '0')}:00`)
|
||||
slots.push(
|
||||
`${String(hour).padStart(2, "0")}:00-${String((hour + 1) % 24).padStart(2, "0")}:00`,
|
||||
);
|
||||
}
|
||||
return slots
|
||||
return slots;
|
||||
}
|
||||
|
||||
function groupSchedulesByTimeSlot(schedules, slots) {
|
||||
const grouped = {}
|
||||
slots.forEach(slot => {
|
||||
grouped[slot] = []
|
||||
})
|
||||
const grouped = {};
|
||||
slots.forEach((slot) => {
|
||||
grouped[slot] = [];
|
||||
});
|
||||
|
||||
schedules.forEach(schedule => {
|
||||
const startHour = new Date(schedule.startTime).getHours()
|
||||
const slot = `${String(startHour).padStart(2, '0')}:00-${String((startHour + 1) % 24).padStart(2, '0')}:00`
|
||||
schedules.forEach((schedule) => {
|
||||
const startHour = new Date(schedule.startTime).getHours();
|
||||
const slot = `${String(startHour).padStart(2, "0")}:00-${String((startHour + 1) % 24).padStart(2, "0")}:00`;
|
||||
if (grouped[slot]) {
|
||||
grouped[slot].push(schedule)
|
||||
grouped[slot].push(schedule);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return grouped
|
||||
return grouped;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
const DAY_GROUPS = [
|
||||
{ value: "weekdays", label: "Weekdays", days: [1, 2, 3, 4, 5] },
|
||||
{ value: "saturday", label: "Saturday", days: [6] },
|
||||
{ value: "sunday", label: "Sunday", days: [0] },
|
||||
];
|
||||
|
||||
const dayLabel = (day) =>
|
||||
[
|
||||
"Sunday",
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
][day];
|
||||
|
||||
export default function WeeklyScheduleEditor({
|
||||
client,
|
||||
busLines,
|
||||
drivers,
|
||||
schedules,
|
||||
onChange,
|
||||
}) {
|
||||
const [lineId, setLineId] = useState("");
|
||||
const [category, setCategory] = useState("weekdays");
|
||||
const [startTimes, setStartTimes] = useState(["08:00"]);
|
||||
const [newStartTime, setNewStartTime] = useState("");
|
||||
const [driverId, setDriverId] = useState("");
|
||||
const [message, setMessage] = useState(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
const groupedSchedules = useMemo(() => {
|
||||
return schedules.reduce((groups, schedule) => {
|
||||
const key =
|
||||
schedule.dayOfWeek >= 1 && schedule.dayOfWeek <= 5
|
||||
? "weekdays"
|
||||
: schedule.dayOfWeek === 6
|
||||
? "saturday"
|
||||
: "sunday";
|
||||
groups[key] = [...(groups[key] || []), schedule];
|
||||
return groups;
|
||||
}, {});
|
||||
}, [schedules]);
|
||||
|
||||
const createSchedules = async (event) => {
|
||||
event.preventDefault();
|
||||
setMessage(null);
|
||||
if (!lineId || startTimes.length === 0) {
|
||||
setMessage("Choose a line and add at least one departure time.");
|
||||
return;
|
||||
}
|
||||
|
||||
const group = DAY_GROUPS.find((item) => item.value === category);
|
||||
setSaving(true);
|
||||
try {
|
||||
const response = await client.post("/admin/weekly-schedules/bulk", {
|
||||
busLineId: Number(lineId),
|
||||
dayOfWeeks: group.days,
|
||||
startTimes,
|
||||
driverId: driverId ? Number(driverId) : null,
|
||||
});
|
||||
setMessage(
|
||||
`${response.data.createdCount} departures created for ${group.label}.`,
|
||||
);
|
||||
setStartTimes(["08:00"]);
|
||||
setNewStartTime("");
|
||||
onChange();
|
||||
} catch (error) {
|
||||
setMessage(error.response?.data?.error || "Could not save the schedule.");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const addStartTime = () => {
|
||||
if (!newStartTime || startTimes.includes(newStartTime)) return;
|
||||
setStartTimes([...startTimes, newStartTime].sort());
|
||||
setNewStartTime("");
|
||||
};
|
||||
|
||||
const removeStartTime = (time) => {
|
||||
setStartTimes(startTimes.filter((item) => item !== time));
|
||||
};
|
||||
|
||||
const assignDriver = async (scheduleId, value) => {
|
||||
try {
|
||||
await client.put(`/admin/weekly-schedules/${scheduleId}/shift`, {
|
||||
driverId: value ? Number(value) : null,
|
||||
});
|
||||
setMessage("Driver assignment updated.");
|
||||
onChange();
|
||||
} catch (error) {
|
||||
setMessage(
|
||||
error.response?.data?.error ||
|
||||
"Could not update the driver assignment.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSchedule = async (scheduleId) => {
|
||||
if (!window.confirm("Delete this weekly schedule?")) return;
|
||||
try {
|
||||
await client.delete(`/admin/weekly-schedules/${scheduleId}`);
|
||||
setMessage("Weekly schedule deleted.");
|
||||
onChange();
|
||||
} catch (error) {
|
||||
setMessage(
|
||||
error.response?.data?.error || "Could not delete the schedule.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h3>Weekly schedules</h3>
|
||||
<p>
|
||||
Add multiple daily departures for weekdays, Saturday, or Sunday,
|
||||
then assign a driver to the generated templates.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={createSchedules} className="stack-form">
|
||||
<div className="form-row">
|
||||
<select
|
||||
value={lineId}
|
||||
onChange={(event) => setLineId(event.target.value)}
|
||||
>
|
||||
<option value="">Choose bus line</option>
|
||||
{busLines.map((line) => (
|
||||
<option key={line.id} value={line.id}>
|
||||
Line {line.lineNumber} ({line.startStop} to {line.endStop})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(event) => setCategory(event.target.value)}
|
||||
>
|
||||
{DAY_GROUPS.map((group) => (
|
||||
<option key={group.value} value={group.value}>
|
||||
{group.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row" style={{ alignItems: "center" }}>
|
||||
<input
|
||||
type="time"
|
||||
value={newStartTime}
|
||||
onChange={(event) => setNewStartTime(event.target.value)}
|
||||
aria-label="New departure time"
|
||||
/>
|
||||
<button type="button" className="secondary" onClick={addStartTime}>
|
||||
Add departure
|
||||
</button>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "0.5rem",
|
||||
flexWrap: "wrap",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{startTimes.map((time) => (
|
||||
<span
|
||||
key={time}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
gap: "0.4rem",
|
||||
alignItems: "center",
|
||||
padding: "0.4rem 0.6rem",
|
||||
border: "1px solid #c7dcff",
|
||||
borderRadius: "999px",
|
||||
background: "#eaf2ff",
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeStartTime(time)}
|
||||
aria-label={`Remove departure ${time}`}
|
||||
style={{
|
||||
border: "none",
|
||||
background: "transparent",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<select
|
||||
value={driverId}
|
||||
onChange={(event) => setDriverId(event.target.value)}
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
{drivers.map((driver) => (
|
||||
<option key={driver.id} value={driver.id}>
|
||||
{driver.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" disabled={saving}>
|
||||
{saving ? "Saving..." : `Create ${startTimes.length} departures`}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{message && <div className="message">{message}</div>}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
||||
gap: "1rem",
|
||||
marginTop: "1.5rem",
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
{DAY_GROUPS.map((group) => (
|
||||
<section
|
||||
key={group.value}
|
||||
style={{
|
||||
minWidth: 0,
|
||||
border: "1px solid #dbe3ef",
|
||||
borderRadius: "8px",
|
||||
background: "#f8fafc",
|
||||
padding: "0.75rem",
|
||||
}}
|
||||
>
|
||||
<h4 style={{ marginTop: 0 }}>{group.label}</h4>
|
||||
{(groupedSchedules[group.value] || []).length === 0 ? (
|
||||
<p>No schedules configured.</p>
|
||||
) : (
|
||||
<div style={{ display: "grid", gap: "0.75rem" }}>
|
||||
{groupedSchedules[group.value].map((schedule) => (
|
||||
<article key={schedule.id} className="schedule-card">
|
||||
<div className="schedule-header">
|
||||
<strong>Line {schedule.lineNumber}</strong>
|
||||
<span className="schedule-time">
|
||||
{String(schedule.startTime).slice(0, 5)} -{" "}
|
||||
{String(schedule.endTime).slice(0, 5)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="schedule-details">
|
||||
{dayLabel(schedule.dayOfWeek)}: {schedule.startStop} to{" "}
|
||||
{schedule.endStop}
|
||||
</div>
|
||||
<select
|
||||
value={schedule.driverId || ""}
|
||||
onChange={(event) =>
|
||||
assignDriver(schedule.id, event.target.value)
|
||||
}
|
||||
style={{ width: "100%", marginTop: "0.75rem" }}
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
{drivers.map((driver) => (
|
||||
<option key={driver.id} value={driver.id}>
|
||||
{driver.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="danger"
|
||||
onClick={() => removeSchedule(schedule.id)}
|
||||
style={{ width: "100%", marginTop: "0.5rem" }}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,43 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const db = require('./db');
|
||||
require('dotenv').config();
|
||||
const express = require("express");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const db = require("./db");
|
||||
require("dotenv").config();
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Simple CORS middleware for browser-based admin UI testing.
|
||||
app.use((req, res, next) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
||||
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||
if (req.method === "OPTIONS") {
|
||||
return res.sendStatus(204);
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
const ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || 'http://localhost:5003/error_log';
|
||||
const ERROR_LOG_API_URL =
|
||||
process.env.ERROR_LOG_API_URL ||
|
||||
"https://error.xtransit.testinstance.nl/error_log";
|
||||
|
||||
function friendlyErrorMessage(statusCode) {
|
||||
if (statusCode === 400) return 'Controleer je invoer en probeer opnieuw.';
|
||||
if (statusCode === 401) return 'Je sessie is verlopen. Log opnieuw in.';
|
||||
if (statusCode === 403) return 'Je hebt hier geen toegang toe.';
|
||||
if (statusCode === 404) return 'Niet gevonden.';
|
||||
return 'Er ging iets mis. Probeer het later opnieuw.';
|
||||
if (statusCode === 400) return "Controleer je invoer en probeer opnieuw.";
|
||||
if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in.";
|
||||
if (statusCode === 403) return "Je hebt hier geen toegang toe.";
|
||||
if (statusCode === 404) return "Niet gevonden.";
|
||||
return "Er ging iets mis. Probeer het later opnieuw.";
|
||||
}
|
||||
|
||||
async function reportError({ partOfService, error }) {
|
||||
async function reportError({ partofservice, error }) {
|
||||
try {
|
||||
await fetch(ERROR_LOG_API_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
service: 'auth_api',
|
||||
partOfService,
|
||||
service: "auth_api",
|
||||
partofservice,
|
||||
error,
|
||||
}),
|
||||
});
|
||||
@@ -45,9 +47,13 @@ async function reportError({ partOfService, error }) {
|
||||
app.use((req, res, next) => {
|
||||
const originalJson = res.json.bind(res);
|
||||
res.json = (body) => {
|
||||
if (res.statusCode >= 400 && body && typeof body === 'object') {
|
||||
const rawError = body.error || body.message || body.details || `HTTP ${res.statusCode}`;
|
||||
void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: rawError });
|
||||
if (res.statusCode >= 400 && body && typeof body === "object") {
|
||||
const rawError =
|
||||
body.error || body.message || body.details || `HTTP ${res.statusCode}`;
|
||||
void reportError({
|
||||
partofservice: `${req.method} ${req.originalUrl}`,
|
||||
error: rawError,
|
||||
});
|
||||
return originalJson({ error: friendlyErrorMessage(res.statusCode) });
|
||||
}
|
||||
return originalJson(body);
|
||||
@@ -58,46 +64,59 @@ app.use((req, res, next) => {
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET;
|
||||
const ADMIN_USER_CODES = new Set(
|
||||
String(process.env.ADMIN_USER_CODES || '')
|
||||
.split(',')
|
||||
.map(c => Number(c.trim()))
|
||||
.filter(Number.isFinite)
|
||||
String(process.env.ADMIN_USER_CODES || "")
|
||||
.split(",")
|
||||
.map((c) => Number(c.trim()))
|
||||
.filter(Number.isFinite),
|
||||
);
|
||||
|
||||
|
||||
app.post('/login', async (req, res) => {
|
||||
app.post("/login", async (req, res) => {
|
||||
try {
|
||||
const { userCode, password } = req.body;
|
||||
if (!userCode || !password) return res.status(400).json({ error: 'userCode and password required' });
|
||||
if (!userCode || !password)
|
||||
return res.status(400).json({ error: "userCode and password required" });
|
||||
|
||||
const result = await db.query('SELECT id, usercode, password FROM workers.users WHERE usercode = $1', [userCode]);
|
||||
const result = await db.query(
|
||||
"SELECT id, usercode, password FROM workers.users WHERE usercode = $1",
|
||||
[userCode],
|
||||
);
|
||||
const user = result.rows[0];
|
||||
if (!user) return res.status(401).json({ error: 'invalid_credentials' });
|
||||
if (!user) return res.status(401).json({ error: "invalid_credentials" });
|
||||
|
||||
const ok = await bcrypt.compare(password, user.password);
|
||||
if (!ok) return res.status(401).json({ error: 'invalid_credentials' });
|
||||
if (!ok) return res.status(401).json({ error: "invalid_credentials" });
|
||||
|
||||
const isAdmin = ADMIN_USER_CODES.has(Number(user.usercode));
|
||||
const payload = { userId: user.id, userCode: user.usercode, isAdmin };
|
||||
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
|
||||
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1h" });
|
||||
|
||||
res.json({ token, user: { id: user.id, userCode: user.usercode, job: user.job, isAdmin } });
|
||||
res.json({
|
||||
token,
|
||||
user: { id: user.id, userCode: user.usercode, job: user.job, isAdmin },
|
||||
});
|
||||
} catch (err) {
|
||||
await reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) });
|
||||
await reportError({
|
||||
partofservice: `${req.method} ${req.originalUrl}`,
|
||||
error: err?.stack || err?.message || String(err),
|
||||
});
|
||||
console.error(err);
|
||||
res.status(500).json({ error: friendlyErrorMessage(500) });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/me', async (req, res) => {
|
||||
app.get("/me", async (req, res) => {
|
||||
const auth = req.headers.authorization;
|
||||
if (!auth || !auth.startsWith('Bearer ')) return res.status(401).json({ error: 'missing_token' });
|
||||
if (!auth || !auth.startsWith("Bearer "))
|
||||
return res.status(401).json({ error: "missing_token" });
|
||||
const token = auth.slice(7);
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET);
|
||||
res.json({ user: payload });
|
||||
} catch (err) {
|
||||
void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) });
|
||||
void reportError({
|
||||
partofservice: `${req.method} ${req.originalUrl}`,
|
||||
error: err?.stack || err?.message || String(err),
|
||||
});
|
||||
res.status(401).json({ error: friendlyErrorMessage(401) });
|
||||
}
|
||||
});
|
||||
@@ -110,4 +129,8 @@ app.get('/me', async (req, res) => {
|
||||
const PORT = process.env.PORT || 5000;
|
||||
app.listen(PORT, () => console.log(`Auth API listening on ${PORT}`));
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", service: "auth_api" });
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
#github
|
||||
.github/
|
||||
@@ -1,3 +0,0 @@
|
||||
# bus_terminal
|
||||
|
||||
A new Flutter project.
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"name": "bus_terminal",
|
||||
"short_name": "bus_terminal",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-maskable-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Flutter / Dart
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
/coverage/
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
|
||||
# IDE / editor
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
*.swp
|
||||
|
||||
# Environment and secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Generated and platform artifacts
|
||||
android/key.properties
|
||||
android/app/debug
|
||||
android/app/profile
|
||||
android/app/release
|
||||
|
||||
# Logs and temp files
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
# Symbolication / obfuscation
|
||||
*.symbols
|
||||
*.map.json
|
||||
|
||||
# Legacy migration note
|
||||
legacy_windows_xp_plan.md
|
||||
|
||||
|
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 442 B |
|
Before Width: | Height: | Size: 721 B After Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 295 B After Width: | Height: | Size: 295 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 450 B After Width: | Height: | Size: 450 B |
|
Before Width: | Height: | Size: 282 B After Width: | Height: | Size: 282 B |
|
Before Width: | Height: | Size: 462 B After Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 704 B After Width: | Height: | Size: 704 B |
|
Before Width: | Height: | Size: 406 B After Width: | Height: | Size: 406 B |
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 586 B |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 862 B After Width: | Height: | Size: 862 B |
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 762 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
@@ -15,7 +15,7 @@ void main() {
|
||||
FlutterError.onError = (details) {
|
||||
FlutterError.presentError(details);
|
||||
void report() => ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
service: 'deprecated',
|
||||
partOfService: 'FlutterError.onError',
|
||||
error: details.exceptionAsString(),
|
||||
);
|
||||
@@ -26,23 +26,26 @@ void main() {
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
// ignore: unawaited_futures
|
||||
ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
service: 'deprecated',
|
||||
partOfService: 'PlatformDispatcher.onError',
|
||||
error: '$error\n$stack',
|
||||
);
|
||||
return true;
|
||||
};
|
||||
|
||||
runZonedGuarded(() {
|
||||
runApp(const MainApp());
|
||||
}, (error, stack) {
|
||||
// ignore: unawaited_futures
|
||||
ErrorLogService.report(
|
||||
service: 'bus_terminal',
|
||||
partOfService: 'runZonedGuarded',
|
||||
error: '$error\n$stack',
|
||||
);
|
||||
});
|
||||
runZonedGuarded(
|
||||
() {
|
||||
runApp(const MainApp());
|
||||
},
|
||||
(error, stack) {
|
||||
// ignore: unawaited_futures
|
||||
ErrorLogService.report(
|
||||
service: 'deprecated',
|
||||
partOfService: 'runZonedGuarded',
|
||||
error: '$error\n$stack',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class MainApp extends StatelessWidget {
|
||||
@@ -80,10 +83,10 @@ class _InitialRoute extends StatelessWidget {
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// If logged in, go to schedule; otherwise go to login
|
||||
return snapshot.data == true
|
||||
? const ScheduleOverviewScreen()
|
||||
return snapshot.data == true
|
||||
? const ScheduleOverviewScreen()
|
||||
: const LoginScreen();
|
||||
},
|
||||
);
|
||||