diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 1c32e5d..08bdad4 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -307,7 +307,7 @@ jobs: 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') }} + if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && gitea.event_name == 'push' && (gitea.ref_name == 'dev' || gitea.ref_name == 'main' || gitea.ref_name == 'develop' || gitea.ref_name == 'auth-api' || gitea.ref_name == 'employee-api' || gitea.ref_name == 'transit-api' || gitea.ref_name == 'error-log-api' || gitea.ref_name == 'admin-ui' || gitea.ref_name == 'monitoring-ui') }} runs-on: ubuntu-latest steps: diff --git a/monitoring_ui/public/docs/auth_api/README.md b/monitoring_ui/public/docs/auth_api/README.md new file mode 100644 index 0000000..5607f01 --- /dev/null +++ b/monitoring_ui/public/docs/auth_api/README.md @@ -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 ` +- 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). \ No newline at end of file diff --git a/monitoring_ui/public/docs/employee_api/README.md b/monitoring_ui/public/docs/employee_api/README.md new file mode 100644 index 0000000..f49f3d4 --- /dev/null +++ b/monitoring_ui/public/docs/employee_api/README.md @@ -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) \ No newline at end of file diff --git a/monitoring_ui/public/docs/error_log_api/README.md b/monitoring_ui/public/docs/error_log_api/README.md new file mode 100644 index 0000000..0184795 --- /dev/null +++ b/monitoring_ui/public/docs/error_log_api/README.md @@ -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 ` +- 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. \ No newline at end of file diff --git a/monitoring_ui/public/docs/transit_api/README.md b/monitoring_ui/public/docs/transit_api/README.md new file mode 100644 index 0000000..8c3b75e --- /dev/null +++ b/monitoring_ui/public/docs/transit_api/README.md @@ -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 diff --git a/monitoring_ui/src/App.jsx b/monitoring_ui/src/App.jsx index eafb72a..da7118b 100644 --- a/monitoring_ui/src/App.jsx +++ b/monitoring_ui/src/App.jsx @@ -4,26 +4,10 @@ const runtime = typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {}; const configured = { - auth: - runtime.AUTH_API || - import.meta.env.VITE_AUTH_API || - "http://localhost:5000", - 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", + auth: runtime.AUTH_API || import.meta.env.VITE_AUTH_API || "", + employee: runtime.EMPLOYEE_API || import.meta.env.VITE_EMPLOYEE_API || "", + transit: runtime.TRANSIT_API || import.meta.env.VITE_TRANSIT_API || "", + errors: runtime.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API || "", }; const serviceMeta = { @@ -32,7 +16,8 @@ const serviceMeta = { role: "Identity & tokens", color: "amber", data: "Login, JWT sessions, current user profile", - routes: ["/login", "/me"], + routes: ["/login", "/me", "/health"], + docs: "/docs/auth_api/README.md", }, employee: { label: "Employee API", @@ -40,6 +25,7 @@ const serviceMeta = { color: "teal", data: "Employee onboarding and employee records", routes: ["/health", "/createemployee"], + docs: "/docs/employee_api/README.md", }, transit: { label: "Transit API", @@ -52,6 +38,7 @@ const serviceMeta = { "/schedule/daily?date=YYYY-MM-DD", "/admin/stops", ], + docs: "/docs/transit_api/README.md", }, errors: { label: "Error Log API", @@ -59,13 +46,7 @@ const serviceMeta = { color: "coral", data: "Centralized bad requests and service errors", routes: ["/health", "/error_log?limit=100"], - }, - admin: { - label: "Admin UI", - role: "Operator surface", - color: "violet", - data: "The deployed React administration console", - routes: ["/health"], + docs: "/docs/error_log_api/README.md", }, }; @@ -74,9 +55,9 @@ const healthPaths = { employee: "/health", transit: "/health", errors: "/health", - admin: "/health", }; const testerRoutes = [ + { service: "auth", method: "POST", path: "/login", label: "Login (userCode + password)" }, { service: "auth", method: "GET", path: "/me", label: "Current session" }, { service: "employee", @@ -127,6 +108,9 @@ function App() { const [token, setToken] = useState( () => localStorage.getItem("transit-monitor-token") || "", ); + const [user, setUser] = useState( + () => JSON.parse(localStorage.getItem("transit-monitor-user") || "null"), + ); const [health, setHealth] = useState({}); const [history, setHistory] = useState({}); const [errors, setErrors] = useState([]); @@ -142,13 +126,62 @@ function App() { }); const [testerResult, setTesterResult] = useState(null); const [busy, setBusy] = useState(false); + const [loginLoading, setLoginLoading] = useState(false); + const [loginError, setLoginError] = useState(""); + + const login = useCallback(async (userCode, password) => { + const url = urls.auth; + if (!url) { + setLoginError("Auth API URL not configured"); + return; + } + setLoginLoading(true); + setLoginError(""); + try { + const response = await fetch(`${url}/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userCode, password }), + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || `HTTP ${response.status}`); + } + setToken(data.token); + setUser(data.user); + localStorage.setItem("transit-monitor-token", data.token); + localStorage.setItem("transit-monitor-user", JSON.stringify(data.user)); + setLoginError(""); + } catch (error) { + setLoginError(error.message); + } finally { + setLoginLoading(false); + } + }, [urls.auth]); + + const logout = useCallback(() => { + setToken(""); + setUser(null); + localStorage.removeItem("transit-monitor-token"); + localStorage.removeItem("transit-monitor-user"); + }, []); const checkService = useCallback( async (key) => { 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}` } : {}; try { - const response = await fetch(`${urls[key]}${healthPaths[key]}`, { + const response = await fetch(`${url}${healthPaths[key]}`, { headers, }); const body = await response.json().catch(() => null); @@ -346,6 +379,12 @@ function App() { > Service catalog + + + + + + ); + } + return (
@@ -724,18 +896,16 @@ function Connections({ urls, token, save }) { ))}
- +
+

