Files
dashboard/sync/src/index.ts
Matthias Hochmeister 7215e7f472 update
2026-03-13 14:01:06 +01:00

136 lines
4.2 KiB
TypeScript

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<void> {
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<void> {
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);
});