feat: add QTTR values feature to member area
- 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.
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
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 RUN_HOUR = 7
|
||||
const RUN_MINUTE = 0
|
||||
const MAX_TIMEOUT = 2_147_483_647
|
||||
|
||||
let timer = null
|
||||
let running = false
|
||||
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', {
|
||||
@@ -47,12 +51,12 @@ function zonedDateToUtc(year, month, day, hour, minute) {
|
||||
return new Date(utcGuess.getTime() - offset)
|
||||
}
|
||||
|
||||
function nextRunAt(now = new Date()) {
|
||||
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, RUN_HOUR, RUN_MINUTE)
|
||||
let candidate = zonedDateToUtc(year, month, day, hour, minute)
|
||||
|
||||
if (candidate <= now) {
|
||||
const nextDay = zonedDateToUtc(year, month, day + 1, 12, 0)
|
||||
@@ -60,65 +64,86 @@ function nextRunAt(now = new Date()) {
|
||||
year = Number(nextParts.year)
|
||||
month = Number(nextParts.month)
|
||||
day = Number(nextParts.day)
|
||||
candidate = zonedDateToUtc(year, month, day, RUN_HOUR, RUN_MINUTE)
|
||||
candidate = zonedDateToUtc(year, month, day, hour, minute)
|
||||
}
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
async function runDailyJobs(reason, skipSpielplanImport = false) {
|
||||
if (running) return
|
||||
async function runJob(job, reason) {
|
||||
if (runningJobs.has(job.label)) return
|
||||
|
||||
running = true
|
||||
runningJobs.add(job.label)
|
||||
try {
|
||||
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 })
|
||||
}
|
||||
await job.run(reason)
|
||||
} catch (error) {
|
||||
loggerError('[spielplan-import] Import fehlgeschlagen:', { error })
|
||||
loggerError(`[${job.label}] Import fehlgeschlagen:`, { error })
|
||||
} finally {
|
||||
running = false
|
||||
runningJobs.delete(job.label)
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleNext(skipSpielplanImport = false) {
|
||||
const runAt = nextRunAt()
|
||||
function scheduleNext(job) {
|
||||
const runAt = nextRunAt(job.hour, job.minute)
|
||||
const delay = Math.min(Math.max(runAt.getTime() - Date.now(), 1_000), MAX_TIMEOUT)
|
||||
|
||||
timer = setTimeout(async () => {
|
||||
await runDailyJobs('taeglicher Lauf', skipSpielplanImport)
|
||||
scheduleNext(skipSpielplanImport)
|
||||
const timer = setTimeout(async () => {
|
||||
await runJob(job, 'taeglicher Lauf')
|
||||
scheduleNext(job)
|
||||
}, delay)
|
||||
|
||||
timer.unref?.()
|
||||
loggerInfo('[spielplan-import] Naechster Lauf', { runAt: runAt.toISOString(), tz: TIME_ZONE, time: `${String(RUN_HOUR).padStart(2, '0')}:${String(RUN_MINUTE).padStart(2, '0')}` })
|
||||
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) => {
|
||||
@@ -127,13 +152,19 @@ export default defineNitroPlugin((nitroApp) => {
|
||||
loggerInfo('[spielplan-import] Import deaktiviert; Passwort-Reset-Log-Bereinigung bleibt aktiv')
|
||||
}
|
||||
|
||||
scheduleNext(skipSpielplanImport)
|
||||
const spielplanJob = createSpielplanJob(skipSpielplanImport)
|
||||
const qttrJob = createQttrJob()
|
||||
|
||||
scheduleNext({ ...JOBS[0], ...spielplanJob })
|
||||
scheduleNext({ ...JOBS[1], ...qttrJob })
|
||||
|
||||
if (process.env.SPIELPLAN_IMPORT_RUN_ON_START === 'true') {
|
||||
runDailyJobs('Startlauf', skipSpielplanImport)
|
||||
runJob({ label: 'spielplan-import', run: spielplanJob.run }, 'Startlauf')
|
||||
}
|
||||
|
||||
nitroApp.hooks.hookOnce('close', () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
for (const timer of timers.values()) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user