feat(diary): improve predefined activities UI and enhance socket event integration
- Updated UI components for managing predefined activities to enhance user experience and accessibility. - Improved socket event integration for real-time updates related to predefined activities, ensuring seamless interaction with the diary service. - Refactored related mappers to support new UI changes and maintain data consistency across the application.
This commit is contained in:
242
backend/services/clickTtHttpPageService.js
Normal file
242
backend/services/clickTtHttpPageService.js
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Service für HTTP-Aufrufe an click-TT/HTTV-Seiten mit Logging.
|
||||
* Dient zum Verständnis der Seiten-Struktur und URL-Varianten je nach Verband/Saison.
|
||||
*
|
||||
* URL-Struktur httv.click-tt.de (und andere Verbände):
|
||||
* - leaguePage: /cgi-bin/WebObjects/nuLigaTTDE.woa/wa/leaguePage?championship=HTTV+25%2F26
|
||||
* - regionMeetingFilter: /cgi-bin/WebObjects/nuLigaTTDE.woa/wa/regionMeetingFilter?championship=HTTV+25%2F26
|
||||
* - clubInfoDisplay: /cgi-bin/WebObjects/nuLigaTTDE.woa/wa/clubInfoDisplay?club=1060
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import HttpPageFetchLog from '../models/HttpPageFetchLog.js';
|
||||
import { devLog } from '../utils/logger.js';
|
||||
|
||||
/** Verband → click-TT Subdomain */
|
||||
const ASSOCIATION_TO_DOMAIN = {
|
||||
HeTTV: 'httv.click-tt.de',
|
||||
HTTV: 'httv.click-tt.de',
|
||||
RTTV: 'rttv.click-tt.de',
|
||||
WTTV: 'wttv.click-tt.de',
|
||||
TTVNw: 'ttvnw.click-tt.de',
|
||||
BTTV: 'battv.click-tt.de',
|
||||
};
|
||||
|
||||
const SNIPPET_MAX_LEN = 2000;
|
||||
|
||||
/**
|
||||
* Ermittelt die click-TT-Domain für einen Verband
|
||||
*/
|
||||
function getDomainForAssociation(association) {
|
||||
if (!association) return null;
|
||||
const key = association.replace(/\s/g, '');
|
||||
return ASSOCIATION_TO_DOMAIN[association] ?? ASSOCIATION_TO_DOMAIN[key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die leaguePage-URL für einen Verband
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.association - z.B. HeTTV, HTTV
|
||||
* @param {string} opts.championship - z.B. "HTTV 25/26" oder "K43 25/26" (Frankfurt)
|
||||
*/
|
||||
function buildLeaguePageUrl(opts) {
|
||||
const { association, championship } = opts;
|
||||
const domain = getDomainForAssociation(association) || 'httv.click-tt.de';
|
||||
const champEncoded = encodeURIComponent(championship || 'HTTV 25/26');
|
||||
return `https://${domain}/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/leaguePage?championship=${champEncoded}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die clubInfoDisplay-URL
|
||||
*/
|
||||
function buildClubInfoDisplayUrl(opts) {
|
||||
const { association, clubId } = opts;
|
||||
const domain = getDomainForAssociation(association) || 'httv.click-tt.de';
|
||||
return `https://${domain}/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/clubInfoDisplay?club=${encodeURIComponent(clubId)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Baut die regionMeetingFilter-URL
|
||||
*/
|
||||
function buildRegionMeetingFilterUrl(opts) {
|
||||
const { association, championship } = opts;
|
||||
const domain = getDomainForAssociation(association) || 'httv.click-tt.de';
|
||||
const champEncoded = encodeURIComponent(championship || 'HTTV 25/26');
|
||||
return `https://${domain}/cgi-bin/WebObjects/nuLigaTTDE.woa/wa/regionMeetingFilter?championship=${champEncoded}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert die Basis-Domain aus einer URL
|
||||
*/
|
||||
function extractBaseDomain(url) {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
return u.hostname;
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt einen Response-Snippet (gekürzt)
|
||||
*/
|
||||
function createResponseSnippet(body, maxLen = SNIPPET_MAX_LEN) {
|
||||
if (!body || typeof body !== 'string') return null;
|
||||
const trimmed = body.trim();
|
||||
if (trimmed.length <= maxLen) return trimmed;
|
||||
return trimmed.substring(0, maxLen) + '\n...[gekürzt]';
|
||||
}
|
||||
|
||||
/**
|
||||
* Führt einen HTTP-Aufruf durch und loggt das Ergebnis
|
||||
*/
|
||||
async function fetchWithLogging(options) {
|
||||
const {
|
||||
url,
|
||||
fetchType = 'unknown',
|
||||
association = null,
|
||||
championship = null,
|
||||
clubIdParam = null,
|
||||
userId = null,
|
||||
method = 'GET',
|
||||
} = options;
|
||||
|
||||
const baseDomain = extractBaseDomain(url);
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
const executionTimeMs = Date.now() - startTime;
|
||||
const contentType = response.headers.get('content-type') || null;
|
||||
const body = await response.text();
|
||||
const success = response.ok;
|
||||
const snippet = createResponseSnippet(body);
|
||||
|
||||
await HttpPageFetchLog.create({
|
||||
userId,
|
||||
fetchType,
|
||||
baseDomain,
|
||||
fullUrl: url,
|
||||
association,
|
||||
championship,
|
||||
clubIdParam,
|
||||
httpStatus: response.status,
|
||||
success,
|
||||
responseSnippet: snippet,
|
||||
contentType,
|
||||
errorMessage: success ? null : `HTTP ${response.status}: ${response.statusText}`,
|
||||
executionTimeMs,
|
||||
});
|
||||
|
||||
devLog(`[ClickTT] ${fetchType} ${url} → ${response.status} (${executionTimeMs}ms)`);
|
||||
|
||||
return {
|
||||
success,
|
||||
status: response.status,
|
||||
contentType,
|
||||
body,
|
||||
executionTimeMs,
|
||||
};
|
||||
} catch (error) {
|
||||
const executionTimeMs = Date.now() - startTime;
|
||||
|
||||
await HttpPageFetchLog.create({
|
||||
userId,
|
||||
fetchType,
|
||||
baseDomain,
|
||||
fullUrl: url,
|
||||
association,
|
||||
championship,
|
||||
clubIdParam,
|
||||
httpStatus: null,
|
||||
success: false,
|
||||
responseSnippet: null,
|
||||
contentType: null,
|
||||
errorMessage: error.message,
|
||||
executionTimeMs,
|
||||
});
|
||||
|
||||
devLog(`[ClickTT] ${fetchType} ${url} → ERROR: ${error.message}`);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft die leaguePage-Seite ab (Ligenübersicht)
|
||||
*/
|
||||
async function fetchLeaguePage(opts) {
|
||||
const { association = 'HeTTV', championship = 'HTTV 25/26', userId } = opts;
|
||||
const url = buildLeaguePageUrl({ association, championship });
|
||||
return fetchWithLogging({
|
||||
url,
|
||||
fetchType: 'leaguePage',
|
||||
association,
|
||||
championship,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft die clubInfoDisplay-Seite ab (Vereinsinfo)
|
||||
*/
|
||||
async function fetchClubInfoDisplay(opts) {
|
||||
const { association = 'HeTTV', clubId, userId } = opts;
|
||||
const url = buildClubInfoDisplayUrl({ association, clubId });
|
||||
return fetchWithLogging({
|
||||
url,
|
||||
fetchType: 'clubInfoDisplay',
|
||||
association,
|
||||
clubIdParam: String(clubId),
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft die regionMeetingFilter-Seite ab (Regionsspielplan)
|
||||
*/
|
||||
async function fetchRegionMeetingFilter(opts) {
|
||||
const { association = 'HeTTV', championship = 'HTTV 25/26', userId } = opts;
|
||||
const url = buildRegionMeetingFilterUrl({ association, championship });
|
||||
return fetchWithLogging({
|
||||
url,
|
||||
fetchType: 'regionMeetingFilter',
|
||||
association,
|
||||
championship,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft eine beliebige URL ab (für manuelle Tests)
|
||||
*/
|
||||
async function fetchArbitraryUrl(url, opts = {}) {
|
||||
const { fetchType = 'arbitrary', userId } = opts;
|
||||
return fetchWithLogging({
|
||||
url,
|
||||
fetchType,
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchLeaguePage,
|
||||
fetchClubInfoDisplay,
|
||||
fetchRegionMeetingFilter,
|
||||
fetchArbitraryUrl,
|
||||
fetchWithLogging,
|
||||
buildLeaguePageUrl,
|
||||
buildClubInfoDisplayUrl,
|
||||
buildRegionMeetingFilterUrl,
|
||||
getDomainForAssociation,
|
||||
ASSOCIATION_TO_DOMAIN,
|
||||
};
|
||||
Reference in New Issue
Block a user