feat(admin): add system logs viewer, tabbed data management, fix AT20 sync
This commit is contained in:
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user