Logged in as

+

User Code: {user?.userCode}

+

Role: {user?.job}

+

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

+ +
diff --git a/monitoring_ui/src/styles.css b/monitoring_ui/src/styles.css index dc8e1d1..cd26cfb 100644 --- a/monitoring_ui/src/styles.css +++ b/monitoring_ui/src/styles.css @@ -603,6 +603,118 @@ textarea:focus { min-width: 150px; } } +.login-form { + border-top: 1px solid var(--line); + padding-top: 20px; + margin-top: 10px; +} +.login-form h3 { + font-size: 14px; + margin: 0 0 14px; +} +.login-form label { + display: grid; + gap: 7px; + margin: 12px 0; + color: var(--muted); + font: 10px "DM Mono", monospace; + text-transform: uppercase; +} +.login-form input { + width: 100%; +} +.login-error { + color: var(--coral); + font-size: 11px; + margin: 10px 0; +} +.user-info { + border-top: 1px solid var(--line); + padding-top: 20px; + margin-top: 10px; +} +.user-info h3 { + font-size: 14px; + margin: 0 0 12px; +} +.user-info p { + margin: 6px 0; + font-size: 12px; +} +.user-info button { + margin-top: 14px; +} +.docs-panel { + min-height: 500px; +} +.docs-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 16px; +} +.doc-card { + background: rgba(23, 32, 34, 0.88); + border: 1px solid var(--line); + border-radius: 6px; + padding: 20px; + border-top: 2px solid var(--card-color); +} +.doc-card.amber { --card-color: var(--amber); } +.doc-card.teal { --card-color: var(--mint); } +.doc-card.blue { --card-color: var(--blue); } +.doc-card.coral { --card-color: var(--coral); } +.doc-header { + display: flex; + gap: 12px; + align-items: start; + margin-bottom: 12px; +} +.doc-header .service-dot { + flex-shrink: 0; + margin-top: 2px; +} +.doc-header strong { display: block; font-size: 14px; } +.doc-header p { margin: 3px 0 0; color: var(--muted); font-size: 11px; } +.doc-description { + color: var(--muted); + font-size: 12px; + line-height: 1.5; + margin-bottom: 14px; +} +.doc-links { + display: flex; + flex-direction: column; + gap: 12px; +} +.doc-link { + color: var(--mint); + text-decoration: none; + font: 500 11px "DM Mono", monospace; + display: inline-flex; + align-items: center; + gap: 6px; +} +.doc-link:hover { text-decoration: underline; } +.routes-detail summary { + color: var(--muted); + font: 500 10px "DM Mono", monospace; + cursor: pointer; + text-transform: uppercase; +} +.routes-detail ul { + margin: 8px 0 0; + padding-left: 18px; + font-size: 11px; + color: #c6d5d3; +} +.routes-detail li { margin: 4px 0; } +.routes-detail code { + font: 11px "DM Mono", monospace; + background: #111a1c; + padding: 2px 4px; + border-radius: 2px; +} + @media (max-width: 700px) { .app-shell { padding: 0 18px 40px;