[object Object]

← back to Ventura Corridor

diag(signal): log timestamp+pid+ppid+uptime on SIGINT/SIGTERM/SIGHUP/SIGUSR2 to identify mystery restart source (53 restarts in stale counter, hawk logs empty so unclear culprit)

f3c43c00cde2f08c4a4424f98bc0d3722d8ad43c · 2026-05-06 11:50:29 -0700 · SteveStudio2

Files touched

Diff

commit f3c43c00cde2f08c4a4424f98bc0d3722d8ad43c
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Wed May 6 11:50:29 2026 -0700

    diag(signal): log timestamp+pid+ppid+uptime on SIGINT/SIGTERM/SIGHUP/SIGUSR2 to identify mystery restart source (53 restarts in stale counter, hawk logs empty so unclear culprit)
---
 src/server/index.ts | 42 +++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 41 insertions(+), 1 deletion(-)

diff --git a/src/server/index.ts b/src/server/index.ts
index 2979d14..561a3fa 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -17,9 +17,14 @@ process.on('unhandledRejection', (reason) => {
   console.error('[ventura-corridor] unhandledRejection:', reason);
   process.exit(1);
 });
+// Forensic logging — when a SIGINT/SIGTERM arrives, capture who/when so we can
+// trace which watchdog or external process is restarting us. ppid identifies
+// the killer (pm2 daemon, shell, hawk script, etc.).
+const STARTED_AT = Date.now();
 for (const sig of ['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGUSR2'] as const) {
   process.on(sig, () => {
-    console.error(`[ventura-corridor] received ${sig} — exiting cleanly`);
+    const uptimeS = ((Date.now() - STARTED_AT) / 1000).toFixed(1);
+    console.error(`[ventura-corridor] [${new Date().toISOString()}] received ${sig} — pid=${process.pid} ppid=${process.ppid} uptime=${uptimeS}s — exiting cleanly`);
     process.exit(0);
   });
 }
@@ -61,6 +66,7 @@ const ADMIN_PATHS = [
   /^\/responses(\.html)?\/?$/i,
   /^\/api\/responses(\/.*)?$/i,
   /^\/api\/followups\/?$/i,
+  /^\/api\/snapshots\/?$/i,
 ];
 app.use((req, res, next) => {
   if (!ADMIN_PATHS.some((re) => re.test(req.path))) return next();
@@ -879,6 +885,40 @@ app.patch('/api/pitches/:id', async (req, res) => {
   }
 });
 
+// ─── Daily-snapshot trend feed ──────────────────────────────────────
+// Powers the trend chart on /today.html. Returns last N days of pitch state.
+app.get('/api/snapshots', async (req, res) => {
+  try {
+    const days = Math.min(Math.max(parseInt(String(req.query.days ?? '14'), 10) || 14, 1), 90);
+    const dim = String(req.query.dim || 'totals');
+    if (!['status', 'outreach_channel', 'dw_proximity', 'totals'].includes(dim)) {
+      return res.status(400).json({ error: 'dim must be status|outreach_channel|dw_proximity|totals' });
+    }
+    const r = await query(
+      `
+      SELECT snap_date, bucket, count
+      FROM pitch_daily_snapshot
+      WHERE dim = $1 AND snap_date >= CURRENT_DATE - $2::int
+      ORDER BY snap_date ASC, bucket ASC
+      `,
+      [dim, days]
+    );
+    // Pivot into { dates: [...], series: { bucket: [counts...] } }
+    const dates = Array.from(new Set(r.rows.map((x: any) => x.snap_date.toISOString().slice(0, 10)))).sort();
+    const buckets = Array.from(new Set(r.rows.map((x: any) => x.bucket)));
+    const series: Record<string, number[]> = {};
+    for (const b of buckets) series[b] = dates.map(() => 0);
+    for (const row of r.rows) {
+      const d = row.snap_date.toISOString().slice(0, 10);
+      const i = dates.indexOf(d);
+      series[row.bucket][i] = row.count;
+    }
+    res.json({ dim, days, dates, series });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // ─── Responses: who replied, who won, who lost, scheduled follow-ups ─
 // Powers /responses.html.
 app.get('/api/responses/funnel', async (_req, res) => {

← ce21119 iter 49: response tracking — migration 010 adds reply_text/r  ·  back to Ventura Corridor  ·  iter 50: daily corridor activity snapshot + 14-day velocity 368cc0c →