diff --git a/frontend/src/components/admin/FdiskSyncTab.tsx b/frontend/src/components/admin/FdiskSyncTab.tsx index ff0e436..54a9b25 100644 --- a/frontend/src/components/admin/FdiskSyncTab.tsx +++ b/frontend/src/components/admin/FdiskSyncTab.tsx @@ -1,8 +1,9 @@ -import { useRef, useEffect } from 'react'; +import { useRef, useEffect, useCallback } from 'react'; import { - Box, Button, Card, CardContent, Chip, CircularProgress, Typography, + Box, Button, Card, CardContent, Chip, CircularProgress, IconButton, Tooltip, Typography, } from '@mui/material'; import SyncIcon from '@mui/icons-material/Sync'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { adminApi } from '../../services/admin'; import { useNotification } from '../../contexts/NotificationContext'; @@ -41,6 +42,14 @@ function FdiskSyncTab() { const running = data?.running ?? false; + const copyLogs = useCallback(() => { + const text = (data?.logs ?? []).map((e) => e.line).join('\n'); + navigator.clipboard.writeText(text).then( + () => showSuccess('Logs kopiert'), + () => showError('Kopieren fehlgeschlagen'), + ); + }, [data?.logs, showSuccess, showError]); + return ( @@ -73,7 +82,20 @@ function FdiskSyncTab() { - Protokoll (letzte 500 Zeilen) + + Protokoll (letzte 500 Zeilen) + + + + + + + + {isLoading && ( diff --git a/sync/src/scraper.ts b/sync/src/scraper.ts index 283fc0c..5305f60 100644 --- a/sync/src/scraper.ts +++ b/sync/src/scraper.ts @@ -6,9 +6,7 @@ const ID_FEUERWEHREN = process.env.FDISK_ID_FEUERWEHREN ?? '164'; const ID_INSTANZEN = process.env.FDISK_ID_INSTANZEN ?? '2853'; const LOGIN_URL = `${BASE_URL}/fdisk/module/vws/logins/logins.aspx`; -const MEMBERS_URL = `${BASE_URL}/fdisk/module/vws/vws/MitgliedschaftenList.aspx` - + `?id_instanzen=${ID_INSTANZEN}` - + `&id_feuerwehren=${ID_FEUERWEHREN}`; +const MEMBERS_URL = `${BASE_URL}/fdisk/module/mgvw/mitgliedschaften/meine_Mitglieder.aspx`; function log(msg: string) { console.log(`[scraper] ${new Date().toISOString()} ${msg}`); @@ -117,36 +115,21 @@ async function login(page: Page, username: string, password: string): Promise { - const menuFrame = page.frame({ name: 'menu' }); - if (!menuFrame) throw new Error('Menu frame (left.aspx) not found in Start.aspx frameset'); + const mainFrame = page.frame({ name: 'mainFrame' }); + if (!mainFrame) throw new Error('mainFrame not found in Start.aspx frameset'); - await menuFrame.waitForLoadState('networkidle'); + log(`Navigating mainFrame to: ${MEMBERS_URL}`); + await mainFrame.goto(MEMBERS_URL, { waitUntil: 'domcontentloaded' }); + await mainFrame.waitForLoadState('networkidle'); - // Log all menu links for diagnostics - const menuLinks = await menuFrame.$$eval('a', (as) => - as.map((a) => ({ text: (a.textContent ?? '').trim(), href: a.href })).filter((l) => l.href), - ); - log(`Menu links (${menuLinks.length}): ${JSON.stringify(menuLinks.slice(0, 20))}`); + const url = mainFrame.url(); + const title = await mainFrame.title(); + log(`mainFrame loaded: ${url} — title: "${title}"`); - // Find the Mitgliedschaften link and click it — this sets the server-side session - // context and navigates mainFrame to the correct URL - const mitgliedLink = menuFrame.locator('a[href*="MitgliedschaftenList"], a[href*="mitglied" i]').first(); - const found = await mitgliedLink.count() > 0; - if (!found) { - throw new Error('Could not find Mitgliedschaften link in menu frame — check menu link log above'); + if (url.includes('BLError') || url.includes('support.aspx') || url.includes('Error')) { + throw new Error(`Member list returned error page: ${url}`); } - const linkHref = await mitgliedLink.getAttribute('href'); - log(`Clicking menu link: ${linkHref}`); - await mitgliedLink.click(); - - // Wait for mainFrame to load the member list - await page.waitForLoadState('networkidle'); - - const mainFrame = page.frame({ name: 'mainFrame' }); - if (!mainFrame) throw new Error('mainFrame not found after menu navigation'); - - log(`mainFrame after menu click: ${mainFrame.url()}`); return mainFrame; } @@ -163,6 +146,12 @@ async function scrapeMembers(frame: Frame): Promise { log(`After form submit: ${frame.url()}`); } + // Log tables found for diagnostics + const tableInfo = await frame.$$eval('table', (ts) => + ts.map((t) => `${t.className || '(no-class)'}[${t.querySelectorAll('tr').length}rows]`), + ); + log(`Tables: ${tableInfo.join(', ') || 'none'}`); + // The member table uses class FdcLayList await frame.waitForSelector('table.FdcLayList', { timeout: 20000 });