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:
Torsten Schulz (local)
2026-03-10 21:24:45 +01:00
parent 78f1196f0a
commit 055dbf115c
5 changed files with 596 additions and 0 deletions

View 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,
};