chore: standardize service domains and error logging
Build and Push Docker Images / detect-changes (push) Successful in 15s
Build and Push Docker Images / build-auth-api (push) Successful in 1m2s
Build and Push Docker Images / build-employee-api (push) Successful in 1m2s
Build and Push Docker Images / build-transit-api (push) Successful in 1m2s
Build and Push Docker Images / build-error-log-api (push) Successful in 1m10s
Build and Push Docker Images / build-admin-ui (push) Successful in 1m22s
Build and Push Docker Images / deploy-test-node (push) Successful in 4s

This commit is contained in:
Sven laptop
2026-09-18 19:26:05 +02:00
parent 3f0a02aa04
commit 1154d1d658
21 changed files with 609 additions and 380 deletions
+21 -7
View File
@@ -3,12 +3,14 @@
## 🚀 5-Minute Setup
### Step 1: Initial Setup
```bash
# From the project root
./scripts/init.sh
```
This will:
- Check Docker and Docker Compose installation
- Create `.env` file from template
- Build all Docker images
@@ -17,6 +19,7 @@ This will:
### Step 2: Configure Database Connection
Edit `.env` with your database details:
```env
POSTGRES_HOST=your-db-host.com
POSTGRES_USER_AUTH=transitAUTH
@@ -28,11 +31,13 @@ JWT_SECRET=your-secure-secret
```
### Step 3: Start Services
```bash
./scripts/deploy.sh start
```
### Step 4: Access Admin UI
```
http://localhost:5174
```
@@ -42,29 +47,32 @@ Log in with your real user code and password.
## 📱 Admin UI Features
### Stops Management
- Create new transit stops
- Set geolocation (latitude, longitude)
- View all stops
- Link stops to bus lines
### Bus Lines
- Create new bus routes
- Define start and end stops
- Set estimated travel time
- Assign stops to lines with order
### Fleet
- View fleet vehicles (read-only)
- See vehicle details
## 🐳 Docker Services
| Port | Service | URL |
|------|---------|-----|
| 5174 | Admin UI | http://localhost:5174 |
| 5000 | Auth API | http://localhost:5000 |
| 5001 | Transit API | http://localhost:5001 |
| 4000 | Employee API | http://localhost:4000 |
| Port | Service | URL |
| ---- | ------------ | ---------------------------------------- |
| 5174 | Admin UI | http://localhost:5174 |
| 5000 | Auth API | https://auth.xtransit.testinstance.nl |
| 5001 | Transit API | https://transit.xtransit.testinstance.nl |
| 4000 | Employee API | http://localhost:4000 |
## 🔧 Common Commands
@@ -91,6 +99,7 @@ Log in with your real user code and password.
Use the real login flow with user code and password.
### Production
- Keep `DEV_BYPASS=0` and `DEV_MOCK=0`
- Configure proper admin user codes in `ADMIN_USER_CODES`
- Ensure JWTs include the `userCode` claim
@@ -105,6 +114,7 @@ Use the real login flow with user code and password.
## ⚠️ Troubleshooting
### "Address already in use"
```bash
# Find and stop the process using the port
lsof -i :5174 # Find process on port 5174
@@ -112,6 +122,7 @@ kill -9 <PID> # Kill the process
```
### "Cannot connect to database"
```bash
# Check database credentials in .env
# Verify database is accessible from your machine
@@ -119,6 +130,7 @@ kill -9 <PID> # Kill the process
```
### "Admin UI not loading"
```bash
# Check container is running
docker-compose ps admin_ui
@@ -131,7 +143,9 @@ docker-compose restart admin_ui
```
### "Permission denied" errors
The transit API user needs proper database permissions:
```sql
-- Grant permissions to transitAPI user
GRANT CONNECT ON DATABASE "XTransit" TO transitAPI;
@@ -142,7 +156,7 @@ GRANT SELECT, INSERT, UPDATE ON all tables IN SCHEMA public TO transitAPI;
## 📚 For More Information
- **DEPLOYMENT.md** - Full deployment guide for production
- **DOCKER_README.md** - Docker and container information
- **DOCKER_README.md** - Docker and container information
- **admin_ui/README.md** - Admin UI specific details
## 🎯 Next Steps
+7 -4
View File
@@ -6,6 +6,9 @@ Dit repository bevat een suite van Node.js microservices, een admin webapp en ee
- `employee_api` — Employee onboarding / management (standaard port 4000)
- `transit_api` — Transit/schedule endpoints (standaard port 5001)
- `error_log_api` — Centrale foutlogging en viewer (standaard port 5003)
De error API is beschikbaar via `https://error.xtransit.testinstance.nl`.
- `admin_ui` — React + Vite beheerinterface
- `deprecated/` — legacy Flutter bus terminal, retained for reference while moving the client to a Windows XP-era build path
@@ -88,7 +91,7 @@ POSTGRES_PASSWORD_ERROR_LOG=strong_password_error_log
JWT_SECRET=super_secret_jwt_key_here
# Optioneel: centraal error log endpoint voor clients/services
ERROR_LOG_API_URL=http://error_log_api:5003/error_log
ERROR_LOG_API_URL=https://error.xtransit.testinstance.nl/error_log
```
Opmerking: de exacte variabelen worden gelezen door `docker-compose.yml`. Pas de namen/waarden aan naar jouw omgeving.
@@ -243,8 +246,8 @@ CREATE TABLE IF NOT EXISTS data.errors (
id BIGSERIAL PRIMARY KEY,
"date" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
"service" TEXT NOT NULL,
"partOfService" TEXT,
"error" TEXT NOT NULL
partofservice TEXT,
error TEXT NOT NULL
);
```
@@ -356,7 +359,7 @@ The Transit API exposes public (unauthenticated) schedule endpoints:
**Example:**
```bash
curl http://localhost:5001/schedules?date=2026-05-06
curl https://transit.xtransit.testinstance.nl/schedules?date=2026-05-06
```
**TimetableViewer Component:**
+7 -7
View File
@@ -46,9 +46,9 @@ npm install
Create a `.env` file in the `admin_ui` directory:
```env
VITE_AUTH_API=http://localhost:5000
VITE_TRANSIT_API=http://localhost:5001
VITE_ERROR_LOG_API=http://localhost:5003
VITE_AUTH_API=https://auth.xtransit.testinstance.nl
VITE_TRANSIT_API=https://transit.xtransit.testinstance.nl
VITE_ERROR_LOG_API=https://error.xtransit.testinstance.nl
```
### Run Development Server
@@ -102,10 +102,10 @@ http://localhost:5174
### Environment Variables
| Variable | Description | Default |
| ------------------ | -------------------------------------------------------- | --------------------- |
| `VITE_AUTH_API` | Auth API URL (same-origin path recommended in Docker) | http://localhost:5000 |
| `VITE_TRANSIT_API` | Transit API URL (same-origin path recommended in Docker) | http://localhost:5001 |
| Variable | Description | Default |
| ------------------ | --------------- | ---------------------------------------- |
| `VITE_AUTH_API` | Auth API URL | https://auth.xtransit.testinstance.nl |
| `VITE_TRANSIT_API` | Transit API URL | https://transit.xtransit.testinstance.nl |
### Runtime Configuration
+3 -3
View File
@@ -246,7 +246,7 @@ The schedule manager uses:
### Create a schedule (Admin)
```bash
curl -X POST http://localhost:5001/admin/schedules \
curl -X POST https://transit.xtransit.testinstance.nl/admin/schedules \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
@@ -261,13 +261,13 @@ curl -X POST http://localhost:5001/admin/schedules \
### Get today's schedules (Public)
```bash
curl http://localhost:5001/schedules?date=$(date +%Y-%m-%d)
curl https://transit.xtransit.testinstance.nl/schedules?date=$(date +%Y-%m-%d)
```
### Get schedules for specific line (Public)
```bash
curl http://localhost:5001/schedules?lineId=3
curl https://transit.xtransit.testinstance.nl/schedules?lineId=3
```
## Future Enhancements
+3 -3
View File
@@ -1,9 +1,9 @@
version: '3.8'
version: "3.8"
services:
admin-ui:
build: .
ports:
- "5174:80"
environment:
VITE_AUTH_API: ${VITE_AUTH_API:-http://localhost:5000}
VITE_TRANSIT_API: ${VITE_TRANSIT_API:-http://localhost:5001}
VITE_AUTH_API: ${VITE_AUTH_API:-https://auth.xtransit.testinstance.nl}
VITE_TRANSIT_API: ${VITE_TRANSIT_API:-https://transit.xtransit.testinstance.nl}
+3 -3
View File
@@ -3,9 +3,9 @@ set -e
cat > /usr/share/nginx/html/env-config.js <<EOF
window.__ENV__ = {
AUTH_API: "${AUTH_API:-http://localhost:5000}",
TRANSIT_API: "${TRANSIT_API:-http://localhost:5001}",
ERROR_LOG_API: "${ERROR_LOG_API:-http://localhost:5003}"
AUTH_API: "${AUTH_API:-https://auth.xtransit.testinstance.nl}",
TRANSIT_API: "${TRANSIT_API:-https://transit.xtransit.testinstance.nl}",
ERROR_LOG_API: "${ERROR_LOG_API:-https://error.xtransit.testinstance.nl}"
}
EOF
+69 -50
View File
@@ -1,87 +1,106 @@
import axios from 'axios'
import axios from "axios";
const RUNTIME = typeof window !== 'undefined' && window.__ENV__ ? window.__ENV__ : {}
const RUNTIME =
typeof window !== "undefined" && window.__ENV__ ? window.__ENV__ : {};
const normalizeApiBaseUrl = (value, fallbackPath) => {
const rawValue = value || ''
if (!rawValue) return fallbackPath
if (/^https?:\/\//i.test(rawValue) || rawValue.startsWith('//') || rawValue.startsWith('/')) {
return rawValue
const rawValue = value || "";
if (!rawValue) return fallbackPath;
if (
/^https?:\/\//i.test(rawValue) ||
rawValue.startsWith("//") ||
rawValue.startsWith("/")
) {
return rawValue;
}
return `${window.location.protocol}//${rawValue}`
}
return `${window.location.protocol}//${rawValue}`;
};
const AUTH_API = normalizeApiBaseUrl(RUNTIME.AUTH_API || import.meta.env.VITE_AUTH_API, 'http://localhost:5000')
const TRANSIT_API = normalizeApiBaseUrl(RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API, 'http://localhost:5001')
const ERROR_LOG_API = normalizeApiBaseUrl(RUNTIME.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API, 'http://localhost:5003')
const AUTH_API = normalizeApiBaseUrl(
RUNTIME.AUTH_API || import.meta.env.VITE_AUTH_API,
"https://auth.xtransit.testinstance.nl",
);
const TRANSIT_API = normalizeApiBaseUrl(
RUNTIME.TRANSIT_API || import.meta.env.VITE_TRANSIT_API,
"https://transit.xtransit.testinstance.nl",
);
const ERROR_LOG_API = normalizeApiBaseUrl(
RUNTIME.ERROR_LOG_API || import.meta.env.VITE_ERROR_LOG_API,
"https://error.xtransit.testinstance.nl",
);
const reportClientError = async ({ service, partOfService, error }) => {
const reportClientError = async ({ service, partofservice, error }) => {
try {
await fetch(`${ERROR_LOG_API}/error_log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
service,
partOfService,
error: typeof error === 'string' ? error : JSON.stringify(error),
partofservice,
error: typeof error === "string" ? error : JSON.stringify(error),
}),
})
});
} catch (e) {
// ignore logging failures
}
}
};
export const authClient = axios.create({ baseURL: AUTH_API })
export const authClient = axios.create({ baseURL: AUTH_API });
authClient.interceptors.response.use(
response => response,
error => {
(response) => response,
(error) => {
void reportClientError({
service: 'admin_ui',
partOfService: 'authClient',
error: error?.response?.data?.error || error?.message || 'authClient_error',
})
return Promise.reject(error)
}
)
service: "admin_ui",
partofservice: "authClient",
error:
error?.response?.data?.error || error?.message || "authClient_error",
});
return Promise.reject(error);
},
);
export const transitClient = (token) => {
const client = axios.create({
baseURL: TRANSIT_API,
headers: { Authorization: `Bearer ${token}` }
})
headers: { Authorization: `Bearer ${token}` },
});
// Auto-logout on unauthorized responses to avoid delayed failures
client.interceptors.response.use(
response => response,
error => {
(response) => response,
(error) => {
try {
const status = error?.response?.status
const status = error?.response?.status;
if (status === 401 || status === 403) {
localStorage.removeItem('token')
localStorage.removeItem('user')
localStorage.removeItem("token");
localStorage.removeItem("user");
// reload to let App show the Login screen
if (typeof window !== 'undefined') window.location.reload()
if (typeof window !== "undefined") window.location.reload();
}
void reportClientError({
service: 'admin_ui',
partOfService: 'transitClient',
error: error?.response?.data?.error || error?.message || 'transitClient_error',
})
service: "admin_ui",
partofservice: "transitClient",
error:
error?.response?.data?.error ||
error?.message ||
"transitClient_error",
});
} catch (e) {
// ignore
}
return Promise.reject(error)
}
)
return Promise.reject(error);
},
);
return client
}
return client;
};
export const errorLogClient = (token) => axios.create({
baseURL: ERROR_LOG_API,
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
export const errorLogClient = (token) =>
axios.create({
baseURL: ERROR_LOG_API,
headers: token ? { Authorization: `Bearer ${token}` } : {},
});
export { reportClientError }
export { reportClientError };
+45 -31
View File
@@ -1,42 +1,44 @@
import React, { useEffect, useMemo, useState } from 'react'
import { errorLogClient } from '../api'
import React, { useEffect, useMemo, useState } from "react";
import { errorLogClient } from "../api";
const formatDate = (value) => {
if (!value) return '—'
if (!value) return "—";
try {
return new Date(value).toLocaleString()
return new Date(value).toLocaleString();
} catch (_) {
return String(value)
return String(value);
}
}
};
export default function ErrorLogViewer({ token }) {
const client = useMemo(() => errorLogClient(token), [token])
const [logs, setLogs] = useState([])
const [service, setService] = useState('')
const [partOfService, setPartOfService] = useState('')
const [loading, setLoading] = useState(false)
const [message, setMessage] = useState(null)
const client = useMemo(() => errorLogClient(token), [token]);
const [logs, setLogs] = useState([]);
const [service, setService] = useState("");
const [partofservice, setPartofservice] = useState("");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState(null);
const loadLogs = async () => {
setLoading(true)
setMessage(null)
setLoading(true);
setMessage(null);
try {
const params = { limit: 100 }
if (service) params.service = service
if (partOfService) params.partOfService = partOfService
const res = await client.get('/error_log', { params })
setLogs(res.data.errors || [])
const params = { limit: 100 };
if (service) params.service = service;
if (partofservice) params.partofservice = partofservice;
const res = await client.get("/error_log", { params });
setLogs(res.data.errors || []);
} catch (err) {
setMessage(err.response?.data?.error || err.message || 'Kon fouten niet laden')
setMessage(
err.response?.data?.error || err.message || "Kon fouten niet laden",
);
} finally {
setLoading(false)
setLoading(false);
}
}
};
useEffect(() => {
loadLogs()
}, [service, partOfService])
loadLogs();
}, [service, partofservice]);
return (
<section className="card">
@@ -46,29 +48,41 @@ export default function ErrorLogViewer({ token }) {
<p>Laatste foutmeldingen uit de centrale logging service.</p>
</div>
<button className="secondary" onClick={loadLogs} disabled={loading}>
{loading ? 'Laden...' : 'Ververs'}
{loading ? "Laden..." : "Ververs"}
</button>
</div>
<div className="form-row">
<input placeholder="Service filter" value={service} onChange={e => setService(e.target.value)} />
<input placeholder="Part of service filter" value={partOfService} onChange={e => setPartOfService(e.target.value)} />
<input
placeholder="Service filter"
value={service}
onChange={(e) => setService(e.target.value)}
/>
<input
placeholder="Part of service filter"
value={partofservice}
onChange={(e) => setPartofservice(e.target.value)}
/>
</div>
{message && <div className="message">{message}</div>}
<div className="list-grid">
{logs.map(log => (
{logs.map((log) => (
<article className="mini-card" key={log.id}>
<strong>{log.service}</strong>
<div>{formatDate(log.date)}</div>
<div className="muted" style={{ marginTop: 6 }}>{log.partOfService || '—'}</div>
<div className="muted" style={{ marginTop: 6 }}>
{log.partofservice || "—"}
</div>
<div style={{ marginTop: 8 }}>{log.error}</div>
</article>
))}
</div>
{!logs.length && !message && <div className="muted">Nog geen foutmeldingen gevonden.</div>}
{!logs.length && !message && (
<div className="muted">Nog geen foutmeldingen gevonden.</div>
)}
</section>
)
);
}
+188 -85
View File
@@ -1,72 +1,85 @@
import React, { useEffect, useState } from 'react'
import React, { useEffect, useState } from "react";
export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }) {
const [schedules, setSchedules] = useState([])
const [summary, setSummary] = useState(null)
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0])
const [selectedLineId, setSelectedLineId] = useState('')
const [lines, setLines] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
export default function TimetableViewer({
apiBaseUrl = "https://transit.xtransit.testinstance.nl",
}) {
const [schedules, setSchedules] = useState([]);
const [summary, setSummary] = useState(null);
const [selectedDate, setSelectedDate] = useState(
new Date().toISOString().split("T")[0],
);
const [selectedLineId, setSelectedLineId] = useState("");
const [lines, setLines] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Fetch available bus lines
useEffect(() => {
const fetchLines = async () => {
try {
const res = await fetch(`${apiBaseUrl}/admin/bus-lines`)
const data = await res.json()
setLines(data.busLines || [])
const res = await fetch(`${apiBaseUrl}/admin/bus-lines`);
const data = await res.json();
setLines(data.busLines || []);
} catch (err) {
console.error('Failed to load lines:', err)
console.error("Failed to load lines:", err);
}
}
fetchLines()
}, [apiBaseUrl])
};
fetchLines();
}, [apiBaseUrl]);
// Fetch schedules based on date and line filter
useEffect(() => {
const fetchSchedules = async () => {
setLoading(true)
setError(null)
setLoading(true);
setError(null);
try {
let url = `${apiBaseUrl}/schedules/overview?date=${selectedDate}`
let url = `${apiBaseUrl}/schedules/overview?date=${selectedDate}`;
if (selectedLineId) {
url += `&lineId=${selectedLineId}`
url += `&lineId=${selectedLineId}`;
}
const res = await fetch(url)
const data = await res.json()
setSchedules(data.schedules || [])
setSummary(data.summary || null)
const res = await fetch(url);
const data = await res.json();
setSchedules(data.schedules || []);
setSummary(data.summary || null);
} catch (err) {
setError('Failed to load schedules: ' + err.message)
setError("Failed to load schedules: " + err.message);
} finally {
setLoading(false)
setLoading(false);
}
}
fetchSchedules()
}, [selectedDate, selectedLineId, apiBaseUrl])
};
fetchSchedules();
}, [selectedDate, selectedLineId, apiBaseUrl]);
const timeSlots = generateTimeSlots()
const schedulesBySlot = groupSchedulesByTimeSlot(schedules, timeSlots)
const timeSlots = generateTimeSlots();
const schedulesBySlot = groupSchedulesByTimeSlot(schedules, timeSlots);
return (
<div style={{ padding: '20px', maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ padding: "20px", maxWidth: "1200px", margin: "0 auto" }}>
<h1>Public Transit Timetable</h1>
<div style={{ marginBottom: '20px', display: 'flex', gap: '10px' }}>
<div style={{ marginBottom: "20px", display: "flex", gap: "10px" }}>
<input
type="date"
value={selectedDate}
onChange={e => setSelectedDate(e.target.value)}
style={{ padding: '8px', borderRadius: '6px', border: '1px solid #ccc' }}
onChange={(e) => setSelectedDate(e.target.value)}
style={{
padding: "8px",
borderRadius: "6px",
border: "1px solid #ccc",
}}
/>
<select
value={selectedLineId}
onChange={e => setSelectedLineId(e.target.value)}
style={{ padding: '8px', borderRadius: '6px', border: '1px solid #ccc', minWidth: '200px' }}
onChange={(e) => setSelectedLineId(e.target.value)}
style={{
padding: "8px",
borderRadius: "6px",
border: "1px solid #ccc",
minWidth: "200px",
}}
>
<option value="">All lines</option>
{lines.map(line => (
{lines.map((line) => (
<option key={line.id} value={line.id}>
Line {line.lineNumber} ({line.startStop} → {line.endStop})
</option>
@@ -75,101 +88,191 @@ export default function TimetableViewer({ apiBaseUrl = 'http://localhost:5001' }
</div>
{summary && (
<div style={{ marginBottom: '16px', display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#eef2ff', color: '#3730a3' }}>
<div
style={{
marginBottom: "16px",
display: "flex",
gap: "12px",
flexWrap: "wrap",
}}
>
<span
style={{
padding: "6px 10px",
borderRadius: "999px",
backgroundColor: "#eef2ff",
color: "#3730a3",
}}
>
Total: {summary.total}
</span>
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#ecfeff', color: '#155e75' }}>
<span
style={{
padding: "6px 10px",
borderRadius: "999px",
backgroundColor: "#ecfeff",
color: "#155e75",
}}
>
Vast: {summary.recurring}
</span>
<span style={{ padding: '6px 10px', borderRadius: '999px', backgroundColor: '#f8fafc', color: '#334155' }}>
<span
style={{
padding: "6px 10px",
borderRadius: "999px",
backgroundColor: "#f8fafc",
color: "#334155",
}}
>
Eenmalig: {summary.oneTime}
</span>
</div>
)}
{loading && <p>Loading schedules...</p>}
{error && <p style={{ color: 'red' }}>{error}</p>}
{error && <p style={{ color: "red" }}>{error}</p>}
<table style={{ width: '100%', borderCollapse: 'collapse', marginTop: '20px' }}>
<thead style={{ backgroundColor: '#f1f5f9' }}>
<table
style={{ width: "100%", borderCollapse: "collapse", marginTop: "20px" }}
>
<thead style={{ backgroundColor: "#f1f5f9" }}>
<tr>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Time</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Line</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Vehicle</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Day status</th>
<th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #ddd' }}>Delay</th>
<th
style={{
padding: "12px",
textAlign: "left",
borderBottom: "2px solid #ddd",
}}
>
Time
</th>
<th
style={{
padding: "12px",
textAlign: "left",
borderBottom: "2px solid #ddd",
}}
>
Line
</th>
<th
style={{
padding: "12px",
textAlign: "left",
borderBottom: "2px solid #ddd",
}}
>
Vehicle
</th>
<th
style={{
padding: "12px",
textAlign: "left",
borderBottom: "2px solid #ddd",
}}
>
Day status
</th>
<th
style={{
padding: "12px",
textAlign: "left",
borderBottom: "2px solid #ddd",
}}
>
Delay
</th>
</tr>
</thead>
<tbody>
{timeSlots.map((slot, idx) => {
const slotSchedules = schedulesBySlot[slot] || []
if (slotSchedules.length === 0) return null
const slotSchedules = schedulesBySlot[slot] || [];
if (slotSchedules.length === 0) return null;
return slotSchedules.map((schedule, sidx) => (
<tr key={`${idx}-${sidx}`} style={{ borderBottom: '1px solid #eee' }}>
<td style={{ padding: '12px' }}>
{sidx === 0 ? slot : ''}
</td>
<td style={{ padding: '12px', fontWeight: 'bold', color: '#4f46e5' }}>
<tr
key={`${idx}-${sidx}`}
style={{ borderBottom: "1px solid #eee" }}
>
<td style={{ padding: "12px" }}>{sidx === 0 ? slot : ""}</td>
<td
style={{
padding: "12px",
fontWeight: "bold",
color: "#4f46e5",
}}
>
Line {schedule.lineNumber}
</td>
<td style={{ padding: '12px' }}>
<td style={{ padding: "12px" }}>
{schedule.busName} ({schedule.licensePlate})
</td>
<td style={{ padding: '12px' }}>
<td style={{ padding: "12px" }}>
<span
style={{
padding: '4px 8px',
borderRadius: '4px',
backgroundColor: schedule.status === 'in_progress' ? '#dbeafe' : schedule.status === 'completed' ? '#dcfce7' : '#f3f4f6',
color: schedule.status === 'in_progress' ? '#1d4ed8' : schedule.status === 'completed' ? '#166534' : '#374151',
fontSize: '0.9rem',
padding: "4px 8px",
borderRadius: "4px",
backgroundColor:
schedule.status === "in_progress"
? "#dbeafe"
: schedule.status === "completed"
? "#dcfce7"
: "#f3f4f6",
color:
schedule.status === "in_progress"
? "#1d4ed8"
: schedule.status === "completed"
? "#166534"
: "#374151",
fontSize: "0.9rem",
}}
>
{schedule.status}
</span>
</td>
<td style={{ padding: '12px' }}>
{typeof schedule.currentDelayMinutes === 'number' && schedule.currentDelayMinutes !== null
? `${schedule.currentDelayMinutes > 0 ? '+' : ''}${schedule.currentDelayMinutes} min`
: '—'}
<td style={{ padding: "12px" }}>
{typeof schedule.currentDelayMinutes === "number" &&
schedule.currentDelayMinutes !== null
? `${schedule.currentDelayMinutes > 0 ? "+" : ""}${schedule.currentDelayMinutes} min`
: "—"}
</td>
</tr>
))
));
})}
</tbody>
</table>
{schedules.length === 0 && !loading && (
<p style={{ textAlign: 'center', color: '#999', marginTop: '40px' }}>
<p style={{ textAlign: "center", color: "#999", marginTop: "40px" }}>
No schedules found for the selected date and line.
</p>
)}
</div>
)
);
}
function generateTimeSlots() {
const slots = []
const slots = [];
for (let hour = 0; hour < 24; hour++) {
slots.push(`${String(hour).padStart(2, '0')}:00-${String((hour + 1) % 24).padStart(2, '0')}:00`)
slots.push(
`${String(hour).padStart(2, "0")}:00-${String((hour + 1) % 24).padStart(2, "0")}:00`,
);
}
return slots
return slots;
}
function groupSchedulesByTimeSlot(schedules, slots) {
const grouped = {}
slots.forEach(slot => {
grouped[slot] = []
})
const grouped = {};
slots.forEach((slot) => {
grouped[slot] = [];
});
schedules.forEach(schedule => {
const startHour = new Date(schedule.startTime).getHours()
const slot = `${String(startHour).padStart(2, '0')}:00-${String((startHour + 1) % 24).padStart(2, '0')}:00`
schedules.forEach((schedule) => {
const startHour = new Date(schedule.startTime).getHours();
const slot = `${String(startHour).padStart(2, "0")}:00-${String((startHour + 1) % 24).padStart(2, "0")}:00`;
if (grouped[slot]) {
grouped[slot].push(schedule)
grouped[slot].push(schedule);
}
})
});
return grouped
return grouped;
}
@@ -215,32 +215,50 @@ export default function WeeklyScheduleEditor({
{message && <div className="message">{message}</div>}
{DAY_GROUPS.map((group) => (
<div key={group.value} style={{ marginTop: "1.5rem" }}>
<h4>{group.label}</h4>
{(groupedSchedules[group.value] || []).length === 0 ? (
<p>No schedules configured.</p>
) : (
<div style={{ display: "grid", gap: "0.75rem" }}>
{groupedSchedules[group.value].map((schedule) => (
<article key={schedule.id} className="schedule-card">
<div className="schedule-header">
<strong>Line {schedule.lineNumber}</strong>
<span className="schedule-time">
{String(schedule.startTime).slice(0, 5)} -{" "}
{String(schedule.endTime).slice(0, 5)}
</span>
</div>
<div className="schedule-details">
{dayLabel(schedule.dayOfWeek)}: {schedule.startStop} to{" "}
{schedule.endStop}
</div>
<div className="form-row" style={{ marginTop: "0.75rem" }}>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
gap: "1rem",
marginTop: "1.5rem",
alignItems: "start",
}}
>
{DAY_GROUPS.map((group) => (
<section
key={group.value}
style={{
minWidth: 0,
border: "1px solid #dbe3ef",
borderRadius: "8px",
background: "#f8fafc",
padding: "0.75rem",
}}
>
<h4 style={{ marginTop: 0 }}>{group.label}</h4>
{(groupedSchedules[group.value] || []).length === 0 ? (
<p>No schedules configured.</p>
) : (
<div style={{ display: "grid", gap: "0.75rem" }}>
{groupedSchedules[group.value].map((schedule) => (
<article key={schedule.id} className="schedule-card">
<div className="schedule-header">
<strong>Line {schedule.lineNumber}</strong>
<span className="schedule-time">
{String(schedule.startTime).slice(0, 5)} -{" "}
{String(schedule.endTime).slice(0, 5)}
</span>
</div>
<div className="schedule-details">
{dayLabel(schedule.dayOfWeek)}: {schedule.startStop} to{" "}
{schedule.endStop}
</div>
<select
value={schedule.driverId || ""}
onChange={(event) =>
assignDriver(schedule.id, event.target.value)
}
style={{ width: "100%", marginTop: "0.75rem" }}
>
<option value="">Unassigned</option>
{drivers.map((driver) => (
@@ -253,16 +271,17 @@ export default function WeeklyScheduleEditor({
type="button"
className="danger"
onClick={() => removeSchedule(schedule.id)}
style={{ width: "100%", marginTop: "0.5rem" }}
>
Delete
</button>
</div>
</article>
))}
</div>
)}
</div>
))}
</article>
))}
</div>
)}
</section>
))}
</div>
</section>
);
}
+58 -39
View File
@@ -1,41 +1,43 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const db = require('./db');
require('dotenv').config();
const express = require("express");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const db = require("./db");
require("dotenv").config();
const app = express();
app.use(express.json());
// Simple CORS middleware for browser-based admin UI testing.
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") {
return res.sendStatus(204);
}
next();
});
const ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || 'http://localhost:5003/error_log';
const ERROR_LOG_API_URL =
process.env.ERROR_LOG_API_URL ||
"https://error.xtransit.testinstance.nl/error_log";
function friendlyErrorMessage(statusCode) {
if (statusCode === 400) return 'Controleer je invoer en probeer opnieuw.';
if (statusCode === 401) return 'Je sessie is verlopen. Log opnieuw in.';
if (statusCode === 403) return 'Je hebt hier geen toegang toe.';
if (statusCode === 404) return 'Niet gevonden.';
return 'Er ging iets mis. Probeer het later opnieuw.';
if (statusCode === 400) return "Controleer je invoer en probeer opnieuw.";
if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in.";
if (statusCode === 403) return "Je hebt hier geen toegang toe.";
if (statusCode === 404) return "Niet gevonden.";
return "Er ging iets mis. Probeer het later opnieuw.";
}
async function reportError({ partOfService, error }) {
async function reportError({ partofservice, error }) {
try {
await fetch(ERROR_LOG_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
service: 'auth_api',
partOfService,
service: "auth_api",
partofservice,
error,
}),
});
@@ -45,9 +47,13 @@ async function reportError({ partOfService, error }) {
app.use((req, res, next) => {
const originalJson = res.json.bind(res);
res.json = (body) => {
if (res.statusCode >= 400 && body && typeof body === 'object') {
const rawError = body.error || body.message || body.details || `HTTP ${res.statusCode}`;
void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: rawError });
if (res.statusCode >= 400 && body && typeof body === "object") {
const rawError =
body.error || body.message || body.details || `HTTP ${res.statusCode}`;
void reportError({
partofservice: `${req.method} ${req.originalUrl}`,
error: rawError,
});
return originalJson({ error: friendlyErrorMessage(res.statusCode) });
}
return originalJson(body);
@@ -58,46 +64,59 @@ app.use((req, res, next) => {
const JWT_SECRET = process.env.JWT_SECRET;
const ADMIN_USER_CODES = new Set(
String(process.env.ADMIN_USER_CODES || '')
.split(',')
.map(c => Number(c.trim()))
.filter(Number.isFinite)
String(process.env.ADMIN_USER_CODES || "")
.split(",")
.map((c) => Number(c.trim()))
.filter(Number.isFinite),
);
app.post('/login', async (req, res) => {
app.post("/login", async (req, res) => {
try {
const { userCode, password } = req.body;
if (!userCode || !password) return res.status(400).json({ error: 'userCode and password required' });
if (!userCode || !password)
return res.status(400).json({ error: "userCode and password required" });
const result = await db.query('SELECT id, usercode, password FROM workers.users WHERE usercode = $1', [userCode]);
const result = await db.query(
"SELECT id, usercode, password FROM workers.users WHERE usercode = $1",
[userCode],
);
const user = result.rows[0];
if (!user) return res.status(401).json({ error: 'invalid_credentials' });
if (!user) return res.status(401).json({ error: "invalid_credentials" });
const ok = await bcrypt.compare(password, user.password);
if (!ok) return res.status(401).json({ error: 'invalid_credentials' });
if (!ok) return res.status(401).json({ error: "invalid_credentials" });
const isAdmin = ADMIN_USER_CODES.has(Number(user.usercode));
const payload = { userId: user.id, userCode: user.usercode, isAdmin };
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '1h' });
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: "1h" });
res.json({ token, user: { id: user.id, userCode: user.usercode, job: user.job, isAdmin } });
res.json({
token,
user: { id: user.id, userCode: user.usercode, job: user.job, isAdmin },
});
} catch (err) {
await reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) });
await reportError({
partofservice: `${req.method} ${req.originalUrl}`,
error: err?.stack || err?.message || String(err),
});
console.error(err);
res.status(500).json({ error: friendlyErrorMessage(500) });
}
});
app.get('/me', async (req, res) => {
app.get("/me", async (req, res) => {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) return res.status(401).json({ error: 'missing_token' });
if (!auth || !auth.startsWith("Bearer "))
return res.status(401).json({ error: "missing_token" });
const token = auth.slice(7);
try {
const payload = jwt.verify(token, JWT_SECRET);
res.json({ user: payload });
} catch (err) {
void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) });
void reportError({
partofservice: `${req.method} ${req.originalUrl}`,
error: err?.stack || err?.message || String(err),
});
res.status(401).json({ error: friendlyErrorMessage(401) });
}
});
+5 -5
View File
@@ -12,7 +12,7 @@ services:
PG_DB: ${POSTGRES_DB}
PG_USER: ${POSTGRES_USER_LOGGER}
PG_PASS: ${POSTGRES_PASSWORD_LOGGER}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log}
restart: unless-stopped
auth_api:
@@ -30,7 +30,7 @@ services:
PG_PASS: ${POSTGRES_PASSWORD_AUTH}
ADMIN_USER_CODES: ${ADMIN_USER_CODES}
DEV_BYPASS: ${DEV_BYPASS:-0}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log}
depends_on:
- error_log_api
restart: unless-stopped
@@ -47,7 +47,7 @@ services:
PG_DB: ${POSTGRES_DB}
PG_USER: ${POSTGRES_USER_EMPLOYEE}
PG_PASS: ${POSTGRES_PASSWORD_EMPLOYEE}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log}
depends_on:
- error_log_api
restart: unless-stopped
@@ -66,7 +66,7 @@ services:
PG_USER: ${POSTGRES_USER_TRANSIT}
PG_PASS: ${POSTGRES_PASSWORD_TRANSIT}
DEV_MOCK: ${DEV_MOCK:-0}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL}
ERROR_LOG_API_URL: ${ERROR_LOG_API_URL:-https://error.xtransit.testinstance.nl/error_log}
depends_on:
- error_log_api
restart: unless-stopped
@@ -79,7 +79,7 @@ services:
environment:
AUTH_API: ${AUTH_API}
TRANSIT_API: ${TRANSIT_API}
ERROR_LOG_API: ${ERROR_LOG_API}
ERROR_LOG_API: ${ERROR_LOG_API:-https://error.xtransit.testinstance.nl}
depends_on:
- auth_api
- transit_api
+82 -59
View File
@@ -1,81 +1,104 @@
const express = require('express');
const bcrypt = require('bcryptjs');
const db = require('./db');
require('dotenv').config();
const express = require("express");
const bcrypt = require("bcryptjs");
const db = require("./db");
require("dotenv").config();
const app = express();
app.use(express.json());
const ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || 'http://localhost:5003/error_log';
const ERROR_LOG_API_URL =
process.env.ERROR_LOG_API_URL ||
"https://error.xtransit.testinstance.nl/error_log";
function friendlyErrorMessage(statusCode) {
if (statusCode === 400) return 'Controleer je invoer en probeer opnieuw.';
if (statusCode === 401) return 'Je sessie is verlopen. Log opnieuw in.';
if (statusCode === 403) return 'Je hebt hier geen toegang toe.';
if (statusCode === 404) return 'Niet gevonden.';
return 'Er ging iets mis. Probeer het later opnieuw.';
if (statusCode === 400) return "Controleer je invoer en probeer opnieuw.";
if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in.";
if (statusCode === 403) return "Je hebt hier geen toegang toe.";
if (statusCode === 404) return "Niet gevonden.";
return "Er ging iets mis. Probeer het later opnieuw.";
}
async function reportError({ partOfService, error }) {
try {
await fetch(ERROR_LOG_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
service: 'employee_api',
partOfService,
error,
}),
});
} catch (_) {}
async function reportError({ partofservice, error }) {
try {
await fetch(ERROR_LOG_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
service: "employee_api",
partofservice,
error,
}),
});
} catch (_) {}
}
app.use((req, res, next) => {
const originalJson = res.json.bind(res);
res.json = (body) => {
if (res.statusCode >= 400 && body && typeof body === 'object') {
const rawError = body.error || body.message || body.details || `HTTP ${res.statusCode}`;
void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: rawError });
return originalJson({ error: friendlyErrorMessage(res.statusCode) });
}
return originalJson(body);
};
const originalJson = res.json.bind(res);
res.json = (body) => {
if (res.statusCode >= 400 && body && typeof body === "object") {
const rawError =
body.error || body.message || body.details || `HTTP ${res.statusCode}`;
void reportError({
partofservice: `${req.method} ${req.originalUrl}`,
error: rawError,
});
return originalJson({ error: friendlyErrorMessage(res.statusCode) });
}
return originalJson(body);
};
next();
next();
});
// Employee onboarding
app.post('/createemployee', async (req, res) => {
try {
const {firstname, lastname, job, password} = req.body;
if (!firstname || !lastname || !job || !password) return res.status(400).json({error: 'Firstname, Lastname, Job title and password are required'});
app.post("/createemployee", async (req, res) => {
try {
const { firstname, lastname, job, password } = req.body;
if (!firstname || !lastname || !job || !password)
return res.status(400).json({
error: "Firstname, Lastname, Job title and password are required",
});
const resultCreateEmployee = await db.query('INSERT INTO workers.employees (firstname, lastname, job) VALUES ($1, $2, $3) RETURNING id', [firstname, lastname, job]);
const newEmployee = resultCreateEmployee.rows[0];
const encryptedpassword = await bcrypt.hash(password, 12);
const resultCreateUser = await db.query('INSERT INTO workers.users (employee, password) VALUES ($1, $2) RETURNING usercode', [newEmployee.id, encryptedpassword]);
const newUser = resultCreateUser.rows[0];
return res.status(201).json({ employee: firstname, code: newUser.usercoden });
}
catch (err) {
void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) });
console.error(err);
res.status(500).json({ error: friendlyErrorMessage(500) });
const resultCreateEmployee = await db.query(
"INSERT INTO workers.employees (firstname, lastname, job) VALUES ($1, $2, $3) RETURNING id",
[firstname, lastname, job],
);
const newEmployee = resultCreateEmployee.rows[0];
const encryptedpassword = await bcrypt.hash(password, 12);
const resultCreateUser = await db.query(
"INSERT INTO workers.users (employee, password) VALUES ($1, $2) RETURNING usercode",
[newEmployee.id, encryptedpassword],
);
const newUser = resultCreateUser.rows[0];
return res
.status(201)
.json({ employee: firstname, code: newUser.usercoden });
} catch (err) {
void reportError({
partofservice: `${req.method} ${req.originalUrl}`,
error: err?.stack || err?.message || String(err),
});
console.error(err);
res.status(500).json({ error: friendlyErrorMessage(500) });
}
})
});
app.get('/health', async (_req, res, _next) => {
try {
return res.status(200).json({endpoint: 'reachable'})
}
catch (err) {
void reportError({ partOfService: `${req.method} ${req.originalUrl}`, error: err?.stack || err?.message || String(err) });
console.error(err);
res.status(500).json({ endpoint: 'unreachable', error: friendlyErrorMessage(500) })
}
})
app.get("/health", async (_req, res, _next) => {
try {
return res.status(200).json({ endpoint: "reachable" });
} catch (err) {
void reportError({
partofservice: `${req.method} ${req.originalUrl}`,
error: err?.stack || err?.message || String(err),
});
console.error(err);
res
.status(500)
.json({ endpoint: "unreachable", error: friendlyErrorMessage(500) });
}
});
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log(`Auth API listening on ${PORT}`));
module.exports = app;
module.exports = app;
+51 -39
View File
@@ -1,29 +1,29 @@
const express = require('express');
const cors = require('cors');
const jwt = require('jsonwebtoken');
require('dotenv').config();
const express = require("express");
const cors = require("cors");
const jwt = require("jsonwebtoken");
require("dotenv").config();
const db = require('./db');
const db = require("./db");
const app = express();
app.use(express.json({ limit: '64kb' }));
app.use(express.json({ limit: "64kb" }));
app.use(cors());
const PORT = Number(process.env.PORT || 5003);
const JWT_SECRET = process.env.JWT_SECRET || '';
const JWT_SECRET = process.env.JWT_SECRET || "";
// Admin verification is done by `auth_api` and carried in the JWT as `isAdmin`.
function toFriendlyError(statusCode) {
if (statusCode === 400) return 'Controleer je invoer en probeer opnieuw.';
if (statusCode === 401) return 'Je sessie is verlopen. Log opnieuw in.';
if (statusCode === 403) return 'Je hebt hier geen toegang toe.';
if (statusCode === 404) return 'Niet gevonden.';
return 'Er ging iets mis. Probeer het later opnieuw.';
if (statusCode === 400) return "Controleer je invoer en probeer opnieuw.";
if (statusCode === 401) return "Je sessie is verlopen. Log opnieuw in.";
if (statusCode === 403) return "Je hebt hier geen toegang toe.";
if (statusCode === 404) return "Niet gevonden.";
return "Er ging iets mis. Probeer het later opnieuw.";
}
function normalizeErrorText(value) {
if (!value) return 'Unknown error';
if (typeof value === 'string') return value;
if (!value) return "Unknown error";
if (typeof value === "string") return value;
try {
return JSON.stringify(value);
} catch (_) {
@@ -31,20 +31,28 @@ function normalizeErrorText(value) {
}
}
async function insertErrorLog({ service, partOfService, error }) {
const serviceName = String(service || 'unknown_service').slice(0, 120);
const part = partOfService ? String(partOfService).slice(0, 240) : null;
async function insertErrorLog({
service,
partofservice,
partOfService,
error,
}) {
const serviceName = String(service || "unknown_service").slice(0, 120);
const part =
partofservice || partOfService
? String(partofservice || partOfService).slice(0, 240)
: null;
const errorText = String(normalizeErrorText(error)).slice(0, 8000);
await db.query(
'INSERT INTO data.errors ("service", "partOfService", "error") VALUES ($1, $2, $3)',
[serviceName, part, errorText]
"INSERT INTO data.errors (service, partofservice, error) VALUES ($1, $2, $3)",
[serviceName, part, errorText],
);
}
function verifyAdmin(req, res, next) {
const auth = req.headers.authorization;
if (!auth || !auth.startsWith('Bearer ')) {
if (!auth || !auth.startsWith("Bearer ")) {
return res.status(401).json({ error: toFriendlyError(401) });
}
@@ -61,33 +69,37 @@ function verifyAdmin(req, res, next) {
}
}
app.get('/health', async (_req, res) => {
res.json({ status: 'ok', service: 'error_log_api' });
app.get("/health", async (_req, res) => {
res.json({ status: "ok", service: "error_log_api" });
});
app.post('/error_log', async (req, res) => {
app.post("/error_log", async (req, res) => {
try {
const service = req.body?.service;
const partOfService = req.body?.partOfService || null;
const partofservice =
req.body?.partofservice || req.body?.partOfService || null;
const error = req.body?.error;
if (!service || !error) {
return res.status(400).json({ error: toFriendlyError(400) });
}
await insertErrorLog({ service, partOfService, error });
await insertErrorLog({ service, partofservice, error });
return res.status(201).json({ ok: true });
} catch (err) {
console.error('POST /error_log failed:', err);
console.error("POST /error_log failed:", err);
return res.status(500).json({ error: toFriendlyError(500) });
}
});
app.get('/error_log', verifyAdmin, async (req, res) => {
app.get("/error_log", verifyAdmin, async (req, res) => {
try {
const limit = Math.min(Number(req.query.limit || 100), 500);
const service = req.query.service ? String(req.query.service) : null;
const partOfService = req.query.partOfService ? String(req.query.partOfService) : null;
const partofservice =
req.query.partofservice || req.query.partOfService
? String(req.query.partofservice || req.query.partOfService)
: null;
const where = [];
const params = [];
@@ -96,25 +108,25 @@ app.get('/error_log', verifyAdmin, async (req, res) => {
params.push(service);
where.push(`"service" = $${params.length}`);
}
if (partOfService) {
params.push(partOfService);
where.push(`"partOfService" ILIKE $${params.length}`);
params[params.length - 1] = `%${partOfService}%`;
if (partofservice) {
params.push(partofservice);
where.push(`partofservice ILIKE $${params.length}`);
params[params.length - 1] = `%${partofservice}%`;
}
params.push(limit);
const rows = await db.query(
`SELECT id, "date", "service", "partOfService", "error"
`SELECT id, date, service, partofservice, error
FROM data.errors
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
${where.length ? `WHERE ${where.join(" AND ")}` : ""}
ORDER BY "date" DESC, id DESC
LIMIT $${params.length}`,
params
params,
);
return res.json({ errors: rows.rows });
} catch (err) {
console.error('GET /error_log failed:', err);
console.error("GET /error_log failed:", err);
return res.status(500).json({ error: toFriendlyError(500) });
}
});
@@ -122,12 +134,12 @@ app.get('/error_log', verifyAdmin, async (req, res) => {
app.use(async (err, req, res, _next) => {
try {
await insertErrorLog({
service: 'error_log_api',
partOfService: `${req.method} ${req.originalUrl}`,
service: "error_log_api",
partofservice: `${req.method} ${req.originalUrl}`,
error: err?.stack || err?.message || String(err),
});
} catch (logErr) {
console.error('Failed to log internal error:', logErr);
console.error("Failed to log internal error:", logErr);
}
if (res.headersSent) return;
+2 -2
View File
@@ -14,8 +14,8 @@ case $COMMAND in
echo ""
echo "📋 Service URLs:"
echo " Admin UI: http://localhost:5174"
echo " Auth API: http://localhost:5000"
echo " Transit API: http://localhost:5001"
echo " Auth API: https://auth.xtransit.testinstance.nl"
echo " Transit API: https://transit.xtransit.testinstance.nl"
echo " Employee API: http://localhost:4000"
;;
+2 -2
View File
@@ -53,7 +53,7 @@ Note: `ADMIN_USER_CODES` has been moved to the `auth_api` service. Configure adm
npm start
```
API draait op `http://localhost:5001`
API draait op `https://transit.xtransit.testinstance.nl`
## API Endpoints
@@ -154,7 +154,7 @@ De legacy endpoint **GET** `/schedules` blijft beschikbaar voor bestaande client
Voorbeeld:
```bash
curl "http://localhost:5001/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3"
curl "https://transit.xtransit.testinstance.nl/schedules/overview?date=2026-05-06&chauffeurId=12&busStopId=3"
```
### Navigation
+5 -4
View File
@@ -10,7 +10,8 @@ app.use(express.json());
app.use(cors());
const ERROR_LOG_API_URL =
process.env.ERROR_LOG_API_URL || "http://localhost:5003/error_log";
process.env.ERROR_LOG_API_URL ||
"https://error.xtransit.testinstance.nl/error_log";
function friendlyErrorMessage(statusCode) {
if (statusCode === 400) return "Controleer je invoer en probeer opnieuw.";
@@ -20,14 +21,14 @@ function friendlyErrorMessage(statusCode) {
return "Er ging iets mis. Probeer het later opnieuw.";
}
async function reportError({ partOfService, error }) {
async function reportError({ partofservice, error }) {
try {
await fetch(ERROR_LOG_API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
service: "transit_api",
partOfService,
partofservice,
error,
}),
});
@@ -41,7 +42,7 @@ app.use((req, res, next) => {
const rawError =
body.error || body.message || body.details || `HTTP ${res.statusCode}`;
void reportError({
partOfService: `${req.method} ${req.originalUrl}`,
partofservice: `${req.method} ${req.originalUrl}`,
error: rawError,
});
return originalJson({ error: friendlyErrorMessage(res.statusCode) });
+3 -3
View File
@@ -2,9 +2,9 @@
Native Win32 driver client targeting Windows XP-era systems.
The client authenticates with `auth_api` at `192.168.68.66:5000`, then requests
assigned weekly schedule templates from `transit_api` at
`192.168.68.66:5001/driver/schedules/daily`.
The client authenticates with `https://auth.xtransit.testinstance.nl`, then requests
assigned weekly schedule templates from
`https://transit.xtransit.testinstance.nl/driver/schedules/daily`.
Schedules are assigned through `transit.shifts`; the client does not expect a
`driver_id`, `bus_id`, or `weekdays` column on `transit.schedules`.
Binary file not shown.
+8 -6
View File
@@ -25,7 +25,7 @@
#define ID_ROUTES_LABEL 116
#define MAX_BUF 65535
#define API_HOST "192.168.68.66"
#define API_HOST "auth.xtransit.testinstance.nl"
#define AUTH_PORT 5000
#define TRANSIT_PORT 5001
@@ -538,6 +538,7 @@ static int HttpRequestText(const char *method,
const char *src;
const char *slash;
const char *colon;
int secure = 0;
int port = 80;
DWORD flags = 0;
DWORD status = 0;
@@ -554,6 +555,7 @@ static int HttpRequestText(const char *method,
src += 7;
} else if (strncmp(src, "https://", 8) == 0) {
src += 8;
secure = 1;
}
host[0] = '\0';
@@ -596,7 +598,7 @@ static int HttpRequestText(const char *method,
}
if (host[0] == '\0') {
snprintf(host, sizeof(host), "192.168.68.66");
snprintf(host, sizeof(host), "%s", API_HOST);
}
if (port == 0) {
@@ -633,7 +635,7 @@ static int HttpRequestText(const char *method,
NULL,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
0);
secure ? WINHTTP_FLAG_SECURE : 0);
if (!hRequest) {
HttpErrorMessage("Unable to open request", GetLastError());
goto cleanup;
@@ -747,7 +749,7 @@ static void PerformLogin(HWND hwnd, APPSTATE *state)
userCode, password);
SetStatusText(hwnd, "Logging in...");
if (!HttpRequestText("POST", "http://192.168.68.66:5000/login", payload, NULL, response, sizeof(response))) {
if (!HttpRequestText("POST", "https://auth.xtransit.testinstance.nl/login", payload, NULL, response, sizeof(response))) {
SetOutputText(hwnd, "Driver service unreachable. Please try again.");
SetStatusText(hwnd, "Login failed");
return;
@@ -784,7 +786,7 @@ static void PerformRefresh(HWND hwnd, APPSTATE *state)
if (date[0] == '\0') {
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), "https://transit.xtransit.testinstance.nl/driver/schedules/daily?date=%s", date);
SetStatusText(hwnd, "Loading schedule...");
if (!HttpRequestText("GET", scheduleUrl, NULL, authHeader, response, sizeof(response))) {
@@ -825,7 +827,7 @@ static void PerformLoadStops(HWND hwnd, APPSTATE *state)
}
snprintf(authHeader, sizeof(authHeader), "Authorization: Bearer %s\r\n", state->token);
snprintf(stopsUrl, sizeof(stopsUrl), "http://192.168.68.66:5001/bus-lines/%d/stops", lineId);
snprintf(stopsUrl, sizeof(stopsUrl), "https://transit.xtransit.testinstance.nl/bus-lines/%d/stops", lineId);
SetStatusText(hwnd, "Loading stop details...");
if (!HttpRequestText("GET", stopsUrl, NULL, authHeader, response, sizeof(response))) {
Binary file not shown.