4 Commits

Author SHA1 Message Date
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
7 changed files with 844 additions and 45 deletions
+1 -1
View File
@@ -307,7 +307,7 @@ jobs:
build-admin-ui, build-admin-ui,
build-monitoring-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 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
+214 -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,9 @@ 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 +108,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 +126,62 @@ 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 +379,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 +443,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 +458,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 +538,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 +552,55 @@ 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 +792,84 @@ 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 +896,16 @@ function Connections({ urls, token, save }) {
</label> </label>
))} ))}
</div> </div>
<label> <div className="user-info">
Admin JWT token <h3>Logged in as</h3>
<input <p><strong>User Code:</strong> {user?.userCode}</p>
type="password" <p><strong>Role:</strong> {user?.job}</p>
value={draftToken} <p><strong>Admin:</strong> {user?.isAdmin ? "Yes" : "No"}</p>
onChange={(event) => setDraftToken(event.target.value)} <button className="ghost-button" onClick={logout}>Logout</button>
placeholder="eyJ..." </div>
/>
</label>
<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;