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

@@ -289,6 +289,41 @@ export async function syncToDatabase(
}
}
/**
* Scans the ausbildung table for AT20 courses with erfolgscode = 'mit Erfolg'
* and upserts atemschutz_traeger records accordingly.
* Must run AFTER syncToDatabase() has committed — i.e. on the same pool, outside the transaction.
*/
export async function syncAT20ToAtemschutz(pool: Pool): Promise<void> {
// First, log a sample of what's actually stored so we can verify the filter strings match.
const sample = await pool.query<{ kurs_kurzbezeichnung: string | null; erfolgscode: string | null; count: string }>(
`SELECT kurs_kurzbezeichnung, erfolgscode, COUNT(*)::text AS count
FROM ausbildung
WHERE kurs_kurzbezeichnung IS NOT NULL
GROUP BY kurs_kurzbezeichnung, erfolgscode
ORDER BY count DESC
LIMIT 20`
);
log(`AT20-Sync: kurs_kurzbezeichnung/erfolgscode distribution (top 20):`);
for (const row of sample.rows) {
log(` kurzbezeichnung=${JSON.stringify(row.kurs_kurzbezeichnung)} erfolgscode=${JSON.stringify(row.erfolgscode)} count=${row.count}`);
}
const result = await pool.query<{ rowCount: number }>(
`INSERT INTO atemschutz_traeger (id, user_id, atemschutz_lehrgang, lehrgang_datum)
SELECT uuid_generate_v4(), a.user_id, true, MIN(a.kurs_datum)
FROM ausbildung a
WHERE TRIM(a.kurs_kurzbezeichnung) = 'AT20'
AND TRIM(a.erfolgscode) = 'mit Erfolg'
GROUP BY a.user_id
ON CONFLICT (user_id) DO UPDATE
SET atemschutz_lehrgang = true,
lehrgang_datum = COALESCE(atemschutz_traeger.lehrgang_datum, EXCLUDED.lehrgang_datum),
updated_at = NOW()`
);
log(`AT20-Sync: ${result.rowCount ?? 0} atemschutz_traeger rows upserted`);
}
async function syncAusbildungen(
client: PoolClient,
ausbildungen: FdiskAusbildung[],

View File

@@ -2,7 +2,7 @@ import 'dotenv/config';
import * as http from 'http';
import { Pool } from 'pg';
import { scrapeAll } from './scraper';
import { syncToDatabase } from './db';
import { syncToDatabase, syncAT20ToAtemschutz } from './db';
// In-memory log ring buffer — last 500 lines captured from all modules
const LOG_BUFFER_MAX = 500;
@@ -70,6 +70,7 @@ async function runSync(force = false): Promise<void> {
const { members, ausbildungen, befoerderungen, untersuchungen, fahrgenehmigungen } = await scrapeAll(username, password);
await syncToDatabase(pool, members, ausbildungen, befoerderungen, untersuchungen, fahrgenehmigungen, force);
log(`Sync complete — ${members.length} members, ${ausbildungen.length} Ausbildungen, ${befoerderungen.length} Beförderungen, ${untersuchungen.length} Untersuchungen, ${fahrgenehmigungen.length} Fahrgenehmigungen`);
await syncAT20ToAtemschutz(pool);
} finally {
syncRunning = false;
await pool.end();