Enhance myTischtennis URL controller with improved error handling and logging

Updated MyTischtennisUrlController to include detailed error messages for missing user IDs and team configurations. Added logging for session details and automatic login attempts, improving debugging capabilities. Enhanced request logging for API calls to myTischtennis, ensuring all requests are logged, even in case of errors. Updated requestLoggingMiddleware to only log myTischtennis-related requests, streamlining log management. Improved validation checks in autoFetchMatchResultsService for team and league data integrity.
This commit is contained in:
Torsten Schulz (local)
2025-10-29 18:07:43 +01:00
parent c2b8656783
commit 89329607dc
5 changed files with 295 additions and 50 deletions

View File

@@ -1,6 +1,7 @@
import myTischtennisUrlParserService from '../services/myTischtennisUrlParserService.js';
import myTischtennisService from '../services/myTischtennisService.js';
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
import apiLogService from '../services/apiLogService.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
@@ -228,6 +229,10 @@ class MyTischtennisUrlController {
throw new HttpError(400, 'clubTeamId is required');
}
if (!userIdOrEmail) {
throw new HttpError(401, 'User-ID fehlt. Bitte melden Sie sich an.');
}
// Get actual user ID (userid header might be email address)
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
@@ -246,17 +251,63 @@ class MyTischtennisUrlController {
try {
session = await myTischtennisService.getSession(userId);
console.log('Session found:', !!session);
if (session) {
console.log('Session details:', {
hasCookie: !!session.cookie,
hasAccessToken: !!session.accessToken,
expiresAt: session.expiresAt ? new Date(session.expiresAt * 1000).toISOString() : null
});
}
} catch (sessionError) {
console.log('Session invalid, attempting login...', sessionError.message);
console.error('Session error details:', {
message: sessionError.message,
stack: sessionError.stack,
name: sessionError.name
});
// Versuche automatischen Login mit gespeicherten Credentials
try {
await myTischtennisService.verifyLogin(userId);
console.log('Attempting automatic login for userId:', userId);
// Check if account exists and has password
const accountCheck = await myTischtennisService.getAccount(userId);
if (!accountCheck) {
throw new Error('MyTischtennis-Account nicht gefunden');
}
console.log('Account found:', {
email: accountCheck.email,
hasPassword: !!accountCheck.encryptedPassword,
hasAccessToken: !!accountCheck.accessToken,
hasCookie: !!accountCheck.cookie
});
if (!accountCheck.encryptedPassword) {
throw new Error('Kein Passwort gespeichert. Bitte melden Sie sich in den MyTischtennis-Einstellungen an und speichern Sie Ihr Passwort.');
}
console.log('Calling verifyLogin...');
const verifyResult = await myTischtennisService.verifyLogin(userId);
console.log('verifyLogin result:', verifyResult);
session = await myTischtennisService.getSession(userId);
console.log('Automatic login successful');
console.log('Automatic login successful, session:', {
hasCookie: !!session?.cookie,
hasAccessToken: !!session?.accessToken
});
} catch (loginError) {
console.error('Automatic login failed:', loginError.message);
throw new HttpError(401, 'MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.');
console.error('Automatic login failed - DETAILED ERROR:', {
message: loginError.message,
stack: loginError.stack,
name: loginError.name,
userId: userId,
response: loginError.response?.data,
status: loginError.response?.status
});
const errorMessage = loginError.message || 'Automatischer Login fehlgeschlagen';
throw new HttpError(401, `MyTischtennis-Session abgelaufen und automatischer Login fehlgeschlagen: ${errorMessage}. Bitte melden Sie sich in den MyTischtennis-Einstellungen an.`);
}
}
@@ -291,7 +342,7 @@ class MyTischtennisUrlController {
});
if (!team) {
throw new HttpError(404, 'Team not found');
throw new HttpError(404, `Team mit ID ${clubTeamId} nicht gefunden`);
}
console.log('Team data:', {
@@ -309,11 +360,74 @@ class MyTischtennisUrlController {
} : null
});
if (!team.myTischtennisTeamId || !team.league || !team.league.myTischtennisGroupId) {
throw new HttpError(400, 'Team is not configured for myTischtennis');
// Verbesserte Validierung mit detaillierten Fehlermeldungen
if (!team.myTischtennisTeamId) {
throw new HttpError(400, `Team "${team.name}" (interne ID: ${team.id}) ist nicht für myTischtennis konfiguriert: myTischtennisTeamId fehlt. Bitte konfigurieren Sie das Team zuerst über die MyTischtennis-URL.`);
}
// Stelle sicher, dass die myTischtennisTeamId auch wirklich verwendet wird
console.log(`Verwende myTischtennisTeamId: ${team.myTischtennisTeamId} (nicht die interne clubTeamId: ${team.id})`);
if (!team.league) {
throw new HttpError(400, 'Team ist keiner Liga zugeordnet. Bitte ordnen Sie das Team einer Liga zu.');
}
if (!team.league.myTischtennisGroupId) {
throw new HttpError(400, 'Liga ist nicht für myTischtennis konfiguriert: myTischtennisGroupId fehlt. Bitte konfigurieren Sie die Liga zuerst über die MyTischtennis-URL.');
}
// Validate season before proceeding
if (!team.league.season || !team.league.season.season) {
throw new HttpError(400, 'Liga ist keiner Saison zugeordnet. Bitte ordnen Sie die Liga einer Saison zu.');
}
// Build the URL that will be used - do this early so we can log it even if errors occur
const seasonFull = team.league.season.season;
const seasonParts = seasonFull.split('/');
const seasonShort = seasonParts.length === 2
? `${seasonParts[0].slice(-2)}/${seasonParts[1].slice(-2)}`
: seasonFull;
const seasonStr = seasonShort.replace('/', '--');
const teamnameEncoded = encodeURIComponent(team.name.replace(/\s/g, '_'));
const myTischtennisUrl = `https://www.mytischtennis.de/click-tt/${team.league.association}/${seasonStr}/ligen/${team.league.groupname}/gruppe/${team.league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/spielerbilanzen/gesamt`;
// Log the request to myTischtennis BEFORE making the call
// This ensures we always see what WILL BE sent, even if the call fails
const requestStartTime = Date.now();
console.log('=== ABOUT TO FETCH FROM MYTISCHTENNIS ===');
console.log('URL:', myTischtennisUrl);
console.log('myTischtennisTeamId:', team.myTischtennisTeamId);
console.log('clubTeamId:', team.id);
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: null,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league.name,
association: team.league.association,
groupId: team.league.myTischtennisGroupId,
groupname: team.league.groupname,
season: seasonFull
}),
responseBody: null,
executionTime: null,
errorMessage: 'Request wird ausgeführt...',
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
console.error('Error logging request (non-critical):', logError);
}
// Fetch data for this specific team
// Note: fetchTeamResults will also log and update with actual response
const result = await autoFetchMatchResultsService.fetchTeamResults(
{
userId: account.userId,
@@ -326,32 +440,63 @@ class MyTischtennisUrlController {
team
);
// Also fetch and update league table data
let tableUpdateResult = null;
// Also fetch and update league table data
let tableUpdateResult = null;
try {
await autoFetchMatchResultsService.fetchAndUpdateLeagueTable(account.userId, team.league.id);
tableUpdateResult = 'League table updated successfully';
console.log('✓ League table updated for league:', team.league.id);
} catch (error) {
console.error('Error fetching league table data:', error);
tableUpdateResult = 'League table update failed: ' + error.message;
// Don't fail the entire request if table update fails
}
res.json({
success: true,
message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`,
data: {
fetchedCount: result.fetchedCount,
teamName: team.name,
tableUpdate: tableUpdateResult
tableUpdateResult = 'League table updated successfully';
console.log('✓ League table updated for league:', team.league.id);
} catch (error) {
console.error('Error fetching league table data:', error);
tableUpdateResult = 'League table update failed: ' + error.message;
// Don't fail the entire request if table update fails
}
});
} catch (error) {
console.error('Error in fetchTeamData:', error);
console.error('Error stack:', error.stack);
next(error);
}
res.json({
success: true,
message: `${result.fetchedCount} Datensätze abgerufen und verarbeitet`,
data: {
fetchedCount: result.fetchedCount,
teamName: team.name,
tableUpdate: tableUpdateResult
}
});
} catch (error) {
console.error('Error in fetchTeamData:', error);
console.error('Error stack:', error.stack);
// Update log with error information if we got far enough to build the URL
if (typeof myTischtennisUrl !== 'undefined' && account && team) {
const requestExecutionTime = Date.now() - requestStartTime;
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: myTischtennisUrl.replace('https://www.mytischtennis.de', ''),
statusCode: 0,
requestBody: JSON.stringify({
url: myTischtennisUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
leagueName: team.league?.name,
association: team.league?.association,
groupname: team.league?.groupname,
groupId: team.league?.myTischtennisGroupId
}),
responseBody: null,
executionTime: requestExecutionTime,
errorMessage: error.message || String(error),
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
console.error('Error logging failed request:', logError);
}
}
next(error);
}
}
/**

View File

@@ -41,8 +41,8 @@ export const requestLoggingMiddleware = async (req, res, next) => {
const ipAddress = req.ip || req.connection.remoteAddress || req.headers['x-forwarded-for'];
const path = req.path || req.url;
// Nur myTischtennis-Requests loggen
// Skip logging for non-data endpoints (Status-Checks, Health-Checks, etc.)
// Nur Daten-Abrufe von API-Endpunkten werden geloggt
// Exclude any endpoint containing 'status' or root paths
if (
path.includes('/status') ||
@@ -54,6 +54,11 @@ export const requestLoggingMiddleware = async (req, res, next) => {
return;
}
// Nur myTischtennis-Endpunkte loggen (z.B. /api/mytischtennis/*)
if (!path.includes('/mytischtennis')) {
return;
}
// Get user ID if available (wird von authMiddleware gesetzt)
const userId = req.user?.id || null;

View File

@@ -102,7 +102,13 @@ class ApiLogService {
}
if (path) {
where.path = { [Op.like]: `%${path}%` };
// Handle "NOT:" prefix for excluding paths (e.g., "NOT:/mytischtennis")
if (path.startsWith('NOT:')) {
const excludePath = path.substring(4);
where.path = { [Op.not]: { [Op.like]: `%${excludePath}%` } };
} else {
where.path = { [Op.like]: `%${path}%` };
}
}
if (statusCode !== null) {

View File

@@ -1,5 +1,6 @@
import myTischtennisService from './myTischtennisService.js';
import myTischtennisFetchLogService from './myTischtennisFetchLogService.js';
import apiLogService from './apiLogService.js';
import myTischtennisClient from '../clients/myTischtennisClient.js';
import MyTischtennis from '../models/MyTischtennis.js';
import ClubTeam from '../models/ClubTeam.js';
@@ -182,6 +183,14 @@ class AutoFetchMatchResultsService {
* Fetch results for a specific team
*/
async fetchTeamResults(account, team) {
// Validate required data
if (!team.league) {
throw new Error('Team league is required');
}
if (!team.league.season || !team.league.season.season) {
throw new Error('Team league season is required');
}
const league = team.league;
const season = league.season;
@@ -207,26 +216,83 @@ class AutoFetchMatchResultsService {
const playerStatsUrl = `https://www.mytischtennis.de/click-tt/${league.association}/${seasonStr}/ligen/${league.groupname}/gruppe/${league.myTischtennisGroupId}/mannschaft/${team.myTischtennisTeamId}/${teamnameEncoded}/spielerbilanzen/gesamt?_data=routes%2Fclick-tt%2B%2F%24association%2B%2F%24season%2B%2F%24type%2B%2F%28%24groupname%29.gruppe.%24urlid_.mannschaft.%24teamid.%24teamname%2B%2Fspielerbilanzen.%24filter`;
devLog(`Fetching player stats from: ${playerStatsUrl}`);
const fetchStartTime = Date.now();
let playerStatsResponse = null;
let fetchExecutionTime = 0;
let responseStatus = 0;
let responseBodyData = null;
let fetchError = null;
const playerStatsResponse = await fetch(playerStatsUrl, {
headers: {
'Cookie': account.cookie || '',
'Authorization': `Bearer ${account.accessToken}`,
'Accept': 'application/json'
try {
playerStatsResponse = await fetch(playerStatsUrl, {
headers: {
'Cookie': account.cookie || '',
'Authorization': `Bearer ${account.accessToken}`,
'Accept': 'application/json'
}
});
fetchExecutionTime = Date.now() - fetchStartTime;
responseStatus = playerStatsResponse.status;
if (playerStatsResponse.ok) {
const playerStatsData = await playerStatsResponse.json();
responseBodyData = playerStatsData;
// Log complete response for debugging
console.log('=== PLAYER STATS RESPONSE START ===');
console.log(JSON.stringify(playerStatsData, null, 2));
console.log('=== PLAYER STATS RESPONSE END ===');
const playerCount = await this.processTeamData(team, playerStatsData);
totalProcessed += playerCount;
devLog(`Processed ${playerCount} player statistics`);
} else {
// Read error response body
try {
responseBodyData = await playerStatsResponse.text();
} catch (e) {
responseBodyData = `Error reading response: ${e.message}`;
}
}
});
if (playerStatsResponse.ok) {
const playerStatsData = await playerStatsResponse.json();
// Log complete response for debugging
console.log('=== PLAYER STATS RESPONSE START ===');
console.log(JSON.stringify(playerStatsData, null, 2));
console.log('=== PLAYER STATS RESPONSE END ===');
const playerCount = await this.processTeamData(team, playerStatsData);
totalProcessed += playerCount;
devLog(`Processed ${playerCount} player statistics`);
} catch (error) {
fetchExecutionTime = Date.now() - fetchStartTime;
fetchError = error.message || String(error);
responseStatus = 0;
console.error('Error fetching from myTischtennis:', error);
}
// Log external request to myTischtennis - IMMER, auch bei Fehlern
try {
await apiLogService.logRequest({
userId: account.userId,
method: 'GET',
path: playerStatsUrl.replace('https://www.mytischtennis.de', ''), // Relative path for consistency
statusCode: responseStatus || (fetchError ? 0 : 200),
requestBody: JSON.stringify({
url: playerStatsUrl,
myTischtennisTeamId: team.myTischtennisTeamId,
clubTeamId: team.id,
teamName: team.name,
association: league.association,
groupname: league.groupname,
groupId: league.myTischtennisGroupId
}),
responseBody: responseBodyData ? (typeof responseBodyData === 'string' ? responseBodyData.substring(0, 10000) : JSON.stringify(responseBodyData).substring(0, 10000)) : null,
executionTime: fetchExecutionTime,
errorMessage: fetchError || (playerStatsResponse && !playerStatsResponse.ok ? `myTischtennis API returned ${responseStatus}` : null),
logType: 'api_request',
schedulerJobType: 'mytischtennis_fetch'
});
} catch (logError) {
console.error('Error logging myTischtennis request:', logError);
// Don't throw - logging failures shouldn't break the main operation
}
// Re-throw error if fetch failed
if (fetchError) {
throw new Error(`Failed to fetch from myTischtennis: ${fetchError}`);
}
// Note: Match results are already included in the player stats response above

View File

@@ -7,6 +7,15 @@
<div class="filters-section">
<div class="filter-controls">
<div class="filter-group">
<label>Backend:</label>
<select v-model="filters.backend" class="filter-select">
<option value="">Alle</option>
<option value="mytischtennis">myTischtennis</option>
<option value="own">Eigenes Backend</option>
</select>
</div>
<div class="filter-group">
<label>Log-Typ:</label>
<select v-model="filters.logType" class="filter-select">
@@ -202,6 +211,7 @@ export default {
const lastLoadTime = ref(null);
const filters = ref({
backend: '',
logType: '',
method: '',
statusCode: '',
@@ -232,6 +242,18 @@ export default {
...filters.value
};
// Convert backend filter to path filter
if (params.backend === 'mytischtennis') {
params.path = '/mytischtennis';
delete params.backend;
} else if (params.backend === 'own') {
// Exclude myTischtennis paths
params.path = 'NOT:/mytischtennis';
delete params.backend;
} else {
delete params.backend;
}
// Remove empty filters
Object.keys(params).forEach(key => {
if (params[key] === '' || params[key] === null) {
@@ -266,6 +288,7 @@ export default {
const clearFilters = () => {
filters.value = {
backend: '',
logType: '',
method: '',
statusCode: '',