- Implemented QTTR values screen in the member area with data fetching and display. - Added new API endpoint for QTTR values retrieval. - Created a new view model for managing QTTR data state. - Updated navigation to include QTTR section. - Enhanced error handling and loading states for QTTR data. - Adjusted server-side logic to import QTTR values from external source. - Updated Android app version and adjusted build configurations. - Added necessary UI components and styling for QTTR display.
171 lines
5.2 KiB
JavaScript
171 lines
5.2 KiB
JavaScript
import { importSpielplan } from '../utils/spielplan-import.js'
|
|
import { importLeagueTables } from '../utils/spielklassen-tables-import.js'
|
|
import { importQttrValues } from '../utils/qttr-import.js'
|
|
import { publishImportedSpielplan } from '../utils/spielplan-publish.js'
|
|
import { info as loggerInfo, error as loggerError } from '../utils/logger.js'
|
|
import { cleanupPasswordResetLogs } from '../utils/password-reset-log.js'
|
|
|
|
const TIME_ZONE = 'Europe/Berlin'
|
|
const MAX_TIMEOUT = 2_147_483_647
|
|
|
|
const JOBS = [
|
|
{ label: 'spielplan-import', hour: 7, minute: 0 },
|
|
{ label: 'qttr-import', hour: 7, minute: 30 }
|
|
]
|
|
|
|
const timers = new Map()
|
|
const runningJobs = new Set()
|
|
|
|
function getTimeParts(date) {
|
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: TIME_ZONE,
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit',
|
|
hour12: false
|
|
}).formatToParts(date)
|
|
|
|
return Object.fromEntries(parts.map((part) => [part.type, part.value]))
|
|
}
|
|
|
|
function getTimeZoneOffset(date) {
|
|
const parts = getTimeParts(date)
|
|
const zonedAsUtc = Date.UTC(
|
|
Number(parts.year),
|
|
Number(parts.month) - 1,
|
|
Number(parts.day),
|
|
Number(parts.hour),
|
|
Number(parts.minute),
|
|
Number(parts.second)
|
|
)
|
|
|
|
return zonedAsUtc - date.getTime()
|
|
}
|
|
|
|
function zonedDateToUtc(year, month, day, hour, minute) {
|
|
const utcGuess = new Date(Date.UTC(year, month - 1, day, hour, minute, 0))
|
|
const offset = getTimeZoneOffset(utcGuess)
|
|
return new Date(utcGuess.getTime() - offset)
|
|
}
|
|
|
|
function nextRunAt(hour, minute, now = new Date()) {
|
|
const parts = getTimeParts(now)
|
|
let year = Number(parts.year)
|
|
let month = Number(parts.month)
|
|
let day = Number(parts.day)
|
|
let candidate = zonedDateToUtc(year, month, day, hour, minute)
|
|
|
|
if (candidate <= now) {
|
|
const nextDay = zonedDateToUtc(year, month, day + 1, 12, 0)
|
|
const nextParts = getTimeParts(nextDay)
|
|
year = Number(nextParts.year)
|
|
month = Number(nextParts.month)
|
|
day = Number(nextParts.day)
|
|
candidate = zonedDateToUtc(year, month, day, hour, minute)
|
|
}
|
|
|
|
return candidate
|
|
}
|
|
|
|
async function runJob(job, reason) {
|
|
if (runningJobs.has(job.label)) return
|
|
|
|
runningJobs.add(job.label)
|
|
try {
|
|
await job.run(reason)
|
|
} catch (error) {
|
|
loggerError(`[${job.label}] Import fehlgeschlagen:`, { error })
|
|
} finally {
|
|
runningJobs.delete(job.label)
|
|
}
|
|
}
|
|
|
|
function scheduleNext(job) {
|
|
const runAt = nextRunAt(job.hour, job.minute)
|
|
const delay = Math.min(Math.max(runAt.getTime() - Date.now(), 1_000), MAX_TIMEOUT)
|
|
|
|
const timer = setTimeout(async () => {
|
|
await runJob(job, 'taeglicher Lauf')
|
|
scheduleNext(job)
|
|
}, delay)
|
|
|
|
timer.unref?.()
|
|
timers.set(job.label, timer)
|
|
loggerInfo(`[${job.label}] Naechster Lauf`, { runAt: runAt.toISOString(), tz: TIME_ZONE, time: `${String(job.hour).padStart(2, '0')}:${String(job.minute).padStart(2, '0')}` })
|
|
}
|
|
|
|
function createSpielplanJob(skipSpielplanImport) {
|
|
return {
|
|
run: async (reason) => {
|
|
try {
|
|
const cleanup = await cleanupPasswordResetLogs()
|
|
loggerInfo(`[password-reset-log] ${reason}: Bereinigung abgeschlossen`, cleanup)
|
|
} catch (error) {
|
|
loggerError('[password-reset-log] Bereinigung fehlgeschlagen:', { error })
|
|
}
|
|
|
|
if (skipSpielplanImport) {
|
|
return
|
|
}
|
|
|
|
const spielplan = await importSpielplan()
|
|
loggerInfo(`[spielplan-import] ${reason}: ${spielplan.matchCount} Spiele importiert`, { range: `${spielplan.source.season.dateStart} - ${spielplan.source.season.dateEnd}` })
|
|
|
|
const published = await publishImportedSpielplan({ inputPath: spielplan.jsonFile })
|
|
loggerInfo(`[spielplan-import] ${reason}: Spielplan publiziert`, {
|
|
season: published.seasonSlug,
|
|
internalPath: published.internalSeasonPath
|
|
})
|
|
|
|
try {
|
|
const tables = await importLeagueTables()
|
|
loggerInfo(`[spielplan-import] ${reason}: ${tables.importedCount}/${tables.teamCount} Tabellen importiert`, {
|
|
season: tables.seasonSlug,
|
|
outputFile: tables.outputFile,
|
|
errors: tables.errorCount
|
|
})
|
|
} catch (error) {
|
|
loggerError('[spielplan-import] Tabellen-Import fehlgeschlagen:', { error })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function createQttrJob() {
|
|
return {
|
|
run: async (reason) => {
|
|
const qttr = await importQttrValues()
|
|
loggerInfo(`[qttr-import] ${reason}: ${qttr.rowCount} QTTR-Werte importiert`, {
|
|
outputFile: qttr.outputFile,
|
|
tableCount: qttr.tableCount
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
export default defineNitroPlugin((nitroApp) => {
|
|
const skipSpielplanImport = process.env.SPIELPLAN_IMPORT_DISABLED === 'true'
|
|
if (skipSpielplanImport) {
|
|
loggerInfo('[spielplan-import] Import deaktiviert; Passwort-Reset-Log-Bereinigung bleibt aktiv')
|
|
}
|
|
|
|
const spielplanJob = createSpielplanJob(skipSpielplanImport)
|
|
const qttrJob = createQttrJob()
|
|
|
|
scheduleNext({ ...JOBS[0], ...spielplanJob })
|
|
scheduleNext({ ...JOBS[1], ...qttrJob })
|
|
|
|
if (process.env.SPIELPLAN_IMPORT_RUN_ON_START === 'true') {
|
|
runJob({ label: 'spielplan-import', run: spielplanJob.run }, 'Startlauf')
|
|
}
|
|
|
|
nitroApp.hooks.hookOnce('close', () => {
|
|
for (const timer of timers.values()) {
|
|
clearTimeout(timer)
|
|
}
|
|
})
|
|
})
|