feat(admin): add system logs viewer, tabbed data management, fix AT20 sync

This commit is contained in:
Matthias Hochmeister
2026-04-18 18:31:22 +02:00
parent 0a6377a64f
commit 0a5402a9e5
7 changed files with 526 additions and 205 deletions

View File

@@ -15,12 +15,13 @@
import { Router, Request, Response } from 'express';
import { z } from 'zod';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { authenticate } from '../middleware/auth.middleware';
import { requirePermission } from '../middleware/rbac.middleware';
import { auditExport } from '../middleware/audit.middleware';
import auditService, { AuditAction, AuditResourceType, AuditFilters } from '../services/audit.service';
import cleanupService from '../services/cleanup.service';
import atemschutzService from '../services/atemschutz.service';
import userService from '../services/user.service';
import pool from '../config/database';
import logger from '../utils/logger';
@@ -208,10 +209,6 @@ router.post(
try {
const response = await axios.post(`${FDISK_SYNC_URL}/trigger`, req.body, { timeout: 5000 });
res.json({ success: true, data: response.data });
// Fire-and-forget: sync AT20 courses to atemschutz_traeger after FDISK data is written
atemschutzService.syncLehrgangFromKurse().catch(err =>
logger.error('AT20 Atemschutz-Sync fehlgeschlagen', { error: err })
);
} catch (err: unknown) {
if (axios.isAxiosError(err) && err.response?.status === 409) {
res.status(409).json({ success: false, message: 'Sync already in progress' });
@@ -222,6 +219,47 @@ router.post(
}
);
// ---------------------------------------------------------------------------
// GET /api/admin/logs — read Winston application log files
// ---------------------------------------------------------------------------
const LOG_DIR = path.join(__dirname, '../../logs');
router.get(
'/logs',
authenticate,
requirePermission('admin:view'),
async (req: Request, res: Response): Promise<void> => {
try {
const file = req.query.file === 'error' ? 'error.log' : 'combined.log';
const maxLines = Math.min(Math.max(Number(req.query.lines) || 500, 1), 2000);
const search = typeof req.query.search === 'string' ? req.query.search.toLowerCase() : '';
const filePath = path.join(LOG_DIR, file);
if (!fs.existsSync(filePath)) {
res.json({ success: true, data: { file, lines: [], total: 0 } });
return;
}
const content = fs.readFileSync(filePath, 'utf-8');
let allLines = content.split('\n').filter(l => l.trim().length > 0);
if (search) {
allLines = allLines.filter(l => l.toLowerCase().includes(search));
}
const total = allLines.length;
const lines = allLines.slice(-maxLines);
res.json({ success: true, data: { file, lines, total } });
} catch (error) {
logger.error('Failed to read log file', { error });
res.status(500).json({ success: false, message: 'Failed to read log file' });
}
}
);
// ---------------------------------------------------------------------------
// Cleanup / Data Management endpoints
// ---------------------------------------------------------------------------