From 7cb6b6697173c2a960bc6aa99c3bc4d9caf255a4 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Wed, 11 Mar 2026 17:25:15 +0100 Subject: [PATCH] refactor(clickTtPlayerRegistrationService, memberController): improve error handling and diagnostics - Updated error response structure in memberController to include detailed information instead of a trace array, enhancing clarity for the client. - Enhanced ClickTtPlayerRegistrationService to capture and return detailed information about the selected search result and last submission attempt, improving error diagnostics. - Modified frontend to format and display the new detailed error information, providing better context for users during registration failures. --- backend/controllers/memberController.js | 2 +- .../clickTtPlayerRegistrationService.js | 83 ++++++++++++++++--- frontend/src/views/MembersView.vue | 52 +++++++----- 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/backend/controllers/memberController.js b/backend/controllers/memberController.js index 478ae99b..7e580e67 100644 --- a/backend/controllers/memberController.js +++ b/backend/controllers/memberController.js @@ -225,7 +225,7 @@ const requestClickTtPlayerRegistration = async (req, res) => { res.status(error.statusCode || error.status || 500).json({ success: false, error: error.message || 'Click-TT-Antrag konnte nicht eingereicht werden', - trace: Array.isArray(error.trace) ? error.trace : [] + details: error.details || null }); } }; diff --git a/backend/services/clickTtPlayerRegistrationService.js b/backend/services/clickTtPlayerRegistrationService.js index b8dda633..f66c0bb4 100644 --- a/backend/services/clickTtPlayerRegistrationService.js +++ b/backend/services/clickTtPlayerRegistrationService.js @@ -102,6 +102,8 @@ class ClickTtPlayerRegistrationService { let context = null; let page = null; const trace = []; + let selectedSearchResult = null; + let lastSubmitResult = null; try { browser = await chromium.launch({ @@ -121,15 +123,18 @@ class ClickTtPlayerRegistrationService { await this._clickByText(page, 'Personen suchen', trace); await page.waitForLoadState('domcontentloaded'); - await this._openApplicationAfterSearch(page, memberJson, trace); + selectedSearchResult = await this._openApplicationAfterSearch(page, memberJson, trace); await this._fillApplicationForm(page, memberJson, primaryEmail, trace); await this._clickByText(page, 'Weiter >>', trace); await page.waitForLoadState('domcontentloaded'); + lastSubmitResult = await this._buildLastSubmitResult(page, 'Weiter >>'); await this._dismissConsentOverlays(page, trace); await this._clickByText(page, 'Speichern', trace); await page.waitForLoadState('domcontentloaded'); + lastSubmitResult = await this._buildLastSubmitResult(page, 'Speichern'); await this._clickByText(page, 'Einreichen', trace); await page.waitForLoadState('domcontentloaded'); + lastSubmitResult = await this._buildLastSubmitResult(page, 'Einreichen'); const finalText = sanitizePageText(await page.locator('body').innerText()); const storageState = await context.storageState(); @@ -145,14 +150,19 @@ class ClickTtPlayerRegistrationService { message: `Spielberechtigung fuer ${memberJson.firstName} ${memberJson.lastName} wurde in click-TT eingereicht.`, finalUrl: page.url(), finalText, - trace + details: { + selectedSearchResult, + lastSubmitResult + } }; } catch (error) { const diagnostics = { url: null, text: null, traceTail: trace.slice(-25), - htmlPath: null + htmlPath: null, + selectedSearchResult, + lastSubmitResult }; try { @@ -191,6 +201,10 @@ class ClickTtPlayerRegistrationService { const wrappedError = new HttpError(`${message}${diagnostics.url ? ` (Seite: ${diagnostics.url})` : ''}${diagnostics.htmlPath ? ` (HTML: ${diagnostics.htmlPath})` : ''}${diagnostics.text ? ` - ${diagnostics.text}` : ''}`, error.statusCode || error.status || 500); wrappedError.trace = diagnostics.traceTail || []; wrappedError.htmlPath = diagnostics.htmlPath || null; + wrappedError.details = { + selectedSearchResult: diagnostics.selectedSearchResult || null, + lastSubmitResult: diagnostics.lastSubmitResult || null + }; throw wrappedError; } finally { if (context) { @@ -484,7 +498,7 @@ class ClickTtPlayerRegistrationService { const expectedName = normalizeComparableText(`${member.lastName}, ${member.firstName}`); const expectedBirthDate = formatGermanDate(member.birthDate); - const explicitApplicationHref = await page.locator('table.result-set tr').evaluateAll((rows, criteria) => { + const selectedResult = await page.locator('table.result-set tr').evaluateAll((rows, criteria) => { const normalize = (value) => String(value || '') .normalize('NFKC') .replace(/\s+/g, ' ') @@ -504,7 +518,12 @@ class ClickTtPlayerRegistrationService { if (nameText !== criteria.expectedName) continue; if (birthDateText !== normalize(criteria.expectedBirthDate)) continue; - return link.getAttribute('href'); + return { + href: link.getAttribute('href'), + name: String(strong.textContent || '').replace(/\s+/g, ' ').trim(), + birthDate: String(cells[1]?.textContent || '').replace(/\s+/g, ' ').trim(), + section: String(row.closest('table')?.querySelector('h2')?.textContent || '').replace(/\s+/g, ' ').trim() + }; } return null; @@ -513,8 +532,8 @@ class ClickTtPlayerRegistrationService { expectedBirthDate }); - if (explicitApplicationHref) { - const explicitApplicationLink = page.locator(`a[href="${explicitApplicationHref}"]`).first(); + if (selectedResult?.href) { + const explicitApplicationLink = page.locator(`a[href="${selectedResult.href}"]`).first(); let dialogSeen = false; page.once('dialog', async (dialog) => { dialogSeen = true; @@ -528,9 +547,11 @@ class ClickTtPlayerRegistrationService { this._trace(trace, 'step', { name: 'click', label: 'Spielberechtigung beantragen', - selector: `a[href="${explicitApplicationHref}"]`, + selector: `a[href="${selectedResult.href}"]`, expectedName: `${member.lastName}, ${member.firstName}`, - expectedBirthDate + expectedBirthDate, + selectedName: selectedResult.name, + selectedBirthDate: selectedResult.birthDate }); await explicitApplicationLink.click(); await page.waitForLoadState('domcontentloaded'); @@ -540,7 +561,7 @@ class ClickTtPlayerRegistrationService { dialogSeen, url: page.url() }); - return; + return selectedResult; } const searchResultsText = sanitizePageText(await page.locator('table.result-set').innerText().catch(() => '')); @@ -552,6 +573,48 @@ class ClickTtPlayerRegistrationService { } } + async _buildLastSubmitResult(page, action) { + return { + action, + url: page.url(), + applicant: await this._readCurrentApplicant(page), + pageText: sanitizePageText(await page.locator('body').innerText().catch(() => '')) + }; + } + + async _readCurrentApplicant(page) { + return page.evaluate(() => { + const form = document.querySelector('form.edit-object'); + if (!form) { + return null; + } + + const readValue = (selector) => { + const field = form.querySelector(selector); + if (!field) return ''; + if (field.tagName === 'SELECT') { + const selected = field.options?.[field.selectedIndex]; + return String(selected?.textContent || '').replace(/\s+/g, ' ').trim(); + } + return String(field.value || '').replace(/\s+/g, ' ').trim(); + }; + + const lastName = readValue('input[name$=".15"], input[name$=".31.1.1.5.3"]'); + const firstName = readValue('input[name$=".17"], input[name$=".31.1.1.5.5"]'); + const birthDate = readValue('input[name$=".25"], input[name$=".31.1.1.5.11"]'); + + if (!lastName && !firstName && !birthDate) { + return null; + } + + return { + lastName, + firstName, + birthDate + }; + }).catch(() => null); + } + async _fillFirstAvailable(page, selectors, value) { for (const selector of selectors) { const locator = page.locator(selector).first(); diff --git a/frontend/src/views/MembersView.vue b/frontend/src/views/MembersView.vue index 5cad259e..037b64c9 100644 --- a/frontend/src/views/MembersView.vue +++ b/frontend/src/views/MembersView.vue @@ -675,10 +675,10 @@ export default { this.clickTtPendingMemberIds = [...this.clickTtPendingMemberIds, member.id]; try { const response = await apiClient.post(`/clubmembers/clicktt-registration/${this.currentClub}/${member.id}`); - const traceDetails = this.formatClickTtTrace(response.data?.trace); + const detailsText = this.formatClickTtDetails(response.data?.details); if (response.data?.success) { member.clickTtApplicationSubmitted = true; - const successDetails = [response.data.finalUrl || '', traceDetails].filter(Boolean).join('\n\n'); + const successDetails = [response.data.finalUrl || '', detailsText].filter(Boolean).join('\n\n'); await this.showInfo( 'Click-TT-Antrag', getSafeMessage(response.data.message, 'Der Click-TT-Antrag wurde erfolgreich eingereicht.'), @@ -689,7 +689,7 @@ export default { await this.showInfo( 'Click-TT-Antrag', getSafeMessage(response.data?.error, 'Der Click-TT-Antrag konnte nicht eingereicht werden.'), - traceDetails, + detailsText, 'error' ); } @@ -699,33 +699,47 @@ export default { await this.showInfo( 'Click-TT-Antrag', errorMessage, - this.formatClickTtTrace(error?.response?.data?.trace), + this.formatClickTtDetails(error?.response?.data?.details), 'error' ); } finally { this.clickTtPendingMemberIds = this.clickTtPendingMemberIds.filter(id => id !== member.id); } }, - formatClickTtTrace(trace) { - if (!Array.isArray(trace) || trace.length === 0) { + formatClickTtDetails(details) { + if (!details) { return ''; } - return trace - .slice(-12) - .map((entry) => { - const type = entry?.type || 'trace'; - const parts = [type]; + const parts = []; + const selected = details.selectedSearchResult; + const lastSubmit = details.lastSubmitResult; - if (entry?.name) parts.push(entry.name); - if (entry?.label) parts.push(`label=${entry.label}`); - if (entry?.status) parts.push(`status=${entry.status}`); - if (entry?.url) parts.push(`url=${entry.url}`); - if (entry?.message) parts.push(`message=${entry.message}`); + if (selected?.name || selected?.birthDate || selected?.href) { + parts.push([ + 'Ausgewaehlter Suchtreffer:', + selected?.name ? `Name: ${selected.name}` : '', + selected?.birthDate ? `Geburtsdatum: ${selected.birthDate}` : '', + selected?.href ? `Link: ${selected.href}` : '' + ].filter(Boolean).join('\n')); + } - return parts.join(' | '); - }) - .join('\n'); + if (lastSubmit?.action || lastSubmit?.applicant || lastSubmit?.pageText) { + parts.push([ + 'Ergebnis letzter Submit:', + lastSubmit?.action ? `Aktion: ${lastSubmit.action}` : '', + lastSubmit?.applicant + ? `Antrag fuer: ${[ + lastSubmit.applicant.lastName, + lastSubmit.applicant.firstName + ].filter(Boolean).join(', ')}${lastSubmit.applicant.birthDate ? ` (${lastSubmit.applicant.birthDate})` : ''}` + : '', + lastSubmit?.url ? `URL: ${lastSubmit.url}` : '', + lastSubmit?.pageText ? `Seitenstatus: ${lastSubmit.pageText}` : '' + ].filter(Boolean).join('\n')); + } + + return parts.join('\n\n'); }, toggleNewMember() { this.memberFormIsOpen = !this.memberFormIsOpen;