137 lines
4.5 KiB
TypeScript
137 lines
4.5 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, befoerderungen, untersuchungen, fahrgenehmigungen } = await scrapeAll(username, password);
|
|
await syncToDatabase(pool, members, ausbildungen, befoerderungen, untersuchungen, fahrgenehmigungen, force);
|
|
log(`Sync complete — ${members.length} members, ${ausbildungen.length} Ausbildungen, ${befoerderungen.length} Beförderungen, ${untersuchungen.length} Untersuchungen, ${fahrgenehmigungen.length} Fahrgenehmigungen`);
|
|
} 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);
|
|
});
|