import 'dotenv/config'; import * as http from 'http'; import { Pool } from 'pg'; import { scrapeAll } from './scraper'; import { syncToDatabase } from './db'; // In-memory log ring buffer — last 500 lines captured from all modules const LOG_BUFFER_MAX = 500; const logBuffer: Array<{ ts: string; line: string }> = []; const _origLog = console.log; const _origErr = console.error; function captureToBuffer(line: string) { logBuffer.push({ ts: new Date().toISOString(), line }); if (logBuffer.length > LOG_BUFFER_MAX) logBuffer.shift(); } console.log = (...args: unknown[]) => { const line = args.map(String).join(' '); _origLog(line); captureToBuffer(line); }; console.error = (...args: unknown[]) => { const line = args.map(String).join(' '); _origErr(line); captureToBuffer(line); }; function log(msg: string) { console.log(`[sync] ${new Date().toISOString()} ${msg}`); } let syncRunning = false; function requireEnv(name: string): string { const val = process.env[name]; if (!val) throw new Error(`Missing required environment variable: ${name}`); return val; } /** Returns milliseconds until the next midnight (00:00:00) in local time. */ function msUntilMidnight(): number { const now = new Date(); const midnight = new Date(now); midnight.setDate(now.getDate() + 1); midnight.setHours(0, 0, 0, 0); return midnight.getTime() - now.getTime(); } async function runSync(force = false): Promise { if (syncRunning) { log('Sync already in progress, skipping'); return; } syncRunning = true; const username = requireEnv('FDISK_USERNAME'); const password = requireEnv('FDISK_PASSWORD'); const pool = new Pool({ host: requireEnv('DB_HOST'), port: parseInt(process.env.DB_PORT ?? '5432'), database: requireEnv('DB_NAME'), user: requireEnv('DB_USER'), password: requireEnv('DB_PASSWORD'), }); try { if (force) log('Force mode: ON'); log('Starting FDISK sync'); const { members, ausbildungen } = await scrapeAll(username, password); await syncToDatabase(pool, members, ausbildungen, force); log(`Sync complete — ${members.length} members, ${ausbildungen.length} Ausbildungen`); } finally { syncRunning = false; await pool.end(); } } function startHttpServer(port: number) { const server = http.createServer((req, res) => { res.setHeader('Content-Type', 'application/json'); if (req.method === 'GET' && req.url === '/logs') { res.writeHead(200); res.end(JSON.stringify({ running: syncRunning, logs: logBuffer })); } else if (req.method === 'POST' && req.url === '/trigger') { if (syncRunning) { res.writeHead(409); res.end(JSON.stringify({ running: true, message: 'Sync already in progress' })); return; } let body = ''; req.on('data', (chunk: Buffer) => { body += chunk.toString(); }); req.on('end', () => { let force = false; try { const parsed = JSON.parse(body); force = parsed?.force === true; } catch { // no body or invalid JSON — force stays false } res.writeHead(200); res.end(JSON.stringify({ started: true, force })); runSync(force).catch(err => log(`ERROR during manual sync: ${err.message}`)); }); } else { res.writeHead(404); res.end(JSON.stringify({ message: 'Not found' })); } }); server.listen(port, () => log(`HTTP control server listening on port ${port}`)); } async function main(): Promise { log('FDISK sync service started'); const httpPort = parseInt(process.env.SYNC_HTTP_PORT ?? '3001', 10); startHttpServer(httpPort); // Run once immediately on startup so the first sync doesn't wait until midnight await runSync().catch(err => log(`ERROR during initial sync: ${err.message}`)); // Then schedule at midnight every day while (true) { const delay = msUntilMidnight(); const nextRun = new Date(Date.now() + delay); log(`Next sync scheduled at ${nextRun.toLocaleString()} (in ${Math.round(delay / 60000)} min)`); await new Promise(r => setTimeout(r, delay)); await runSync().catch(err => log(`ERROR during scheduled sync: ${err.message}`)); } } main().catch(err => { console.error(`[sync] Fatal error: ${err.message}`); process.exit(1); });