← back to Nationalrealestate

src/jobs/alerts_check.ts

53 lines

/**
 * Evaluates every watchlist threshold against the latest metric value for its
 * region; fires a row into alerts_log when tripped. Re-fire suppression: skips
 * if an alert for the same watchlist+metric already fired in the last 25 days
 * (monthly cadence means one alert per period, not one per run).
 * Threshold shape: {metric:'zhvi_yoy', op:'>'|'<'|'>='|'<=', value:number}
 */
import { query, pool } from '../../db/pool.ts';

const OPS: Record<string, (a: number, b: number) => boolean> = {
  '>': (a, b) => a > b,
  '<': (a, b) => a < b,
  '>=': (a, b) => a >= b,
  '<=': (a, b) => a <= b,
};

async function main() {
  const wl = await query<{ id: number; region_id: number; label: string | null; thresholds: any }>(
    `SELECT id, region_id, label, thresholds FROM watchlist WHERE thresholds IS NOT NULL`,
  );
  let fired = 0;
  for (const w of wl.rows) {
    const t = w.thresholds;
    if (!t || !t.metric || !OPS[t.op] || typeof t.value !== 'number') continue;
    const m = await query<{ value: string; period: string }>(
      `SELECT value::text AS value, to_char(period,'YYYY-MM-DD') AS period
         FROM metric_series WHERE region_id = $1 AND metric = $2
        ORDER BY period DESC LIMIT 1`,
      [w.region_id, t.metric],
    );
    if (!m.rows.length) continue;
    const observed = Number(m.rows[0].value);
    if (!OPS[t.op](observed, t.value)) continue;
    const recent = await query(
      `SELECT 1 FROM alerts_log WHERE watchlist_id = $1 AND metric = $2
        AND fired_at > NOW() - INTERVAL '25 days' LIMIT 1`,
      [w.id, t.metric],
    );
    if (recent.rows.length) continue;
    await query(
      `INSERT INTO alerts_log (watchlist_id, metric, observed, detail)
       VALUES ($1, $2, $3, $4)`,
      [w.id, t.metric, observed, JSON.stringify({ threshold: t, period: m.rows[0].period, label: w.label })],
    );
    fired++;
    console.log(`[alerts] FIRED watchlist#${w.id} ${t.metric} ${t.op} ${t.value} (observed ${observed})`);
  }
  console.log(`[alerts] checked ${wl.rows.length} watches, fired ${fired}`);
  await pool.end();
}

main().catch((e) => { console.error(e); process.exit(1); });