This commit is contained in:
Matthias Hochmeister
2026-03-13 21:33:18 +01:00
parent b3266afbf8
commit 7245cd577e
2 changed files with 118 additions and 121 deletions

View File

@@ -141,6 +141,7 @@ interface NextcloudChatMessage {
messageParameters: Record<string, any>; messageParameters: Record<string, any>;
reactions: Record<string, any>; reactions: Record<string, any>;
reactionsSelf: string[]; reactionsSelf: string[];
parent?: NextcloudChatMessage;
} }
async function getAllConversations(loginName: string, appPassword: string): Promise<NextcloudConversation[]> { async function getAllConversations(loginName: string, appPassword: string): Promise<NextcloudConversation[]> {
@@ -259,6 +260,20 @@ async function getMessages(token: string, loginName: string, appPassword: string
messageParameters: m.messageParameters ?? {}, messageParameters: m.messageParameters ?? {},
reactions: m.reactions ?? {}, reactions: m.reactions ?? {},
reactionsSelf: m.reactionsSelf ?? [], reactionsSelf: m.reactionsSelf ?? [],
...(m.parent ? { parent: {
id: m.parent.id,
token: m.parent.token,
actorType: m.parent.actorType,
actorId: m.parent.actorId,
actorDisplayName: m.parent.actorDisplayName,
message: m.parent.message,
timestamp: m.parent.timestamp,
messageType: m.parent.messageType ?? '',
systemMessage: m.parent.systemMessage ?? '',
messageParameters: m.parent.messageParameters ?? {},
reactions: m.parent.reactions ?? {},
reactionsSelf: m.parent.reactionsSelf ?? [],
}} : {}),
})); }));
} catch (error) { } catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) { if (axios.isAxiosError(error) && error.response?.status === 401) {

View File

@@ -616,9 +616,62 @@ async function scrapeAusbildungenFromDetailPage(frame: Frame, member: FdiskMembe
return ausbildungen; return ausbildungen;
} }
/**
* Navigate to a sub-section URL and wait for any data table.
* Logs the actual URL and title so wrong-page issues are visible.
* Returns all <tr> rows from the first table found, or null if none.
*/
async function navigateAndGetTableRows(
frame: Frame,
url: string,
): Promise<Array<{ cells: string[] }> | null> {
await frame_goto(frame, url);
const landed = frame.url();
const title = await frame.title().catch(() => '');
log(` → landed: ${landed} | title: "${title}"`);
// Check for FDISK error pages
if (landed.includes('BLError') || landed.includes('support.aspx') || title.toLowerCase().includes('fehler')) {
log(` → ERROR page, skipping`);
return null;
}
// Try table.FdcLayList first, then any table with tbody rows
const selectors = ['table.FdcLayList', 'table'];
for (const sel of selectors) {
const exists = await frame.$(sel).then(el => !!el).catch(() => false);
if (!exists) continue;
const rows = await frame.$$eval(`${sel} tbody tr`, (trs) =>
trs.map((tr) => ({
cells: Array.from(tr.querySelectorAll('td')).map(td => {
const input = td.querySelector('input[type="text"], input:not([type])') as HTMLInputElement | null;
if (input) return input.value?.trim() ?? '';
const select = td.querySelector('select') as HTMLSelectElement | null;
if (select) {
const opt = select.options[select.selectedIndex];
return (opt?.text || opt?.value || '').trim();
}
return td.textContent?.trim() ?? '';
}),
}))
).catch(() => [] as Array<{ cells: string[] }>);
if (rows.length > 0) {
log(` → found ${rows.length} rows via "${sel}"`);
return rows;
}
}
// No table rows found — page might be empty or structured differently
const bodyText = await frame.evaluate(() => document.body?.textContent?.slice(0, 300) ?? '').catch(() => '');
log(` → no table rows found. Body preview: ${bodyText.replace(/\s+/g, ' ')}`);
return [];
}
/** /**
* Navigate to the Beförderungen sub-page and scrape all promotions. * Navigate to the Beförderungen sub-page and scrape all promotions.
* URL is constructed from the mitgliedschaft ID extracted from PersonenForm URL.
*/ */
async function scrapeMemberBefoerderungen( async function scrapeMemberBefoerderungen(
frame: Frame, frame: Frame,
@@ -629,41 +682,25 @@ async function scrapeMemberBefoerderungen(
): Promise<FdiskBefoerderung[]> { ): Promise<FdiskBefoerderung[]> {
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/befoerderungenList.aspx` const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/befoerderungenList.aspx`
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`; + `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
await frame_goto(frame, url);
const rows = await navigateAndGetTableRows(frame, url);
if (!rows) return [];
const results: FdiskBefoerderung[] = []; const results: FdiskBefoerderung[] = [];
try {
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
trs.map((tr) => {
const cells = Array.from(tr.querySelectorAll('td'));
const cell = (i: number) => (cells[i]?.textContent ?? '').trim();
return { datum: cell(0), dienstgrad: cell(1) };
})
);
for (const row of rows) { for (const row of rows) {
const dienstgrad = cellText(row.dienstgrad); const dienstgrad = cellText(row.cells[1]);
if (!dienstgrad) continue; if (!dienstgrad) continue;
const datum = parseDate(row.datum); const datum = parseDate(row.cells[0]);
const syncKey = `${standesbuchNr}::${dienstgrad}::${datum ?? ''}`; const syncKey = `${standesbuchNr}::${dienstgrad}::${datum ?? ''}`;
results.push({ standesbuchNr, datum, dienstgrad, syncKey }); results.push({ standesbuchNr, datum, dienstgrad, syncKey });
} }
log(` Beförderungen for StNr ${standesbuchNr}: ${results.length} rows`); log(` Beförderungen for StNr ${standesbuchNr}: ${results.length} rows`);
for (const b of results) { for (const b of results) log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
log(` ${b.datum ?? '—'} ${b.dienstgrad}`);
}
} catch {
log(` WARN: could not parse Beförderungen table for StNr ${standesbuchNr} (url: ${url})`);
}
return results; return results;
} }
/** /**
* Navigate to the Untersuchungen sub-page and scrape all medical exams. * Navigate to the Untersuchungen sub-page and scrape all medical exams.
* Keeps all rows (one per art+datum); DB stores all, queries filter latest per category.
*/ */
async function scrapeMemberUntersuchungen( async function scrapeMemberUntersuchungen(
frame: Frame, frame: Frame,
@@ -674,54 +711,33 @@ async function scrapeMemberUntersuchungen(
): Promise<FdiskUntersuchung[]> { ): Promise<FdiskUntersuchung[]> {
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/UntersuchungenList.aspx` const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/UntersuchungenList.aspx`
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`; + `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
await frame_goto(frame, url);
const rows = await navigateAndGetTableRows(frame, url);
if (!rows) return [];
const results: FdiskUntersuchung[] = []; const results: FdiskUntersuchung[] = [];
try {
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
trs.map((tr) => {
const cells = Array.from(tr.querySelectorAll('td'));
const cell = (i: number) => (cells[i]?.textContent ?? '').trim();
// Columns: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
return {
datum: cell(0),
anmerkungen: cell(1),
art: cell(2),
ergebnis: cell(3),
};
})
);
for (const row of rows) { for (const row of rows) {
const art = cellText(row.art); // Columns: 0=Datum, 1=Anmerkungen, 2=Untersuchungsart, 3=Tauglichkeitsstufe
const art = cellText(row.cells[2]);
if (!art) continue; if (!art) continue;
const datum = parseDate(row.datum); const datum = parseDate(row.cells[0]);
const syncKey = `${standesbuchNr}::${art}::${datum ?? ''}`; const syncKey = `${standesbuchNr}::${art}::${datum ?? ''}`;
results.push({ results.push({
standesbuchNr, standesbuchNr,
datum, datum,
anmerkungen: cellText(row.anmerkungen), anmerkungen: cellText(row.cells[1]),
art, art,
ergebnis: cellText(row.ergebnis), ergebnis: cellText(row.cells[3]),
syncKey, syncKey,
}); });
} }
log(` Untersuchungen for StNr ${standesbuchNr}: ${results.length} rows`); log(` Untersuchungen for StNr ${standesbuchNr}: ${results.length} rows`);
for (const u of results) { for (const u of results) log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
log(` ${u.datum ?? '—'} [${u.art}] ${u.ergebnis ?? '—'} | ${u.anmerkungen ?? ''}`);
}
} catch {
log(` WARN: could not parse Untersuchungen table for StNr ${standesbuchNr} (url: ${url})`);
}
return results; return results;
} }
/** /**
* Navigate to the Gesetzliche Fahrgenehmigungen sub-page and scrape all entries. * Navigate to the Gesetzliche Fahrgenehmigungen sub-page and scrape all entries.
* This is an inline-edit (ListEdit) page — values are in <input> fields.
*/ */
async function scrapeMemberFahrgenehmigungen( async function scrapeMemberFahrgenehmigungen(
frame: Frame, frame: Frame,
@@ -732,63 +748,29 @@ async function scrapeMemberFahrgenehmigungen(
): Promise<FdiskFahrgenehmigung[]> { ): Promise<FdiskFahrgenehmigung[]> {
const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/Ges_fahrgenehmigungenListEdit.aspx` const url = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/Ges_fahrgenehmigungenListEdit.aspx`
+ `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`; + `?id_mitgliedschaften=${idMitgliedschaft}&id_instanzen=${idInstanzen}&id_feuerwehren=${idFeuerwehren}`;
await frame_goto(frame, url);
const rows = await navigateAndGetTableRows(frame, url);
if (!rows) return [];
const results: FdiskFahrgenehmigung[] = []; const results: FdiskFahrgenehmigung[] = [];
try {
await frame.waitForSelector('table.FdcLayList', { timeout: 10000 });
// ListEdit pages: each data row has inline <input> fields instead of plain text.
// Columns: 0=Ausstellungsdatum, 1=Gültig bis, 2=Behörde, 3=Nummer, 4=Fahrgenehmigungsklasse
const rows = await frame.$$eval('table.FdcLayList tbody tr', (trs) =>
trs.map((tr) => {
const cells = Array.from(tr.querySelectorAll('td'));
const cellVal = (i: number): string => {
const cell = cells[i];
if (!cell) return '';
const input = cell.querySelector('input[type="text"], input:not([type])') as HTMLInputElement | null;
if (input) return input.value?.trim() ?? '';
const select = cell.querySelector('select') as HTMLSelectElement | null;
if (select) {
const opt = select.options[select.selectedIndex];
return (opt?.text || opt?.value || '').trim();
}
return cell.textContent?.trim() ?? '';
};
return {
ausstellungsdatum: cellVal(0),
gueltigBis: cellVal(1),
behoerde: cellVal(2),
nummer: cellVal(3),
klasse: cellVal(4),
};
})
);
for (const row of rows) { for (const row of rows) {
const klasse = cellText(row.klasse); // Columns: 0=Ausstellungsdatum, 1=Gültig bis, 2=Behörde, 3=Nummer, 4=Fahrgenehmigungsklasse
const klasse = cellText(row.cells[4]);
if (!klasse) continue; if (!klasse) continue;
const ausstellungsdatum = parseDate(row.ausstellungsdatum); const ausstellungsdatum = parseDate(row.cells[0]);
const syncKey = `${standesbuchNr}::${klasse}::${ausstellungsdatum ?? ''}`; const syncKey = `${standesbuchNr}::${klasse}::${ausstellungsdatum ?? ''}`;
results.push({ results.push({
standesbuchNr, standesbuchNr,
ausstellungsdatum, ausstellungsdatum,
gueltigBis: parseDate(row.gueltigBis), gueltigBis: parseDate(row.cells[1]),
behoerde: cellText(row.behoerde), behoerde: cellText(row.cells[2]),
nummer: cellText(row.nummer), nummer: cellText(row.cells[3]),
klasse, klasse,
syncKey, syncKey,
}); });
} }
log(` Fahrgenehmigungen for StNr ${standesbuchNr}: ${results.length} rows`); log(` Fahrgenehmigungen for StNr ${standesbuchNr}: ${results.length} rows`);
for (const f of results) { for (const f of results) log(` ${f.ausstellungsdatum ?? '—'} [${f.klasse}] ${f.behoerde ?? ''} ${f.nummer ?? ''}`);
log(` ${f.ausstellungsdatum ?? '—'} [${f.klasse}] ${f.behoerde ?? ''} ${f.nummer ?? ''}`);
}
} catch {
log(` WARN: could not parse Fahrgenehmigungen table for StNr ${standesbuchNr} (url: ${url})`);
}
return results; return results;
} }