Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89e4f90fc3 | |||
| 39b0b07a77 | |||
| 48c710bfbf | |||
| 1c055d84e8 | |||
| d7100e6f2a | |||
| 6e0b7e4793 | |||
| da5c6b2839 | |||
| 38782bca6d | |||
| ebcc485f51 | |||
| c09b3f3f66 |
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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))) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user