From 0a6377a64f8792f99614e2f2edc36cb246f1def9 Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Sat, 18 Apr 2026 18:15:40 +0200 Subject: [PATCH] fix(sync): remove debug file writing and optimize DB queries --- frontend/src/pages/MitgliedDetail.tsx | 2 +- sync/src/db.ts | 135 +++++++++++++------------- sync/src/scraper.ts | 58 ----------- 3 files changed, 66 insertions(+), 129 deletions(-) diff --git a/frontend/src/pages/MitgliedDetail.tsx b/frontend/src/pages/MitgliedDetail.tsx index 7f7319d..f78910c 100644 --- a/frontend/src/pages/MitgliedDetail.tsx +++ b/frontend/src/pages/MitgliedDetail.tsx @@ -1265,7 +1265,7 @@ function MitgliedDetail() { {f.klasse} - + Erhalten am: {f.ausstellungsdatum ? new Date(f.ausstellungsdatum).toLocaleDateString('de-AT') : '—'} diff --git a/sync/src/db.ts b/sync/src/db.ts index 92581f5..e79948a 100644 --- a/sync/src/db.ts +++ b/sync/src/db.ts @@ -255,61 +255,29 @@ export async function syncToDatabase( log(`Members: ${updated} changed, ${unchanged} unchanged, ${forced} forced, ${created} created, ${skipped} skipped`); + // Build StNr → userId lookup map (single query instead of per-record lookups) + const lookupResult = await client.query<{ fdisk_standesbuch_nr: string; user_id: string }>( + `SELECT fdisk_standesbuch_nr, user_id FROM mitglieder_profile WHERE fdisk_standesbuch_nr IS NOT NULL` + ); + const stNrToUserId = new Map( + lookupResult.rows.map(r => [r.fdisk_standesbuch_nr, r.user_id]) + ); + log(`Lookup map: ${stNrToUserId.size} StNr→userId mappings`); + // Upsert Ausbildungen - let ausbildungNew = 0; - let ausbildungUpdated = 0; - let ausbildungSkipped = 0; - - for (const ausb of ausbildungen) { - const result = await client.query<{ user_id: string }>( - `SELECT user_id FROM mitglieder_profile WHERE fdisk_standesbuch_nr = $1`, - [ausb.standesbuchNr] - ); - - if (result.rows.length === 0) { - ausbildungSkipped++; - continue; - } - - const userId = result.rows[0].user_id; - - const upsertResult = await client.query<{ was_inserted: boolean }>( - `INSERT INTO ausbildung (user_id, kursname, kursnummer, kurs_kurzbezeichnung, erfolgscode, kurs_datum, ablaufdatum, ort, bemerkung, fdisk_sync_key) - VALUES ($1, $2, $3, $4, $5, $6::date, $7::date, $8, $9, $10) - ON CONFLICT (user_id, fdisk_sync_key) DO UPDATE SET - kursname = EXCLUDED.kursname, - kursnummer = EXCLUDED.kursnummer, - kurs_kurzbezeichnung = EXCLUDED.kurs_kurzbezeichnung, - erfolgscode = EXCLUDED.erfolgscode, - kurs_datum = EXCLUDED.kurs_datum, - ablaufdatum = EXCLUDED.ablaufdatum, - ort = EXCLUDED.ort, - bemerkung = EXCLUDED.bemerkung, - updated_at = NOW() - RETURNING (xmax = 0) AS was_inserted`, - [userId, ausb.kursname, ausb.kursnummer, ausb.kurzbezeichnung, ausb.erfolgscode, ausb.kursDatum, ausb.ablaufdatum, ausb.ort, ausb.bemerkung, ausb.syncKey] - ); - - if (upsertResult.rows[0]?.was_inserted) { - log(`New Ausbildung: ${ausb.standesbuchNr} — ${ausb.kursname}${ausb.kursDatum ? ` (${ausb.kursDatum})` : ''}`); - ausbildungNew++; - } else { - ausbildungUpdated++; - } - } - - log(`Ausbildungen: ${ausbildungNew} neu, ${ausbildungUpdated} unverändert, ${ausbildungSkipped} übersprungen`); + const ausbildungStats = await syncAusbildungen(client, ausbildungen, stNrToUserId); + log(`Ausbildungen: ${ausbildungStats.neu} neu, ${ausbildungStats.updated} unverändert, ${ausbildungStats.skipped} übersprungen`); // Upsert Beförderungen - const befoerderungStats = await syncBefoerderungen(client, befoerderungen); + const befoerderungStats = await syncBefoerderungen(client, befoerderungen, stNrToUserId); log(`Beförderungen: ${befoerderungStats.neu} neu, ${befoerderungStats.updated} unverändert, ${befoerderungStats.skipped} übersprungen`); // Upsert Untersuchungen - const untersuchungStats = await syncUntersuchungen(client, untersuchungen); + const untersuchungStats = await syncUntersuchungen(client, untersuchungen, stNrToUserId); log(`Untersuchungen: ${untersuchungStats.neu} neu, ${untersuchungStats.updated} unverändert, ${untersuchungStats.skipped} übersprungen`); // Upsert Fahrgenehmigungen - const fahrgenStats = await syncFahrgenehmigungen(client, fahrgenehmigungen); + const fahrgenStats = await syncFahrgenehmigungen(client, fahrgenehmigungen, stNrToUserId); log(`Fahrgenehmigungen: ${fahrgenStats.neu} neu, ${fahrgenStats.updated} unverändert, ${fahrgenStats.skipped} übersprungen`); await client.query('COMMIT'); @@ -321,20 +289,55 @@ export async function syncToDatabase( } } +async function syncAusbildungen( + client: PoolClient, + ausbildungen: FdiskAusbildung[], + stNrToUserId: Map +): Promise<{ neu: number; updated: number; skipped: number }> { + let neu = 0, updated = 0, skipped = 0; + + for (const ausb of ausbildungen) { + const userId = stNrToUserId.get(ausb.standesbuchNr); + if (!userId) { skipped++; continue; } + + const upsertResult = await client.query<{ was_inserted: boolean }>( + `INSERT INTO ausbildung (user_id, kursname, kursnummer, kurs_kurzbezeichnung, erfolgscode, kurs_datum, ablaufdatum, ort, bemerkung, fdisk_sync_key) + VALUES ($1, $2, $3, $4, $5, $6::date, $7::date, $8, $9, $10) + ON CONFLICT (user_id, fdisk_sync_key) DO UPDATE SET + kursname = EXCLUDED.kursname, + kursnummer = EXCLUDED.kursnummer, + kurs_kurzbezeichnung = EXCLUDED.kurs_kurzbezeichnung, + erfolgscode = EXCLUDED.erfolgscode, + kurs_datum = EXCLUDED.kurs_datum, + ablaufdatum = EXCLUDED.ablaufdatum, + ort = EXCLUDED.ort, + bemerkung = EXCLUDED.bemerkung, + updated_at = NOW() + RETURNING (xmax = 0) AS was_inserted`, + [userId, ausb.kursname, ausb.kursnummer, ausb.kurzbezeichnung, ausb.erfolgscode, ausb.kursDatum, ausb.ablaufdatum, ausb.ort, ausb.bemerkung, ausb.syncKey] + ); + + if (upsertResult.rows[0]?.was_inserted) { + log(`New Ausbildung: ${ausb.standesbuchNr} — ${ausb.kursname}${ausb.kursDatum ? ` (${ausb.kursDatum})` : ''}`); + neu++; + } else { + updated++; + } + } + + return { neu, updated, skipped }; +} + async function syncBefoerderungen( client: PoolClient, - befoerderungen: FdiskBefoerderung[] + befoerderungen: FdiskBefoerderung[], + stNrToUserId: Map ): Promise<{ neu: number; updated: number; skipped: number }> { let neu = 0, updated = 0, skipped = 0; for (const b of befoerderungen) { - const result = await client.query<{ user_id: string }>( - `SELECT user_id FROM mitglieder_profile WHERE fdisk_standesbuch_nr = $1`, - [b.standesbuchNr] - ); - - if (result.rows.length === 0) { skipped++; continue; } - const userId = result.rows[0].user_id; + const userId = stNrToUserId.get(b.standesbuchNr); + if (!userId) { skipped++; continue; } const upsertResult = await client.query<{ was_inserted: boolean }>( `INSERT INTO befoerderungen (user_id, datum, dienstgrad, fdisk_sync_key) @@ -360,18 +363,14 @@ async function syncBefoerderungen( async function syncUntersuchungen( client: PoolClient, - untersuchungen: FdiskUntersuchung[] + untersuchungen: FdiskUntersuchung[], + stNrToUserId: Map ): Promise<{ neu: number; updated: number; skipped: number }> { let neu = 0, updated = 0, skipped = 0; for (const u of untersuchungen) { - const result = await client.query<{ user_id: string }>( - `SELECT user_id FROM mitglieder_profile WHERE fdisk_standesbuch_nr = $1`, - [u.standesbuchNr] - ); - - if (result.rows.length === 0) { skipped++; continue; } - const userId = result.rows[0].user_id; + const userId = stNrToUserId.get(u.standesbuchNr); + if (!userId) { skipped++; continue; } const upsertResult = await client.query<{ was_inserted: boolean }>( `INSERT INTO untersuchungen (user_id, datum, anmerkungen, art, ergebnis, fdisk_sync_key) @@ -399,7 +398,8 @@ async function syncUntersuchungen( async function syncFahrgenehmigungen( client: PoolClient, - fahrgenehmigungen: FdiskFahrgenehmigung[] + fahrgenehmigungen: FdiskFahrgenehmigung[], + stNrToUserId: Map ): Promise<{ neu: number; updated: number; skipped: number }> { let neu = 0, updated = 0, skipped = 0; @@ -420,13 +420,8 @@ async function syncFahrgenehmigungen( continue; } - const result = await client.query<{ user_id: string }>( - `SELECT user_id FROM mitglieder_profile WHERE fdisk_standesbuch_nr = $1`, - [f.standesbuchNr] - ); - - if (result.rows.length === 0) { skipped++; continue; } - const userId = result.rows[0].user_id; + const userId = stNrToUserId.get(f.standesbuchNr); + if (!userId) { skipped++; continue; } const upsertResult = await client.query<{ was_inserted: boolean }>( `INSERT INTO fahrgenehmigungen (user_id, ausstellungsdatum, gueltig_bis, behoerde, nummer, klasse, fdisk_sync_key) diff --git a/sync/src/scraper.ts b/sync/src/scraper.ts index 1278e51..914f02a 100644 --- a/sync/src/scraper.ts +++ b/sync/src/scraper.ts @@ -1,6 +1,4 @@ import { chromium, Page, Frame } from '@playwright/test'; -import * as fs from 'fs'; -import * as path from 'path'; import { FdiskMember, FdiskAusbildung, @@ -12,27 +10,10 @@ import { const BASE_URL = process.env.FDISK_BASE_URL ?? 'https://app.fdisk.at'; const ID_FEUERWEHREN = process.env.FDISK_ID_FEUERWEHREN ?? '164'; const ID_INSTANZEN = process.env.FDISK_ID_INSTANZEN ?? '2853'; -const DEBUG_HTML = process.env.FDISK_DEBUG_HTML === '1' || process.env.FDISK_DEBUG_HTML === 'true'; const LOGIN_URL = `${BASE_URL}/fdisk/module/vws/logins/logins.aspx`; const MEMBERS_URL = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/meine_Mitglieder.aspx`; -/** Save frame HTML to debug/ folder when FDISK_DEBUG_HTML=1 */ -async function dumpHtml(frame: Frame, label: string): Promise { - if (!DEBUG_HTML) return; - try { - const debugDir = path.resolve(process.cwd(), 'debug'); - fs.mkdirSync(debugDir, { recursive: true }); - const html = await frame.content(); - const safeName = label.replace(/[^a-zA-Z0-9_-]/g, '_'); - const filePath = path.join(debugDir, `${safeName}.html`); - fs.writeFileSync(filePath, html, 'utf-8'); - log(` [debug] saved HTML → ${filePath} (${(html.length / 1024).toFixed(1)} KB)`); - } catch (err: any) { - log(` [debug] failed to save HTML for "${label}": ${err.message}`); - } -} - /** * Maps a raw FDISK status string to a dashboard status value. * Returns null for unknown/unneeded statuses — those members should be skipped. @@ -219,7 +200,6 @@ export async function scrapeAll(username: string, password: string): Promise<{ const members = await scrapeMembers(mainFrame); log(`Found ${members.length} members (full scrape)`); - if (DEBUG_HTML) log(`[debug] HTML dump mode ON — saving pages to debug/`); const ausbildungen: FdiskAusbildung[] = []; const befoerderungen: FdiskBefoerderung[] = []; @@ -246,9 +226,6 @@ export async function scrapeAll(username: string, password: string): Promise<{ member.wohnort = profileFields.wohnort; member.plz = profileFields.plz; - // Debug: dump the member detail page (Ausbildungen are scraped from here) - await dumpHtml(mainFrame, `detail_StNr${member.standesbuchNr}`); - // Extract mitgliedschaft + person params from the current URL for constructing sub-section URLs. // PersonenForm.aspx is in the personen module; sub-sections are each in their own module. // URL pattern: ?search=1&searchid_mitgliedschaften=X&id_personen=Y&id_mitgliedschaften=X&searchid_personen=Y&searchid_maskmode= @@ -266,15 +243,6 @@ export async function scrapeAll(username: string, password: string): Promise<{ log(` ${member.vorname} ${member.zuname}: ${quals.length} Ausbildungen`); } catch (err: any) { log(` WARN: Ausbildungen scrape failed for ${member.vorname} ${member.zuname} (StNr ${member.standesbuchNr}): ${err.message}`); - // Always dump HTML on failure for diagnosis - try { - const debugDir = path.resolve(process.cwd(), 'debug'); - fs.mkdirSync(debugDir, { recursive: true }); - const html = await mainFrame.content(); - const filePath = path.join(debugDir, `ausbildungen_error_StNr${member.standesbuchNr}.html`); - fs.writeFileSync(filePath, html, 'utf-8'); - log(` [debug] saved error HTML → ${filePath}`); - } catch { /* ignore dump errors */ } } } @@ -769,9 +737,6 @@ async function scrapeAusbildungenFromDetailPage( // Ensure all rows are visible (the URL param should already set this, but belt-and-suspenders) await selectAlleAnzeige(frame); - // Dump HTML for debugging - await dumpHtml(frame, `kurse_StNr${member.standesbuchNr}`); - // Read indexed form fields — same pattern as scrapeMemberFahrgenehmigungen const rawRows = await frame.evaluate((stNr: string) => { const rows: Array<{ @@ -849,10 +814,6 @@ async function scrapeAusbildungenFromDetailPage( }; }); - if (results.length === 0) { - await dumpHtml(frame, `kurse_empty_StNr${member.standesbuchNr}`); - } - return results; } @@ -958,12 +919,6 @@ async function navigateAndGetTableRows( log(` → ${allRows.length} total rows, ${fdcRows.length} FdcLayList rows, ${dataRows.length} data rows (date in col ${dateColIdx})`); - // Debug: dump HTML when no data rows found - if (dataRows.length === 0) { - const urlSlug = url.split('/').pop()?.split('?')[0] ?? 'unknown'; - await dumpHtml(frame, `navigateAndGetTableRows_${urlSlug}`); - } - return { rows: dataRows, dateColIdx }; } @@ -1021,16 +976,12 @@ async function scrapeMemberUntersuchungen( const title = await frame.title().catch(() => ''); if (landed.includes('BLError') || landed.includes('support.aspx') || title.toLowerCase().includes('fehler')) { log(` → Untersuchungen ERROR page: ${landed}`); - await dumpHtml(frame, `untersuchungen_error_StNr${standesbuchNr}`); return []; } // Show all rows await selectAlleAnzeige(frame); - // Dump HTML for diagnosis (always when debug enabled) - await dumpHtml(frame, `untersuchungen_StNr${standesbuchNr}`); - // Try to navigate to history/detail view if available // FDISK may show only the most recent per exam type on the list page. // Look for a "Verlauf" or "Detail" or "Alle anzeigen" link/button @@ -1073,7 +1024,6 @@ async function scrapeMemberUntersuchungen( await frame.waitForNavigation({ timeout: 5000 }).catch(() => {}); } await selectAlleAnzeige(frame); - await dumpHtml(frame, `untersuchungen_history_StNr${standesbuchNr}`); } catch (e) { log(` → Failed to follow history link: ${e}`); } @@ -1135,10 +1085,6 @@ async function scrapeMemberUntersuchungen( log(` → Untersuchungen: ${allRows.length} total rows, ${dataRows.length} data rows (date in col ${dateColIdx})`); - if (dataRows.length === 0) { - await dumpHtml(frame, `untersuchungen_empty_StNr${standesbuchNr}`); - } - const results: FdiskUntersuchung[] = []; for (const row of dataRows) { const valueCols: string[] = []; @@ -1196,9 +1142,6 @@ async function scrapeMemberFahrgenehmigungen( // Show all rows (default is 10) await selectAlleAnzeige(frame); - // Dump HTML for diagnostics - await dumpHtml(frame, `fahrgenehmigungen_StNr${standesbuchNr}`); - // Read form fields by ID pattern: {fieldname}_{rowIndex} const rawRows = await frame.evaluate(() => { const rows: Array<{ @@ -1402,7 +1345,6 @@ async function scrapeMemberFahrgenehmigungen( if (klasseIdx === -1) { log(` Fahrgenehmigungen for StNr ${standesbuchNr}: could not determine Klasse column. Returning empty.`); - await dumpHtml(frame, `fahrgenehmigungen_fallback_StNr${standesbuchNr}`); return []; }