7 Commits

Author SHA1 Message Date
Sven laptop a17125bdee fixed monitor api polling
Build and Push Docker Images / detect-changes (push) Successful in 10s
Build and Push Docker Images / build-auth-api (push) Successful in 1m6s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 58s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m10s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m15s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m16s
Build and Push Docker Images / deploy-test-node (push) Successful in 4s
2026-09-21 19:59:53 +02:00
Sven laptop 217dfd3abc Update readme's, branches and actions
Build and Push Docker Images / detect-changes (push) Successful in 9s
Build and Push Docker Images / build-auth-api (push) Successful in 1m5s
Build and Push Docker Images / build-employee-api (push) Successful in 1m1s
Build and Push Docker Images / build-transit-api (push) Successful in 57s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m16s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m13s
Build and Push Docker Images / deploy-test-node (push) Successful in 5s
2026-09-21 00:15:05 +02:00
Sven laptop 635a130f0b ──────
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m1s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m5s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m13s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
### Summary of Changes
  1. Documentation Consolidation:
      • Relocated the docs/ folder and all repository .md files (README.md, DEPLOYMENT.md, DOCKER_README.md,
      DOMAINS_README.md, GETTING_STARTED.md, CI_CD_PORTAINER_GUIDE.md, ADMIN_DEPLOYMENT_SUMMARY.md,
      SECURITY_PRIVATE_FILES.md, handoff.md, azure-project-plan.md, plus service-specific READMEs) into .
      • Updated document links in README.md.
  2. AI Agent Project Map:
      • Created PROJECT_MAP.md in the project root containing complete architecture diagrams, service specs, ports, DB
      schemas, environment variables, and developer workflows.
      • Added PROJECT_MAP.md to .gitignore.
  3. Monitoring UI & Infrastructure Integration:
      • Added monitoring_ui service to docker-compose.yml (port 5175).
      • Configured Gitea workflow docker-build.yml for monitoring_ui builds.
      • Added CORS middleware headers in index.js.
      • Added nginx.conf copy step in Dockerfile.

  ──────
  ### Recommended Commit Message

    docs: consolidate project documentation into monitoring_ui and add agent project map

    - Move all markdown documentation files and docs/ folder into monitoring_ui/docs/
    - Update documentation hub index (monitoring_ui/docs/README.md) with relative paths
    - Generate comprehensive AI agent reference map (PROJECT_MAP.md) in repository root
    - Add PROJECT_MAP.md and monitoring_ui build output/env overrides to .gitignore
    - Integrate monitoring_ui service into docker-compose.yml and Gitea CI workflow
    - Configure CORS handling in employee_api for monitoring dashboard access
    ──────
  ### Alternative Short Commit Message

    refactor(docs): move docs to monitoring_ui and create AI agent PROJECT_MAP.md
2026-09-20 23:33:55 +02:00
Sven laptop f935bbeb8c updated transit api
Build and Push Docker Images / detect-changes (push) Successful in 10s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 1m3s
Build and Push Docker Images / build-transit-api (push) Successful in 59s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m9s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m20s
Build and Push Docker Images / deploy-test-node (push) Successful in 4s
2026-09-18 20:08:10 +02:00
Sven laptop 1154d1d658 chore: standardize service domains and error logging
Build and Push Docker Images / detect-changes (push) Successful in 15s
Build and Push Docker Images / build-auth-api (push) Successful in 1m2s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m2s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m10s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m22s
Build and Push Docker Images / deploy-test-node (push) Successful in 4s
2026-09-18 19:26:05 +02:00
Sven laptop 3f0a02aa04 feat: add weekly schedule shifts and bulk departures
Build and Push Docker Images / detect-changes (push) Successful in 11s
Build and Push Docker Images / build-auth-api (push) Successful in 1m1s
Build and Push Docker Images / build-employee-api (push) Successful in 59s
Build and Push Docker Images / build-transit-api (push) Successful in 56s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m11s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m37s
Build and Push Docker Images / deploy-test-node (push) Successful in 5s
2026-09-18 18:00:51 +02:00
Sven laptop 9d1a37e248 Update readme's, branches and actions
Build and Push Docker Images / detect-changes (push) Successful in 10s
Build and Push Docker Images / build-auth-api (push) Successful in 58s
Build and Push Docker Images / build-employee-api (push) Successful in 58s
Build and Push Docker Images / build-transit-api (push) Successful in 59s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m8s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m14s
Build and Push Docker Images / deploy-test-node (push) Successful in 5s
2026-09-17 12:04:44 +02:00
216 changed files with 13217 additions and 3213 deletions
+536
View File
@@ -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>
+479
View File
@@ -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>
+30
View File
@@ -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"
}
]
}
+495
View File
@@ -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>
+608
View File
@@ -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>
+592
View File
@@ -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;
}
}
+247
View File
@@ -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"
}
+265 -38
View File
@@ -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:
+73 -18
View File
@@ -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/
-315
View File
@@ -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
-240
View File
@@ -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`
-157
View File
@@ -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.
+142 -116
View File
@@ -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).
+1
View File
@@ -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
+3 -3
View File
@@ -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 -3
View File
@@ -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
+14
View File
@@ -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;
}
}
+69 -50
View File
@@ -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 };
+45 -31
View File
@@ -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>
)
);
}
+34 -66
View File
@@ -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>
)
);
}
+188 -85
View File
@@ -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>
);
}
+62 -39
View File
@@ -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;
-48
View File
@@ -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/
-3
View File
@@ -1,3 +0,0 @@
# bus_terminal
A new Flutter project.
-35
View File
@@ -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"
}
]
}
+44
View File
@@ -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: 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();
},
);

Some files were not shown because too many files have changed in this diff Show More