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
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:
+17
-3
@@ -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,28 +47,31 @@ 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 |
|
||||
| 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;
|
||||
|
||||
@@ -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:**
|
||||
|
||||
+6
-6
@@ -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
|
||||
@@ -103,9 +103,9 @@ 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 |
|
||||
| ------------------ | --------------- | ---------------------------------------- |
|
||||
| `VITE_AUTH_API` | Auth API URL | https://auth.xtransit.testinstance.nl |
|
||||
| `VITE_TRANSIT_API` | Transit API URL | https://transit.xtransit.testinstance.nl |
|
||||
|
||||
### Runtime Configuration
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
+67
-48
@@ -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({
|
||||
export const errorLogClient = (token) =>
|
||||
axios.create({
|
||||
baseURL: ERROR_LOG_API,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
});
|
||||
|
||||
export { reportClientError }
|
||||
export { reportClientError };
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,9 +215,27 @@ export default function WeeklyScheduleEditor({
|
||||
|
||||
{message && <div className="message">{message}</div>}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
|
||||
gap: "1rem",
|
||||
marginTop: "1.5rem",
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
{DAY_GROUPS.map((group) => (
|
||||
<div key={group.value} style={{ marginTop: "1.5rem" }}>
|
||||
<h4>{group.label}</h4>
|
||||
<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>
|
||||
) : (
|
||||
@@ -235,12 +253,12 @@ export default function WeeklyScheduleEditor({
|
||||
{dayLabel(schedule.dayOfWeek)}: {schedule.startStop} to{" "}
|
||||
{schedule.endStop}
|
||||
</div>
|
||||
<div className="form-row" style={{ marginTop: "0.75rem" }}>
|
||||
<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>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
+58
-39
@@ -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
@@ -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
|
||||
|
||||
+57
-34
@@ -1,29 +1,31 @@
|
||||
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 }) {
|
||||
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: 'employee_api',
|
||||
partOfService,
|
||||
service: "employee_api",
|
||||
partofservice,
|
||||
error,
|
||||
}),
|
||||
});
|
||||
@@ -33,9 +35,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);
|
||||
@@ -45,35 +51,52 @@ app.use((req, res, next) => {
|
||||
});
|
||||
|
||||
// Employee onboarding
|
||||
app.post('/createemployee', async (req, res) => {
|
||||
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'});
|
||||
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 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 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) });
|
||||
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) => {
|
||||
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) });
|
||||
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) })
|
||||
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}`));
|
||||
|
||||
+51
-39
@@ -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
@@ -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"
|
||||
;;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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.
@@ -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.
Reference in New Issue
Block a user