Phase 5: alert subscriptions UI + multiple watchlists + queue fixes
CI / Test & Type-Check (push) Canceled after 0s

UI:
- /alerts page: event history with acknowledge, subscription create/manage with toggle
- /admin/smtp: SMTP config form (host, port, auth, test)
- Watchlist sidebar: dropdown selector for multiple watchlists, create/delete
- Sidebar: alerts count badge, SMTP link under admin
- Mobile tab nav: alerts tab added
- Client trpc.ts: all new API methods + types

Backend:
- watchlists.listByWatchlist procedure + listSymbolsByWatchlist repo fn
- yfinance min-interval 1500->2000ms to reduce Edge 429s
- Fixed e.date.slice error in yfinance-adjustments with typeof guard
- Removed defunct BITF from watchlist+queue
- Cleared 83 failed + 12 backoff queue jobs

Docs:
- FUNCTIONAL_DESIGN.md: alerts + multiple watchlists + SMTP documented
- TECH_DESIGN.md: new modules, tRPC procs, routes updated
This commit is contained in:
Investor Flow Build
2026-07-23 20:51:47 -04:00
parent f71987c119
commit 5b9f770aa4
23 changed files with 1567 additions and 146 deletions
+40
View File
@@ -66,6 +66,46 @@ const SCHEDULE_MS = 30_000;
const scheduleTimer = setInterval(() => { queue.enqueueDueSchedules().catch((e) => console.error('[schedule error]', e)); }, SCHEDULE_MS);
scheduleTimer.unref();
// Per-fetch alert check: run alongside the schedule cycle for critical alert types.
const perFetchAlertTimer = setInterval(async () => {
try {
const alerts = await runProducers(database, 'per-fetch');
if (alerts.length > 0) {
console.log(`[alert] ${alerts.length} per-fetch alert(s) created`);
const { sendAlertEmail } = await import('./services/emailAlertService.ts');
alerts.forEach((a) => sendAlertEmail(database, a).catch(() => {}));
}
} catch (e) {
console.error('[alert] per-fetch check failed:', e);
}
}, SCHEDULE_MS);
perFetchAlertTimer.unref();
// Register alert producers.
import { registerProducer, runProducers } from './alerts/producers/index.ts';
import { informedBuyProducer, informedSellProducer } from './alerts/producers/insiderProducer.ts';
import { new13daProducer } from './alerts/producers/new13daProducer.ts';
registerProducer(informedBuyProducer);
registerProducer(informedSellProducer);
registerProducer(new13daProducer);
// Batched alert check: run every 10 minutes for non-critical producers.
const ALERT_BATCH_MS = 10 * 60 * 1000;
const alertBatchTimer = setInterval(async () => {
try {
const alerts = await runProducers(database, 'batched');
if (alerts.length > 0) console.log(`[alert] ${alerts.length} batch alert(s) created`);
// Send email for each new alert.
const { sendAlertEmail } = await import('./services/emailAlertService.ts');
for (const alert of alerts) {
sendAlertEmail(database, alert).catch((e) => console.error('[alert:email] send error:', e));
}
} catch (e) {
console.error('[alert] batch check failed:', e);
}
}, ALERT_BATCH_MS);
alertBatchTimer.unref();
function readBody(req: IncomingMessage): Promise<string> {
return new Promise((resolve, reject) => {
let data = '';