feat: füge LiveTickerBanner-Komponente hinzu und erweitere Spielplan-Daten mit MeetingId und Live-Daten-Flag
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 6m28s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 3m17s

This commit is contained in:
Torsten Schulz (local)
2026-10-22 23:41:33 +02:00
parent 3dc293517f
commit 6b9198ac41
7 changed files with 143 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
import { getCurrentSeasonSlug, readSpielplanData } from './spielplan-data.js'
const TICKER_WINDOW_BEFORE_MS = 30 * 60 * 1000
const TICKER_WINDOW_AFTER_MS = 6 * 60 * 60 * 1000
const FINISHED_VISIBLE_MS = 60 * 60 * 1000
const cache = new Map()
function matchTime(row) {
const timestamp = Number(row.Timestamp)
return Number.isFinite(timestamp) ? timestamp * 1000 : NaN
}
function score(data) {
const home = data.is_completed ? data.matches_home : data.match?.reduce((sum, item) => sum + (Number(item.matches_home) || 0), 0)
const away = data.is_completed ? data.matches_guest : data.match?.reduce((sum, item) => sum + (Number(item.matches_guest) || 0), 0)
return `${home ?? 0}:${away ?? 0}`
}
export async function refreshLiveTicker(now = new Date()) {
const season = getCurrentSeasonSlug(now)
const { data } = await readSpielplanData({ season })
const nowMs = now.getTime()
const candidates = data.filter((row) => {
const startsAt = matchTime(row)
return Number.isFinite(startsAt) && startsAt - TICKER_WINDOW_BEFORE_MS <= nowMs && nowMs <= startsAt + TICKER_WINDOW_AFTER_MS
})
await Promise.all(candidates.map(async (row) => {
const meetingId = String(row.MeetingId || row.BegegnungNr || '').trim()
if (!/^\d{6,}$/.test(meetingId)) return
try {
const response = await fetch(`https://www.mytischtennis.de/api/meeting/${meetingId}/live`, {
headers: { accept: 'application/json', 'accept-language': 'de-DE,de;q=0.9' },
signal: AbortSignal.timeout(10_000)
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
const payload = await response.json()
const report = payload?.data
if (!report) throw new Error('Keine Spieldaten erhalten')
const completedAt = report.is_completed ? nowMs : null
const previous = cache.get(meetingId)
cache.set(meetingId, {
meetingId,
homeTeam: report.team_home || row.HeimMannschaft,
awayTeam: report.team_guest || row.GastMannschaft,
score: score(report),
startsAt: matchTime(row),
hasLiveData: report.has_nu_live_push_data === true,
completed: report.is_completed === true,
completedAt: completedAt || previous?.completedAt || null,
updatedAt: new Date(nowMs).toISOString()
})
} catch (error) {
const previous = cache.get(meetingId)
if (previous) cache.set(meetingId, { ...previous, updatedAt: previous.updatedAt, error: 'Live-Daten derzeit nicht erreichbar' })
}
}))
for (const [meetingId, entry] of cache) {
if (entry.completed && entry.completedAt && nowMs - entry.completedAt > FINISHED_VISIBLE_MS) cache.delete(meetingId)
}
return getLiveTicker(now)
}
export function getLiveTicker(now = new Date()) {
const nowMs = now.getTime()
const matches = [...cache.values()]
.filter((entry) => !entry.completed || !entry.completedAt || nowMs - entry.completedAt <= FINISHED_VISIBLE_MS)
.sort((a, b) => Number(b.hasLiveData && !b.completed) - Number(a.hasLiveData && !a.completed) || a.startsAt - b.startsAt)
return { matches, updatedAt: new Date(nowMs).toISOString() }
}