diff --git a/backend/services/clickTtTournamentRegistrationService.js b/backend/services/clickTtTournamentRegistrationService.js index 5f23b62d..1da4b99c 100644 --- a/backend/services/clickTtTournamentRegistrationService.js +++ b/backend/services/clickTtTournamentRegistrationService.js @@ -1,4 +1,6 @@ import { chromium } from 'playwright'; +import { mkdir, writeFile } from 'fs/promises'; +import path from 'path'; import Club from '../models/Club.js'; import ClickTtAccount from '../models/ClickTtAccount.js'; import OfficialTournament from '../models/OfficialTournament.js'; @@ -58,6 +60,15 @@ function parseDmyRange(value) { }; } +function buildDebugHtmlPath() { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + return path.join(process.cwd(), 'tmp', `clicktt-tournament-debug-${timestamp}.html`); +} + +function sanitizePageText(text) { + return String(text || '').replace(/\s+/g, ' ').trim().slice(0, 500); +} + class ClickTtTournamentRegistrationService { async autoRegisterPendingParticipants({ userToken, userId, clubId, tournamentId }) { await checkAccess(userToken, clubId); @@ -134,6 +145,7 @@ class ClickTtTournamentRegistrationService { let context = null; let page = null; const trace = []; + let resultsPageUrl = null; try { browser = await chromium.launch({ @@ -154,7 +166,7 @@ class ClickTtTournamentRegistrationService { await clickTtPlayerRegistrationService._selectClubContext(page, associationMemberNumber, trace); await this._openTournamentSearch(page, trace); await this._filterTournamentSearch(page, tournament, trace); - const resultsPageUrl = page.url(); + resultsPageUrl = page.url(); const processedCompetitions = []; for (const group of competitionGroups) { @@ -197,9 +209,49 @@ class ClickTtTournamentRegistrationService { competitions: processedCompetitions }; } catch (error) { - throw error instanceof HttpError - ? error - : new HttpError(`Click-TT-Turnieranmeldung fehlgeschlagen: ${error.message || error}`, 500); + const diagnostics = { + url: null, + text: null, + htmlPath: null, + resultsPageUrl + }; + + try { + const html = await page?.content?.(); + const htmlPath = html ? buildDebugHtmlPath() : null; + if (html && htmlPath) { + await mkdir(path.dirname(htmlPath), { recursive: true }); + await writeFile(htmlPath, html, 'utf8'); + diagnostics.htmlPath = htmlPath; + } + } catch (_err) { + // keep partial diagnostics + } + + try { + diagnostics.url = page?.url?.() || null; + } catch (_err) { + // ignore + } + + try { + diagnostics.text = sanitizePageText(await page?.locator?.('body')?.innerText?.()); + } catch (_err) { + // ignore + } + + const baseMessage = error instanceof HttpError + ? error.message + : `Click-TT-Turnieranmeldung fehlgeschlagen: ${error.message || error}`; + const wrappedError = new HttpError( + `${baseMessage}${diagnostics.url ? ` (Seite: ${diagnostics.url})` : ''}${diagnostics.htmlPath ? ` (HTML: ${diagnostics.htmlPath})` : ''}${diagnostics.text ? ` - ${diagnostics.text}` : ''}`, + error.statusCode || error.status || 500 + ); + wrappedError.htmlPath = diagnostics.htmlPath || null; + wrappedError.details = { + resultsPageUrl: diagnostics.resultsPageUrl || null + }; + throw wrappedError; } finally { if (context) { try { await context.close(); } catch (_err) {} diff --git a/frontend/src/views/OfficialTournaments.vue b/frontend/src/views/OfficialTournaments.vue index f9a28be5..c1a63f1b 100644 --- a/frontend/src/views/OfficialTournaments.vue +++ b/frontend/src/views/OfficialTournaments.vue @@ -885,6 +885,9 @@ export default { const response = await apiClient.post( `/official-tournaments/${this.currentClub}/${this.uploadedId}/auto-register` ); + if (!response?.data?.success) { + throw new Error(response?.data?.error || 'Die automatische click-TT-Anmeldung ist fehlgeschlagen.'); + } await this.reload(); await this.showInfo( 'Erfolg', @@ -1567,4 +1570,3 @@ th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-ali } } -