Files
ui-tracker/backend/src/scheduler.ts
T
2026-03-27 23:33:31 -05:00

86 lines
2.9 KiB
TypeScript

import { getDb, WatchedItem } from './database';
import { checkStockStatus } from './scraper';
import { sendTelegramAlert } from './telegram';
const MIN_INTERVAL_SECONDS = 30;
const timers = new Map<number, NodeJS.Timeout>();
async function runCheck(itemId: number): Promise<void> {
const db = getDb();
const item = db.prepare('SELECT * FROM watched_items WHERE id = ?').get(itemId) as WatchedItem | undefined;
if (!item || !item.is_active) return;
console.log(`[Scheduler] Checking item ${itemId}${item.name || item.url}`);
try {
const result = await checkStockStatus(item.url);
const now = new Date().toISOString();
let alertSent = item.alert_sent;
// Fire alert only on transition to in_stock when no alert has been sent yet
if (result.status === 'in_stock' && !item.alert_sent) {
const displayName = result.name || item.name || item.url;
await sendTelegramAlert(
`🟢 <b>Back in Stock!</b>\n\n` +
`<b>${displayName}</b>\n\n` +
`<a href="${item.url}">Open in Store →</a>`
);
alertSent = 1;
}
// Build update payload
const updates: Record<string, string | number> = {
last_status: result.status,
alert_sent: alertSent,
check_count: item.check_count + 1,
last_checked_at: now,
};
// Populate name and thumbnail on first successful scrape
if (result.name && !item.name) updates.name = result.name;
if (result.thumbnail && !item.thumbnail_url) updates.thumbnail_url = result.thumbnail;
const setClause = Object.keys(updates).map(k => `${k} = @${k}`).join(', ');
db.prepare(`UPDATE watched_items SET ${setClause} WHERE id = @id`).run({ ...updates, id: itemId });
console.log(`[Scheduler] Item ${itemId}${result.status} (check #${item.check_count + 1})`);
} catch (err) {
console.error(`[Scheduler] Error checking item ${itemId}:`, err);
// Still increment count so the UI shows activity
db.prepare('UPDATE watched_items SET check_count = check_count + 1, last_checked_at = ? WHERE id = ?')
.run(new Date().toISOString(), itemId);
}
}
export function startItem(item: WatchedItem): void {
stopItem(item.id);
const intervalMs = Math.max(MIN_INTERVAL_SECONDS, item.check_interval) * 1000;
const timer = setInterval(() => runCheck(item.id), intervalMs);
timers.set(item.id, timer);
console.log(`[Scheduler] Started item ${item.id} — interval ${intervalMs / 1000}s`);
}
export function stopItem(id: number): void {
const timer = timers.get(id);
if (timer) {
clearInterval(timer);
timers.delete(id);
console.log(`[Scheduler] Stopped item ${id}`);
}
}
export function initScheduler(): void {
const db = getDb();
const activeItems = db.prepare('SELECT * FROM watched_items WHERE is_active = 1').all() as WatchedItem[];
for (const item of activeItems) {
startItem(item);
}
console.log(`[Scheduler] Initialized — ${activeItems.length} active item(s)`);
}