feat: add centralized error logging system
- Implemented Error Log API to capture and store error logs from various services. - Updated admin_ui to include Error Log Viewer component for inspecting logs. - Enhanced error handling in auth_api, employee_api, and transit_api to report errors to the Error Log API. - Modified existing services to utilize the new error logging functionality. - Updated Docker Compose configuration to include the Error Log API service. - Added error logging service in bus_terminal for Flutter application.
This commit is contained in:
@@ -10,6 +10,7 @@ Modern React + Vite web application for comprehensive transit system administrat
|
||||
✅ **Stop Linking** - Assign stops to bus lines with order and ETA
|
||||
✅ **JWT Authentication** - Secure authentication with role-based access
|
||||
✅ **Production Ready** - Docker containerized with runtime env configuration
|
||||
✅ **Error Log Viewer** - Inspect centralized error logs from all services
|
||||
|
||||
## Local Development
|
||||
|
||||
@@ -31,6 +32,7 @@ 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
|
||||
```
|
||||
|
||||
### Run Development Server
|
||||
@@ -109,12 +111,14 @@ When running in Docker, environment variables are injected at runtime via `docke
|
||||
- `BusLinesManager.jsx` - Create/list bus routes
|
||||
- `FleetManager.jsx` - View fleet vehicles
|
||||
- `LinkStop.jsx` - Link stops to bus lines
|
||||
- `ErrorLogViewer.jsx` - Central error log viewer
|
||||
|
||||
### API Integration
|
||||
|
||||
The app communicates with:
|
||||
- **Auth API** (/login, /me)
|
||||
- **Transit API** (/admin/stops, /admin/bus-lines, /admin/fleet/buses)
|
||||
- **Error Log API** (/error_log)
|
||||
|
||||
All requests include JWT Bearer token for authentication.
|
||||
|
||||
@@ -139,6 +143,7 @@ Users must have a `userCode` in their JWT that matches `ADMIN_USER_CODES` enviro
|
||||
3. Configure proper JWT generation in auth_api
|
||||
4. Set strong `JWT_SECRET` environment variable
|
||||
5. Configure `ADMIN_USER_CODES` with actual admin user IDs
|
||||
6. Configure `VITE_ERROR_LOG_API` if you use a custom logging endpoint
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ 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}"
|
||||
TRANSIT_API: "${TRANSIT_API:-http://localhost:5001}",
|
||||
ERROR_LOG_API: "${ERROR_LOG_API:-http://localhost:5003}"
|
||||
}
|
||||
EOF
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import LinkStop from './components/LinkStop'
|
||||
import BusLinesManager from './components/BusLinesManager'
|
||||
import FleetManager from './components/FleetManager'
|
||||
import ScheduleMaker from './components/ScheduleMaker'
|
||||
import ErrorLogViewer from './components/ErrorLogViewer'
|
||||
|
||||
export default function App() {
|
||||
const [token, setToken] = useState(localStorage.getItem('token'))
|
||||
@@ -46,6 +47,7 @@ export default function App() {
|
||||
<FleetManager token={token} user={user} />
|
||||
<LinkStop token={token} user={user} />
|
||||
<ScheduleMaker token={token} user={user} />
|
||||
<ErrorLogViewer token={token} />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -14,9 +14,38 @@ const normalizeApiBaseUrl = (value, fallbackPath) => {
|
||||
|
||||
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 reportClientError = async ({ service, partOfService, error }) => {
|
||||
try {
|
||||
await fetch(`${ERROR_LOG_API}/error_log`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
service,
|
||||
partOfService,
|
||||
error: typeof error === 'string' ? error : JSON.stringify(error),
|
||||
}),
|
||||
})
|
||||
} catch (e) {
|
||||
// ignore logging failures
|
||||
}
|
||||
}
|
||||
|
||||
export const authClient = axios.create({ baseURL: AUTH_API })
|
||||
|
||||
authClient.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
void reportClientError({
|
||||
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,
|
||||
@@ -35,6 +64,11 @@ export const transitClient = (token) => {
|
||||
// reload to let App show the Login screen
|
||||
if (typeof window !== 'undefined') window.location.reload()
|
||||
}
|
||||
void reportClientError({
|
||||
service: 'admin_ui',
|
||||
partOfService: 'transitClient',
|
||||
error: error?.response?.data?.error || error?.message || 'transitClient_error',
|
||||
})
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
@@ -44,3 +78,10 @@ export const transitClient = (token) => {
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
export const errorLogClient = (token) => axios.create({
|
||||
baseURL: ERROR_LOG_API,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
})
|
||||
|
||||
export { reportClientError }
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function BusLinesManager({ token, user }) {
|
||||
const [estimatedDurationMinutes, setEstimatedDurationMinutes] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [isAdmin] = useState(Boolean(user?.userCode))
|
||||
const [isAdmin] = useState(Boolean(user?.isAdmin))
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(null)
|
||||
const [editingLine, setEditingLine] = useState(null)
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { errorLogClient } from '../api'
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) return '—'
|
||||
try {
|
||||
return new Date(value).toLocaleString()
|
||||
} catch (_) {
|
||||
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 loadLogs = async () => {
|
||||
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 || [])
|
||||
} catch (err) {
|
||||
setMessage(err.response?.data?.error || err.message || 'Kon fouten niet laden')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadLogs()
|
||||
}, [service, partOfService])
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Error log</h2>
|
||||
<p>Laatste foutmeldingen uit de centrale logging service.</p>
|
||||
</div>
|
||||
<button className="secondary" onClick={loadLogs} disabled={loading}>
|
||||
{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)} />
|
||||
</div>
|
||||
|
||||
{message && <div className="message">{message}</div>}
|
||||
|
||||
<div className="list-grid">
|
||||
{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 style={{ marginTop: 8 }}>{log.error}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!logs.length && !message && <div className="muted">Nog geen foutmeldingen gevonden.</div>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ const FixedScheduleEditor = ({
|
||||
const [availableDrivers, setAvailableDrivers] = useState([])
|
||||
const [pendingBatchData, setPendingBatchData] = useState(null)
|
||||
|
||||
const isAdmin = Boolean(user?.userCode)
|
||||
const isAdmin = Boolean(user?.isAdmin)
|
||||
|
||||
const resetForm = () => {
|
||||
setEditingScheduleId(null)
|
||||
|
||||
@@ -9,7 +9,7 @@ export default function FleetManager({ token, user }) {
|
||||
const [licensePlate, setLicensePlate] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [canCreateBus, setCanCreateBus] = useState(true)
|
||||
const [isAdmin] = useState(Boolean(user?.userCode))
|
||||
const [isAdmin] = useState(Boolean(user?.isAdmin))
|
||||
const [editingBus, setEditingBus] = useState(null)
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(null)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function LinkStop({ token, user }) {
|
||||
const [draggedItem, setDraggedItem] = useState(null)
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(null)
|
||||
|
||||
const isAdmin = Boolean(user?.userCode)
|
||||
const isAdmin = Boolean(user?.isAdmin)
|
||||
|
||||
const nextOrder = useMemo(() => stops.length + 1, [stops])
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function ScheduleMaker({ token, user }) {
|
||||
const [drivers, setDrivers] = useState([])
|
||||
const [selectedDriverFilter, setSelectedDriverFilter] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [isAdmin] = useState(Boolean(user?.userCode))
|
||||
const [isAdmin] = useState(Boolean(user?.isAdmin))
|
||||
const [overviewDate, setOverviewDate] = useState(new Date().toISOString().split('T')[0])
|
||||
|
||||
const loadScheduleTemplates = async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ export default function StopsManager({ token, user }) {
|
||||
const [lat, setLat] = useState('')
|
||||
const [lon, setLon] = useState('')
|
||||
const [message, setMessage] = useState(null)
|
||||
const [isAdmin] = useState(Boolean(user?.userCode))
|
||||
const [isAdmin] = useState(Boolean(user?.isAdmin))
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(null)
|
||||
const [editingStop, setEditingStop] = useState(null)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user