10 Commits

Author SHA1 Message Date
Sven laptop 89e4f90fc3 Fix monitoring UI 2026-09-23 17:23:04 +02:00
Sven laptop 39b0b07a77 updated docker build to always deplyo 2026-09-23 17:22:14 +02:00
Sven laptop 48c710bfbf Fix admin check on monitoring UI
Build and Push Docker Images / detect-changes (push) Successful in 12s
Build and Push Docker Images / build-auth-api (push) Has been skipped
Build and Push Docker Images / build-employee-api (push) Has been skipped
Build and Push Docker Images / build-transit-api (push) Has been skipped
Build and Push Docker Images / build-error-log-api (push) Has been skipped
Build and Push Docker Images / build-admin-ui (push) Has been skipped
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
2026-09-23 17:12:41 +02:00
Sven laptop 1c055d84e8 Merge branch 'driver_ui' into dev
Build and Push Docker Images / build-auth-api (push) Has been skipped
Build and Push Docker Images / build-employee-api (push) Has been skipped
Build and Push Docker Images / detect-changes (push) Successful in 10s
Build and Push Docker Images / build-transit-api (push) Has been skipped
Build and Push Docker Images / build-error-log-api (push) Has been skipped
Build and Push Docker Images / build-admin-ui (push) Has been skipped
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m30s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
2026-09-23 10:00:20 +02:00
Sven laptop d7100e6f2a Add docs tab with local READMEs and login form to monitoring UI
Build and Push Docker Images / detect-changes (pull_request) Successful in 13s
Build and Push Docker Images / build-transit-api (pull_request) Has been skipped
Build and Push Docker Images / build-monitoring-ui (pull_request) Has been skipped
Build and Push Docker Images / deploy-test-node (pull_request) Has been skipped
Build and Push Docker Images / build-employee-api (pull_request) Has been skipped
Build and Push Docker Images / build-auth-api (pull_request) Has been skipped
Build and Push Docker Images / build-error-log-api (pull_request) Has been skipped
Build and Push Docker Images / build-admin-ui (pull_request) Has been skipped
Build and Push Docker Images / detect-changes (push) Successful in 9s
Build and Push Docker Images / build-auth-api (push) Successful in 1m0s
Build and Push Docker Images / build-employee-api (push) Successful in 59s
Build and Push Docker Images / build-transit-api (push) Successful in 57s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m4s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m17s
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m14s
Build and Push Docker Images / deploy-test-node (push) Successful in 3s
2026-09-22 00:14:01 +02:00
Sven laptop 6e0b7e4793 Empty commit for docker-compose.yml
Build and Push Docker Images / detect-changes (push) Successful in 9s
Build and Push Docker Images / build-auth-api (push) Has been skipped
Build and Push Docker Images / build-employee-api (push) Has been skipped
Build and Push Docker Images / build-transit-api (push) Has been skipped
Build and Push Docker Images / build-error-log-api (push) Has been skipped
Build and Push Docker Images / build-admin-ui (push) Has been skipped
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m11s
Build and Push Docker Images / deploy-test-node (push) Successful in 4s
2026-09-22 00:01:30 +02:00
Sven laptop da5c6b2839 added docs tab and improved authentication on dash
Build and Push Docker Images / detect-changes (push) Successful in 9s
Build and Push Docker Images / build-transit-api (push) Has been skipped
Build and Push Docker Images / build-auth-api (push) Has been skipped
Build and Push Docker Images / build-employee-api (push) Has been skipped
Build and Push Docker Images / build-error-log-api (push) Has been skipped
Build and Push Docker Images / build-admin-ui (push) Has been skipped
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m11s
Build and Push Docker Images / deploy-test-node (push) Has been skipped
2026-09-21 23:53:54 +02:00
Sven laptop 38782bca6d fixed 304 errors and removed admin ui from dash
Build and Push Docker Images / detect-changes (push) Successful in 10s
Build and Push Docker Images / build-auth-api (push) Has been skipped
Build and Push Docker Images / build-employee-api (push) Has been skipped
Build and Push Docker Images / build-transit-api (push) Has been skipped
Build and Push Docker Images / build-error-log-api (push) Has been skipped
Build and Push Docker Images / build-admin-ui (push) Has been skipped
Build and Push Docker Images / build-monitoring-ui (push) Successful in 1m15s
Build and Push Docker Images / deploy-test-node (push) Has been skipped
2026-09-21 23:36:07 +02:00
Sven laptop ebcc485f51 updated ui to use multiple screens 2026-09-18 17:15:00 +02:00
Sven laptop c09b3f3f66 created driver_ui with login and viewing assigned routes 2026-09-18 00:03:58 +02:00
10 changed files with 969 additions and 94 deletions
+20 -11
View File
@@ -297,17 +297,26 @@ jobs:
tags: ${{ steps.tags.outputs.tags }} tags: ${{ steps.tags.outputs.tags }}
deploy-test-node: deploy-test-node:
needs: 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' }}
detect-changes, strategy:
build-auth-api, fail-fast: false
build-employee-api, matrix:
build-transit-api, include:
build-error-log-api, - name: auth-api
build-admin-ui, need_job: build-auth-api
build-monitoring-ui, - name: employee-api
] need_job: build-employee-api
if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && gitea.event_name == 'push' && (gitea.ref_name == 'dev' || gitea.ref_name == 'main') }} - name: transit-api
need_job: build-transit-api
- name: error-log-api
need_job: build-error-log-api
- name: admin-ui
need_job: build-admin-ui
- name: monitoring-ui
need_job: build-monitoring-ui
if: ${{ needs[matrix.value.need_job].result == 'success' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -0,0 +1,47 @@
# Auth API
Authentication service for XTransit. Handles login, JWT token generation, and user verification.
## Setup
### Environment Variables
```env
PG_HOST=localhost
PG_PORT=5432
PG_USER=your_user
PG_PASS=your_password
PG_DB=your_database
JWT_SECRET=your_jwt_secret
PORT=5000
ADMIN_USER_CODES=1234,5678
DEV_BYPASS=0
ERROR_LOG_API_URL=https://error.xtransit.testinstance.nl/error_log
```
### Start API
```bash
npm start
```
Runs at `https://auth.xtransit.testinstance.nl`
## API Endpoints
**POST** `/login`
- Body: `{ "userCode": 1234, "password": "secret" }`
- Returns: `{ "token": "jwt...", "user": { "id": 1, "userCode": 1234, "job": "driver", "isAdmin": false } }`
**GET** `/me`
- Requires: `Authorization: Bearer <token>`
- Returns: `{ "user": { "userId": 1, "userCode": 1234, "isAdmin": false } }`
**GET** `/health`
- Returns: `{ "status": "ok", "service": "auth_api" }`
## Database
Uses `workers.users` table with columns: `id`, `usercode`, `password` (bcrypt), `job`
Admin status is determined by `ADMIN_USER_CODES` env variable (comma-separated user codes).
@@ -0,0 +1,39 @@
# Employee API
Employee management service for XTransit. Handles employee onboarding and records.
## Setup
### Environment Variables
```env
PG_HOST=localhost
PG_PORT=5432
PG_USER=your_user
PG_PASS=your_password
PG_DB=your_database
PORT=4000
ERROR_LOG_API_URL=https://error.xtransit.testinstance.nl/error_log
```
### Start API
```bash
npm start
```
Runs at `https://employees.xtransit.testinstance.nl`
## API Endpoints
**POST** `/createemployee`
- Body: `{ "firstname": "John", "lastname": "Doe", "job": "driver", "password": "secret" }`
- Creates employee in `workers.employees` and user in `workers.users`
- Returns: `{ "employee": "John", "code": "1234" }`
**GET** `/health`
- Returns: `{ "endpoint": "reachable", "database": "ok" }` or 503 if DB down
## Database
Uses `workers.employees` (id, firstname, lastname, job) and `workers.users` (id, employee, password, usercode)
@@ -0,0 +1,45 @@
# Error Log API
Centralized error logging service for XTransit. Collects errors from all services.
## Setup
### Environment Variables
```env
PG_HOST=localhost
PG_PORT=5432
PG_USER=your_user
PG_PASS=your_password
PG_DB=your_database
PORT=5003
JWT_SECRET=your_jwt_secret
```
### Start API
```bash
npm start
```
Runs at `https://error.xtransit.testinstance.nl`
## API Endpoints
**POST** `/error_log`
- Body: `{ "service": "transit_api", "partofservice": "/schedule/daily", "error": "DB connection failed" }`
- Logs error to database
- Returns: `{ "ok": true }`
**GET** `/error_log?limit=100&service=transit_api`
- Requires: `Authorization: Bearer <admin_token>`
- Returns: `{ "errors": [...] }`
**GET** `/health`
- Returns: `{ "status": "ok", "service": "error_log_api", "database": "ok" }` or 503
## Database
Uses `data.errors` table (id, date, service, partofservice, error)
Admin access controlled via JWT `isAdmin` claim from auth_api.
@@ -0,0 +1,386 @@
# Transit API
Transit API for managing bus routes, navigation, and GPS tracking for bus drivers.
## Project Structure
```
transit_api/
├── index.js # Main API endpoints
├── middleware.js # JWT authentication middleware
├── db.js # PostgreSQL connection
├── schema.sql # Database schema setup
├── package.json # Dependencies
└── .env # Environment variables
```
## Setup
### 1. Install Dependencies
```bash
npm install
```
### 2. Set Up Database Schema
Execute the following in PostgreSQL:
```bash
# Via psql command line
psql -U your_user -d your_database -f schema.sql
```
Or copy-paste the SQL queries directly into pgAdmin.
### 3. Environment Variables (.env)
```env
PG_HOST=localhost
PG_PORT=5432
PG_USER=your_user
PG_PASS=your_password
PG_DB=your_database
JWT_SECRET=your_jwt_secret
PORT=5001
```
Note: `ADMIN_USER_CODES` has been moved to the `auth_api` service. Configure admin user codes in `auth_api/.env` as a comma-separated list.
### 4. Start API
```bash
npm start
```
The API runs at `https://transit.xtransit.testinstance.nl`
The service group also includes the Employee API at
`https://employees.xtransit.testinstance.nl` and the central monitoring
dashboard. See [`../MONITORING_UI.md`](../MONITORING_UI.md) for health
checks, error inspection, and the controlled request tester.
## API Endpoints
All endpoints require an `Authorization: Bearer {token}` header.
**GET** `/health`
- Checks whether the deployed transit API is running and the database is reachable.
- Returns `200` with `{"status":"ok","database":"ok"}` on success.
- Returns `503` if the API is running but the database is unreachable.
### Schedule
Schedules are weekly templates. Each row in `transit.schedules` represents one
departure time for one bus line and one `day_of_week`:
- `1` to `5`: weekdays
- `6`: Saturday
- `0`: Sunday
Driver assignments are stored separately in `transit.shifts` with
`(schedule_id, driver_id)`, where `driver_id` references `workers.users(id)`.
The current `shifts` shape represents the active assignment. To retain different
assignments for different calendar weeks, add a `week_start` (or assignment date
range) column and include it in the unique key.
**GET** `/driver/schedules/daily?date=2026-09-18`
- Returns only weekly schedule templates assigned to the authenticated driver through `transit.shifts`.
**PATCH** `/driver/weekly-schedules/:scheduleId/status`
- Updates the status of an assigned weekly schedule.
**GET** `/admin/weekly-schedules`
- Lists weekly templates and their current driver assignments.
**GET** `/admin/shift-drivers`
- Lists drivers from `workers.users` for shift assignment.
**POST** `/admin/weekly-schedules`
- Creates one template. Body: `{ "busLineId": 1, "dayOfWeek": 1, "startTime": "08:00", "endTime": "09:00", "driverId": 12 }`.
**POST** `/admin/weekly-schedules/bulk`
- Creates multiple departures in one transaction. The API derives each end time
from the selected bus line duration. Body: `{ "busLineId": 1, "dayOfWeeks": [1,2,3,4,5], "startTimes": ["06:00", "06:30", "07:00"], "driverId": 12 }`.
**PUT** `/admin/weekly-schedules/:scheduleId/shift`
- Replaces the driver assignment. Pass `{ "driverId": 12 }` or `{ "driverId": null }`.
The legacy `/admin/schedules` and `/schedule/daily` endpoints remain for older
clients, but new clients must use the weekly schedule endpoints above.
**GET** `/schedule/daily?date=2024-05-05`
- Retrieve daily schedule for driver
- Response:
```json
{
"date": "2024-05-05",
"routes": [
{
"id": 1,
"startTime": "2024-05-05T08:00:00Z",
"status": "scheduled",
"busLine": {
"lineNumber": 1,
"startStop": "Central Station",
"endStop": "Airport"
},
"busType": {
"name": "Standard Bus",
"seatCapacity": 50,
"licensePlate": "BUS-001"
}
}
]
}
```
**GET** `/routes/:routeId`
- Details of a specific route
- Response: `{ "route": {...} }`
### Public Schedule Overview
**GET** `/schedules/overview?date=2026-05-06&lineId=1`
- Fetches a date-based overview of scheduled services
- Weekly templates are selected by `day_of_week` and driver assignments by `shifts`
- One-off / incidental services are only included on their exact date
- Filters: `driverId`/`chauffeurId`, `stopId`/`busStopId`, `lineId`, `status`
- Response: `{ "date": "2026-05-06", "summary": {...}, "schedules": [...] }`
The legacy endpoint **GET** `/schedules` remains available for existing clients.
Example:
```bash
curl "https://transit.xtransit.testinstance.nl/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3"
```
### Navigation
**POST** `/navigation/route`
- Save calculated route from OSRM
- Body:
```json
{
"routeId": 1,
"startLat": 52.5200,
"startLon": 13.4050,
"endLat": 52.5300,
"endLon": 13.4150,
"totalDistance": 15000,
"totalDuration": 900,
"geojson": {
"type": "LineString",
"coordinates": [[13.4050, 52.5200], ...]
}
}
```
**GET** `/bus-lines/:busLineId/stops`
- All stops of a bus line (for navigation waypoints)
- Response:
```json
{
"stops": [
{
"id": 1,
"order": 1,
"name": "Central Station",
"latitude": 52.52,
"longitude": 13.405
}
]
}
```
### GPS Tracking
**POST** `/gps/track`
- Save real-time position
- Body:
```json
{
"routeId": 1,
"latitude": 52.521,
"longitude": 13.406,
"speed": 45.5,
"heading": 180,
"accuracy": 5.0
}
```
**GET** `/gps/route/:routeId`
- All recorded position points of a route
- Response:
```json
{
"positions": [
{
"id": 1,
"latitude": 52.521,
"longitude": 13.406,
"speed": 45.5,
"heading": 180,
"recordedAt": "2024-05-05T08:15:00Z"
}
]
}
```
### Route Status
**PATCH** `/routes/:routeId/status`
- Update route status
- Body: `{ "status": "in_progress" }`
- Valid statuses: `scheduled`, `in_progress`, `completed`, `cancelled`
### Admin
All admin endpoints require, in addition to a valid JWT token, that your `userCode` is present in `ADMIN_USER_CODES`.
**POST** `/admin/stops`
- Create a reusable stop
- Body:
```json
{
"name": "Central Station",
"latitude": 52.52,
"longitude": 13.405
}
```
**POST** `/admin/bus-lines/:busLineId/stops`
- Link an existing stop to a bus line
- Body:
```json
{
"stopId": 1,
"stopOrder": 1,
"estimatedArrivalMinutes": 0
}
```
## Database Schema
### bus_types
- `id` - Primary key
- `name` - Bus type name (Standard Bus, etc.)
- `seat_capacity` - Number of seats
- `license_plate` - Unique license plate
### bus_lines
- `id` - Primary key
- `line_number` - Unique line number (1, 5, 12, etc.)
- `start_stop` - Start stop name
- `end_stop` - End stop name
- `estimated_duration_minutes` - Expected route duration
### stops
- `id` - Primary key
- `name` - Stop name
- `latitude`, `longitude` - Stop location
### route_stops
- `id` - Primary key
- `bus_line_id` - Which bus line the stop belongs to
- `stop_id` - Reusable stop reference
- `stop_order` - Stop sequence on that bus line
- `estimated_arrival_minutes` - Optional ETA per stop
### routes
- `id` - Primary key
- `user_id` - Driver (references workers.users)
- `bus_line_id` - Which line (references bus_lines)
- `bus_type_id` - Which bus (references bus_types)
- `start_time` - Scheduled start
- `status` - scheduled/in_progress/completed/cancelled
### navigation_routes
- `id` - Primary key
- `route_id` - Link to route (unique)
- `polyline_geojson` - GeoJSON LineString of the route
- `total_distance_meters` - Route distance
- `total_duration_seconds` - Estimated duration
### gps_tracking
- `id` - Primary key
- `route_id` - Which route
- `latitude`, `longitude` - Current position
- `speed_kmh` - Current speed
- `recorded_at` - Timestamp
## Integration with Flutter App
```dart
// In schedule_service.dart
// Save navigation route
await scheduleService.saveNavigationRoute(
routeId: 1,
navRoute: navigationRoute,
);
// Track GPS
await scheduleService.trackGPS(
routeId: 1,
currentPos: navigationPoint,
speed: 45.5,
heading: 180,
);
// Update route status
await scheduleService.updateRouteStatus(1, 'in_progress');
```
## Error Handling
All endpoints return error responses:
```json
{
"error": "error_code",
"details": "More information"
}
```
Common errors:
- `missing_token` - No Authorization header
- `invalid_token` - Token verification failed
- `missing_required_fields` - Required data missing
- `invalid_status` - Invalid status value
- `route_not_found` - Route not found
- `unauthorized` - User doesn't own this resource
+236 -44
View File
@@ -4,26 +4,10 @@ const runtime =
typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {}; typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {};
const configured = { const configured = {
auth: auth: runtime.AUTH_API || import.meta.env.VITE_AUTH_API || "",
runtime.AUTH_API || employee: runtime.EMPLOYEE_API || import.meta.env.VITE_EMPLOYEE_API || "",
import.meta.env.VITE_AUTH_API || transit: runtime.TRANSIT_API || import.meta.env.VITE_TRANSIT_API || "",
"http://localhost:5000", errors: runtime.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API || "",
employee:
runtime.EMPLOYEE_API ||
import.meta.env.VITE_EMPLOYEE_API ||
"http://localhost:4000",
transit:
runtime.TRANSIT_API ||
import.meta.env.VITE_TRANSIT_API ||
"http://localhost:5001",
errors:
runtime.ERROR_LOG_API ||
import.meta.env.VITE_ERROR_LOG_API ||
"http://localhost:5003",
admin:
runtime.ADMIN_UI ||
import.meta.env.VITE_ADMIN_UI ||
"http://localhost:5174",
}; };
const serviceMeta = { const serviceMeta = {
@@ -32,7 +16,8 @@ const serviceMeta = {
role: "Identity & tokens", role: "Identity & tokens",
color: "amber", color: "amber",
data: "Login, JWT sessions, current user profile", data: "Login, JWT sessions, current user profile",
routes: ["/login", "/me"], routes: ["/login", "/me", "/health"],
docs: "/docs/auth_api/README.md",
}, },
employee: { employee: {
label: "Employee API", label: "Employee API",
@@ -40,6 +25,7 @@ const serviceMeta = {
color: "teal", color: "teal",
data: "Employee onboarding and employee records", data: "Employee onboarding and employee records",
routes: ["/health", "/createemployee"], routes: ["/health", "/createemployee"],
docs: "/docs/employee_api/README.md",
}, },
transit: { transit: {
label: "Transit API", label: "Transit API",
@@ -52,6 +38,7 @@ const serviceMeta = {
"/schedule/daily?date=YYYY-MM-DD", "/schedule/daily?date=YYYY-MM-DD",
"/admin/stops", "/admin/stops",
], ],
docs: "/docs/transit_api/README.md",
}, },
errors: { errors: {
label: "Error Log API", label: "Error Log API",
@@ -59,13 +46,7 @@ const serviceMeta = {
color: "coral", color: "coral",
data: "Centralized bad requests and service errors", data: "Centralized bad requests and service errors",
routes: ["/health", "/error_log?limit=100"], routes: ["/health", "/error_log?limit=100"],
}, docs: "/docs/error_log_api/README.md",
admin: {
label: "Admin UI",
role: "Operator surface",
color: "violet",
data: "The deployed React administration console",
routes: ["/health"],
}, },
}; };
@@ -74,9 +55,14 @@ const healthPaths = {
employee: "/health", employee: "/health",
transit: "/health", transit: "/health",
errors: "/health", errors: "/health",
admin: "/health",
}; };
const testerRoutes = [ const testerRoutes = [
{
service: "auth",
method: "POST",
path: "/login",
label: "Login (userCode + password)",
},
{ service: "auth", method: "GET", path: "/me", label: "Current session" }, { service: "auth", method: "GET", path: "/me", label: "Current session" },
{ {
service: "employee", service: "employee",
@@ -127,6 +113,9 @@ function App() {
const [token, setToken] = useState( const [token, setToken] = useState(
() => localStorage.getItem("transit-monitor-token") || "", () => localStorage.getItem("transit-monitor-token") || "",
); );
const [user, setUser] = useState(() =>
JSON.parse(localStorage.getItem("transit-monitor-user") || "null"),
);
const [health, setHealth] = useState({}); const [health, setHealth] = useState({});
const [history, setHistory] = useState({}); const [history, setHistory] = useState({});
const [errors, setErrors] = useState([]); const [errors, setErrors] = useState([]);
@@ -142,13 +131,65 @@ function App() {
}); });
const [testerResult, setTesterResult] = useState(null); const [testerResult, setTesterResult] = useState(null);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState("");
const login = useCallback(
async (userCode, password) => {
const url = urls.auth;
if (!url) {
setLoginError("Auth API URL not configured");
return;
}
setLoginLoading(true);
setLoginError("");
try {
const response = await fetch(`${url}/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userCode, password }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`);
}
setToken(data.token);
setUser(data.user);
localStorage.setItem("transit-monitor-token", data.token);
localStorage.setItem("transit-monitor-user", JSON.stringify(data.user));
setLoginError("");
} catch (error) {
setLoginError(error.message);
} finally {
setLoginLoading(false);
}
},
[urls.auth],
);
const logout = useCallback(() => {
setToken("");
setUser(null);
localStorage.removeItem("transit-monitor-token");
localStorage.removeItem("transit-monitor-user");
}, []);
const checkService = useCallback( const checkService = useCallback(
async (key) => { async (key) => {
const started = performance.now(); const started = performance.now();
const url = urls[key];
if (!url) {
return {
ok: false,
status: 0,
body: { error: "No URL configured" },
latency: 0,
checkedAt: new Date().toISOString(),
};
}
const headers = token ? { Authorization: `Bearer ${token}` } : {}; const headers = token ? { Authorization: `Bearer ${token}` } : {};
try { try {
const response = await fetch(`${urls[key]}${healthPaths[key]}`, { const response = await fetch(`${url}${healthPaths[key]}`, {
headers, headers,
}); });
const body = await response.json().catch(() => null); const body = await response.json().catch(() => null);
@@ -346,6 +387,12 @@ function App() {
> >
Service catalog Service catalog
</button> </button>
<button
className={activeView === "docs" ? "active" : ""}
onClick={() => setActiveView("docs")}
>
Docs
</button>
<button <button
className={activeView === "incidents" ? "active" : ""} className={activeView === "incidents" ? "active" : ""}
onClick={() => setActiveView("incidents")} onClick={() => setActiveView("incidents")}
@@ -404,6 +451,7 @@ function App() {
</> </>
)} )}
{activeView === "catalog" && <Catalog />} {activeView === "catalog" && <Catalog />}
{activeView === "docs" && <Docs />}
{activeView === "incidents" && ( {activeView === "incidents" && (
<Incidents errors={errors} state={errorState} reload={loadErrors} /> <Incidents errors={errors} state={errorState} reload={loadErrors} />
)} )}
@@ -418,7 +466,15 @@ function App() {
/> />
)} )}
{activeView === "settings" && ( {activeView === "settings" && (
<Connections urls={urls} token={token} save={saveSettings} /> <Connections
urls={urls}
token={token}
user={user}
login={login}
logout={logout}
save={saveSettings}
loginLoading={loginLoading}
/>
)} )}
</main> </main>
</div> </div>
@@ -490,7 +546,7 @@ function QuickNotes() {
<li> <li>
<strong>Auth-aware checks</strong> <strong>Auth-aware checks</strong>
<span> <span>
Paste an admin JWT in Connections to unlock /me and error history. Login with admin credentials to unlock /me and error history.
</span> </span>
</li> </li>
<li> <li>
@@ -504,6 +560,57 @@ function QuickNotes() {
</div> </div>
); );
} }
function Docs() {
return (
<section className="panel docs-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">SERVICE DOCUMENTATION</p>
<h2>API Documentation & References</h2>
</div>
<span className="panel-count">
{Object.keys(serviceMeta).length} services
</span>
</div>
<div className="docs-grid">
{Object.entries(serviceMeta).map(([key, meta]) => (
<div className={`doc-card ${meta.color}`} key={key}>
<div className="doc-header">
<span className={`service-dot ${meta.color}`} />
<div>
<strong>{meta.label}</strong>
<p>{meta.role}</p>
</div>
</div>
<p className="doc-description">{meta.data}</p>
<div className="doc-links">
{meta.docs && (
<a
href={meta.docs}
target="_blank"
rel="noopener noreferrer"
className="doc-link"
>
View Documentation →
</a>
)}
<details className="routes-detail">
<summary>API Routes</summary>
<ul>
{meta.routes.map((route) => (
<li key={route}>
<code>{route}</code>
</li>
))}
</ul>
</details>
</div>
</div>
))}
</div>
</section>
);
}
function Catalog() { function Catalog() {
return ( return (
<section className="panel catalog-panel"> <section className="panel catalog-panel">
@@ -695,9 +802,88 @@ function RequestLab({ tester, setTester, result, run, busy, token }) {
</section> </section>
); );
} }
function Connections({ urls, token, save }) { function Connections({ urls, token, user, login, logout, save, loginLoading }) {
const [draft, setDraft] = useState(urls); const [draft, setDraft] = useState(urls);
const [draftToken, setDraftToken] = useState(token); const [userCode, setUserCode] = useState("");
const [password, setPassword] = useState("");
const [loginError, setLoginError] = useState("");
const handleLogin = (e) => {
e.preventDefault();
setLoginError("");
login(userCode, password);
};
if (!token) {
return (
<section className="panel settings-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">RUNTIME CONFIGURATION</p>
<h2>Connections & Login</h2>
</div>
<span className="panel-count">stored locally</span>
</div>
<p className="settings-intro">
Point the dashboard at local Docker ports or deployed domains. Nothing
here is sent anywhere except the service you choose to call.
</p>
<div className="connection-grid">
{Object.entries(serviceMeta).map(([key, meta]) => (
<label key={key}>
{meta.label}
<input
value={draft[key]}
onChange={(event) =>
setDraft({ ...draft, [key]: event.target.value })
}
/>
</label>
))}
</div>
<div className="login-form">
<h3>Admin Login</h3>
<form onSubmit={handleLogin}>
<label>
User Code
<input
type="number"
value={userCode}
onChange={(e) => setUserCode(e.target.value)}
placeholder="1234"
required
/>
</label>
<label>
Password
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</label>
{loginError && <div className="login-error">{loginError}</div>}
<button
type="submit"
className="run-button save-button"
disabled={loginLoading}
>
{loginLoading ? "Logging in..." : "Login"}
</button>
</form>
</div>
<button
className="run-button save-button"
onClick={() => save(draft, "")}
>
Save connections
</button>
</section>
);
}
return ( return (
<section className="panel settings-panel"> <section className="panel settings-panel">
<div className="panel-heading"> <div className="panel-heading">
@@ -724,18 +910,24 @@ function Connections({ urls, token, save }) {
</label> </label>
))} ))}
</div> </div>
<label> <div className="user-info">
Admin JWT token <h3>Logged in as</h3>
<input <p>
type="password" <strong>User Code:</strong> {user?.userCode}
value={draftToken} </p>
onChange={(event) => setDraftToken(event.target.value)} <p>
placeholder="eyJ..." <strong>Role:</strong> {user?.job}
/> </p>
</label> <p>
<strong>Admin:</strong> {user?.isAdmin ? "Yes" : "No"}
</p>
<button className="ghost-button" onClick={logout}>
Logout
</button>
</div>
<button <button
className="run-button save-button" className="run-button save-button"
onClick={() => save(draft, draftToken)} onClick={() => save(draft, token)}
> >
Save connections Save connections
</button> </button>
+112
View File
@@ -603,6 +603,118 @@ textarea:focus {
min-width: 150px; min-width: 150px;
} }
} }
.login-form {
border-top: 1px solid var(--line);
padding-top: 20px;
margin-top: 10px;
}
.login-form h3 {
font-size: 14px;
margin: 0 0 14px;
}
.login-form label {
display: grid;
gap: 7px;
margin: 12px 0;
color: var(--muted);
font: 10px "DM Mono", monospace;
text-transform: uppercase;
}
.login-form input {
width: 100%;
}
.login-error {
color: var(--coral);
font-size: 11px;
margin: 10px 0;
}
.user-info {
border-top: 1px solid var(--line);
padding-top: 20px;
margin-top: 10px;
}
.user-info h3 {
font-size: 14px;
margin: 0 0 12px;
}
.user-info p {
margin: 6px 0;
font-size: 12px;
}
.user-info button {
margin-top: 14px;
}
.docs-panel {
min-height: 500px;
}
.docs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 16px;
}
.doc-card {
background: rgba(23, 32, 34, 0.88);
border: 1px solid var(--line);
border-radius: 6px;
padding: 20px;
border-top: 2px solid var(--card-color);
}
.doc-card.amber { --card-color: var(--amber); }
.doc-card.teal { --card-color: var(--mint); }
.doc-card.blue { --card-color: var(--blue); }
.doc-card.coral { --card-color: var(--coral); }
.doc-header {
display: flex;
gap: 12px;
align-items: start;
margin-bottom: 12px;
}
.doc-header .service-dot {
flex-shrink: 0;
margin-top: 2px;
}
.doc-header strong { display: block; font-size: 14px; }
.doc-header p { margin: 3px 0 0; color: var(--muted); font-size: 11px; }
.doc-description {
color: var(--muted);
font-size: 12px;
line-height: 1.5;
margin-bottom: 14px;
}
.doc-links {
display: flex;
flex-direction: column;
gap: 12px;
}
.doc-link {
color: var(--mint);
text-decoration: none;
font: 500 11px "DM Mono", monospace;
display: inline-flex;
align-items: center;
gap: 6px;
}
.doc-link:hover { text-decoration: underline; }
.routes-detail summary {
color: var(--muted);
font: 500 10px "DM Mono", monospace;
cursor: pointer;
text-transform: uppercase;
}
.routes-detail ul {
margin: 8px 0 0;
padding-left: 18px;
font-size: 11px;
color: #c6d5d3;
}
.routes-detail li { margin: 4px 0; }
.routes-detail code {
font: 11px "DM Mono", monospace;
background: #111a1c;
padding: 2px 4px;
border-radius: 2px;
}
@media (max-width: 700px) { @media (max-width: 700px) {
.app-shell { .app-shell {
padding: 0 18px 40px; padding: 0 18px 40px;
+52
View File
@@ -0,0 +1,52 @@
# Windows XP Driver UI
This project is a native Win32 application built for Windows XP compatibility.
It avoids .NET and WinForms completely and targets a classic Windows API implementation instead.
## Features
- Login form for driver authentication
- Calls the existing auth API on 192.168.68.66:5000
- Fetches the daily schedule from 192.168.68.66:5001
- Works with Windows XP and older native Windows systems
- Uses the WinHTTP API, which is available on XP
## Build
```bash
make
```
This uses the MinGW cross-compiler targeting 32-bit Windows.
## Runtime requirements
The app expects the following services to be available:
- auth_api on http://192.168.68.66:5000
- transit_api on http://192.168.68.66:5001
The app intentionally points at the live network host instead of localhost so it works on a Windows XP machine connected to the same LAN.
## Installer
A custom Windows installer is the distribution package for XP-era machines. The executable is bundled inside the installer; distribute `TransitDriverXPSetup.exe` rather than the internal `driver_xp.exe`.
```bash
makensis installer.nsi
```
This creates `TransitDriverXPSetup.exe`, which installs:
- `driver_xp.exe`
- `README.md`
- Start menu shortcut
- Desktop shortcut
- Uninstaller entry
When run on a machine with an existing Transit Driver XP installation, the installer
reuses the saved installation directory, stops the previous app if it is running,
and replaces the installed files with the new version. Existing shortcuts remain in
place and point to the updated app.
The installer is designed for test deployment on a real Windows XP or older Windows machine compatible with native Win32 executables.
+6 -32
View File
@@ -52,30 +52,6 @@ static void SetOutputText(HWND hwnd, const char *text)
SetWindowTextA(GetDlgItem(hwnd, ID_OUTPUT), text); SetWindowTextA(GetDlgItem(hwnd, ID_OUTPUT), text);
} }
static void LogDiagnostic(const char *stage, DWORD code)
{
char path[MAX_PATH];
char *separator;
FILE *file;
GetModuleFileNameA(NULL, path, sizeof(path));
separator = strrchr(path, '\\');
if (separator) {
*(separator + 1) = '\0';
} else {
snprintf(path, sizeof(path), ".\\");
}
strncat(path, "driver_xp.log", sizeof(path) - strlen(path) - 1);
file = fopen(path, "a");
if (!file) {
return;
}
fprintf(file, "%s: %lu\r\n", stage, (unsigned long)code);
fclose(file);
}
static void ShowControl(HWND hwnd, int id, int visible) static void ShowControl(HWND hwnd, int id, int visible)
{ {
ShowWindow(GetDlgItem(hwnd, id), visible ? SW_SHOW : SW_HIDE); ShowWindow(GetDlgItem(hwnd, id), visible ? SW_SHOW : SW_HIDE);
@@ -354,7 +330,7 @@ static void ExtractScheduleSummary(const char *json, APPSTATE *state, char *out,
ExtractJsonFieldString(routeText, "lineNumber", routeLine, sizeof(routeLine)); ExtractJsonFieldString(routeText, "lineNumber", routeLine, sizeof(routeLine));
ExtractJsonFieldString(routeText, "startTime", routeStartTime, sizeof(routeStartTime)); ExtractJsonFieldString(routeText, "startTime", routeStartTime, sizeof(routeStartTime));
ExtractJsonFieldString(routeText, "id", routeLineId, sizeof(routeLineId)); ExtractJsonFieldString(routeText, "id", routeLineId, sizeof(routeLineId));
{ if (routeLine[0] == '\0' || routeLineId[0] == '\0') {
char busLine[1024]; char busLine[1024];
const char *busKey = strstr(routeText, "\"busLine\""); const char *busKey = strstr(routeText, "\"busLine\"");
if (busKey) { if (busKey) {
@@ -540,7 +516,8 @@ static void ExtractTokenFromJson(const char *json, char *outToken, size_t outSiz
static void HttpErrorMessage(const char *context, DWORD error) static void HttpErrorMessage(const char *context, DWORD error)
{ {
LogDiagnostic(context, error); (void)context;
(void)error;
} }
static int HttpRequestText(const char *method, static int HttpRequestText(const char *method,
@@ -561,7 +538,6 @@ static int HttpRequestText(const char *method,
const char *src; const char *src;
const char *slash; const char *slash;
const char *colon; const char *colon;
int secure = 0;
int port = 80; int port = 80;
DWORD flags = 0; DWORD flags = 0;
DWORD status = 0; DWORD status = 0;
@@ -578,7 +554,6 @@ static int HttpRequestText(const char *method,
src += 7; src += 7;
} else if (strncmp(src, "https://", 8) == 0) { } else if (strncmp(src, "https://", 8) == 0) {
src += 8; src += 8;
secure = 1;
} }
host[0] = '\0'; host[0] = '\0';
@@ -621,7 +596,7 @@ static int HttpRequestText(const char *method,
} }
if (host[0] == '\0') { if (host[0] == '\0') {
snprintf(host, sizeof(host), "%s", API_HOST); snprintf(host, sizeof(host), "192.168.68.66");
} }
if (port == 0) { if (port == 0) {
@@ -658,7 +633,7 @@ static int HttpRequestText(const char *method,
NULL, NULL,
WINHTTP_NO_REFERER, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_DEFAULT_ACCEPT_TYPES,
secure ? WINHTTP_FLAG_SECURE : 0); 0);
if (!hRequest) { if (!hRequest) {
HttpErrorMessage("Unable to open request", GetLastError()); HttpErrorMessage("Unable to open request", GetLastError());
goto cleanup; goto cleanup;
@@ -713,7 +688,6 @@ static int HttpRequestText(const char *method,
} }
if (status >= 400) { if (status >= 400) {
LogDiagnostic("HTTP response status", status);
snprintf(response, responseSize, "HTTP error %lu", status); snprintf(response, responseSize, "HTTP error %lu", status);
ok = 0; ok = 0;
goto cleanup; goto cleanup;
@@ -810,7 +784,7 @@ static void PerformRefresh(HWND hwnd, APPSTATE *state)
if (date[0] == '\0') { if (date[0] == '\0') {
snprintf(date, sizeof(date), "2026-09-17"); snprintf(date, sizeof(date), "2026-09-17");
} }
snprintf(scheduleUrl, sizeof(scheduleUrl), "http://192.168.68.66:5001/driver/schedules/daily?date=%s", date); snprintf(scheduleUrl, sizeof(scheduleUrl), "http://192.168.68.66:5001/schedule/daily?date=%s", date);
SetStatusText(hwnd, "Loading schedule..."); SetStatusText(hwnd, "Loading schedule...");
if (!HttpRequestText("GET", scheduleUrl, NULL, authHeader, response, sizeof(response))) { if (!HttpRequestText("GET", scheduleUrl, NULL, authHeader, response, sizeof(response))) {
+26 -7
View File
@@ -1,29 +1,39 @@
; Transit Driver XP custom installer
!include "MUI2.nsh" !include "MUI2.nsh"
!include "WinVer.nsh"
!define APPNAME "Transit Driver XP" !define APPNAME "Transit Driver XP"
!define COMPANYNAME "Transit Project" !define COMPANYNAME "Transit Project"
!define VERSION "2.0.0" !define DESCRIPTION "Native Windows XP-compatible driver app"
!define VERSIONMAJOR 1
!define VERSIONMINOR 1
!define VERSIONBUILD 0
Name "${APPNAME}" Name "${APPNAME}"
OutFile "TransitDriverXPSetup.exe" OutFile "TransitDriverXPSetup.exe"
InstallDir "$PROGRAMFILES\TransitDriverXP" InstallDir "$PROGRAMFILES\TransitDriverXP"
InstallDirRegKey HKCU "Software\TransitDriverXP" "InstallDir" InstallDirRegKey HKCU "Software\TransitDriverXP" "InstallDir"
RequestExecutionLevel admin RequestExecutionLevel admin
VIProductVersion "2.0.0.0"
VIProductVersion "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}.0"
VIAddVersionKey "ProductName" "${APPNAME}" VIAddVersionKey "ProductName" "${APPNAME}"
VIAddVersionKey "CompanyName" "${COMPANYNAME}" VIAddVersionKey "CompanyName" "${COMPANYNAME}"
VIAddVersionKey "FileDescription" "Native Windows XP driver client"
VIAddVersionKey "FileVersion" "${VERSION}"
VIAddVersionKey "ProductVersion" "${VERSION}"
VIAddVersionKey "LegalCopyright" "Transit Project" VIAddVersionKey "LegalCopyright" "Transit Project"
VIAddVersionKey "FileDescription" "${DESCRIPTION}"
VIAddVersionKey "FileVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
VIAddVersionKey "ProductVersion" "${VERSIONMAJOR}.${VERSIONMINOR}.${VERSIONBUILD}"
Function .onInit Function .onInit
ReadRegStr $0 HKCU "Software\TransitDriverXP" "InstallDir" ReadRegStr $0 HKCU "Software\TransitDriverXP" "InstallDir"
StrCmp $0 "" done StrCmp $0 "" done
IfFileExists "$0\driver_xp.exe" 0 done IfFileExists "$0\driver_xp.exe" 0 done
StrCpy $INSTDIR $0 StrCpy $INSTDIR $0
MessageBox MB_OK|MB_ICONINFORMATION "An existing Transit Driver XP installation was found. It will be updated." MessageBox MB_OK|MB_ICONINFORMATION "An existing Transit Driver XP installation was found. It will be updated in $INSTDIR."
; Release the old executable before NSIS replaces it.
ExecWait '"$SYSDIR\taskkill.exe" /F /IM driver_xp.exe' ExecWait '"$SYSDIR\taskkill.exe" /F /IM driver_xp.exe'
done: done:
FunctionEnd FunctionEnd
@@ -35,14 +45,20 @@ Section "Install"
SetOutPath "$INSTDIR" SetOutPath "$INSTDIR"
SetOverwrite on SetOverwrite on
File "driver_xp.exe" File "driver_xp.exe"
File "README.md"
WriteRegStr HKCU "Software\TransitDriverXP" "InstallDir" "$INSTDIR" WriteRegStr HKCU "Software\TransitDriverXP" "InstallDir" "$INSTDIR"
WriteRegStr HKCU "Software\TransitDriverXP" "DisplayName" "${APPNAME}" WriteRegStr HKCU "Software\TransitDriverXP" "DisplayName" "${APPNAME}"
WriteRegStr HKCU "Software\TransitDriverXP" "Version" "${VERSION}"
CreateDirectory "$SMPROGRAMS\TransitDriverXP" CreateDirectory "$SMPROGRAMS\TransitDriverXP"
CreateShortCut "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe" CreateShortCut "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe"
CreateShortCut "$DESKTOP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe" CreateShortCut "$DESKTOP\Transit Driver XP.lnk" "$INSTDIR\driver_xp.exe"
WriteUninstaller "$INSTDIR\Uninstall.exe" WriteUninstaller "$INSTDIR\Uninstall.exe"
CreateShortCut "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk" "$INSTDIR\Uninstall.exe" CreateShortCut "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk" "$INSTDIR\Uninstall.exe"
; launch after install
ExecShell "open" "$INSTDIR\driver_xp.exe"
SectionEnd SectionEnd
Section "Uninstall" Section "Uninstall"
@@ -50,8 +66,11 @@ Section "Uninstall"
Delete "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk" Delete "$SMPROGRAMS\TransitDriverXP\Transit Driver XP.lnk"
Delete "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk" Delete "$SMPROGRAMS\TransitDriverXP\Uninstall Transit Driver XP.lnk"
RMDir "$SMPROGRAMS\TransitDriverXP" RMDir "$SMPROGRAMS\TransitDriverXP"
Delete "$INSTDIR\driver_xp.exe" Delete "$INSTDIR\driver_xp.exe"
Delete "$INSTDIR\README.md"
Delete "$INSTDIR\Uninstall.exe" Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR" RMDir "$INSTDIR"
DeleteRegKey HKCU "Software\TransitDriverXP" DeleteRegKey HKCU "Software\TransitDriverXP"
SectionEnd SectionEnd