[object Object]

← back to Ventura Corridor

iter 143: feedback POST → George email Steve on typo/correction/tip/complaint (skip praise/note)

d1f488369f003f835635a8194bb7b51d8873798f · 2026-05-06 19:06:00 -0700 · SteveStudio2

Files touched

Diff

commit d1f488369f003f835635a8194bb7b51d8873798f
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 6 19:06:00 2026 -0700

    iter 143: feedback POST → George email Steve on typo/correction/tip/complaint (skip praise/note)
---
 src/server/index.ts | 48 +++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 47 insertions(+), 1 deletion(-)

diff --git a/src/server/index.ts b/src/server/index.ts
index f26a0ad..aad119d 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -3104,6 +3104,7 @@ ${b.notes ? `<div style="margin-top:24px;padding:14px 18px;background:rgba(184,1
 });
 
 // Reader feedback intake — typo reports, business-owner corrections, story tips.
+// Inserts to PG + fires a George Gmail ping so Steve sees it inbox-side.
 app.post('/api/feedback', express.json(), async (req, res) => {
   try {
     const b = req.body || {};
@@ -3116,7 +3117,52 @@ app.post('/api/feedback', express.json(), async (req, res) => {
        VALUES ($1, $2, $3, $4, $5) RETURNING id`,
       [b.feature_id || null, kind, b.body, b.contact || null, b.source_path || null]
     );
-    res.json({ ok: true, id: r.rows[0].id });
+    const fbId = r.rows[0].id;
+
+    // Notify Steve only for real correction-class kinds; skip pure "praise"/"note" to avoid inbox noise.
+    const NOTIFY_KINDS = new Set(['typo', 'correction', 'tip', 'complaint']);
+    if (NOTIFY_KINDS.has(kind)) {
+      // Look up the feature for context (best-effort; never block on this)
+      let context: { headline?: string; biz?: string } = {};
+      if (b.feature_id) {
+        try {
+          const c = await query(
+            `SELECT mf.headline, b.name AS biz FROM magazine_features mf JOIN businesses b ON b.id = mf.business_id WHERE mf.id = $1`,
+            [b.feature_id]
+          );
+          if (c.rowCount) context = c.rows[0];
+        } catch {}
+      }
+      const escEm = (s: any) => String(s ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]!));
+      const html = `<div style="font-family:Georgia,serif;max-width:560px;color:#1a1a1a;padding:24px;background:#faf6ee">
+<h2 style="font-family:Georgia,serif;font-style:italic;font-size:26px;color:#6a3a1a;margin:0 0 8px">Reader ${escEm(kind)}</h2>
+<div style="font-size:11px;letter-spacing:.3em;text-transform:uppercase;color:#8a6d3b;margin-bottom:20px">The Corridor · feedback #${fbId} · ${new Date().toLocaleString('en-US',{dateStyle:'long',timeStyle:'short'})}</div>
+<table style="border-collapse:collapse;font-size:14px;margin-bottom:20px">
+${context.headline ? `<tr><td style="padding:6px 14px 6px 0;color:#888;width:120px">Feature</td><td><a href="http://127.0.0.1:9780/magazine/${escEm(b.feature_id)}">${escEm(context.headline)}</a></td></tr>` : ''}
+${context.biz ? `<tr><td style="padding:6px 14px 6px 0;color:#888">Business</td><td>${escEm(context.biz)}</td></tr>` : ''}
+${b.contact ? `<tr><td style="padding:6px 14px 6px 0;color:#888">Reader</td><td><a href="mailto:${escEm(b.contact)}">${escEm(b.contact)}</a></td></tr>` : '<tr><td style="padding:6px 14px 6px 0;color:#888">Reader</td><td><i>anonymous</i></td></tr>'}
+${b.source_path ? `<tr><td style="padding:6px 14px 6px 0;color:#888">From</td><td><code style="font-size:12px;color:#6e6356">${escEm(b.source_path)}</code></td></tr>` : ''}
+</table>
+<div style="padding:14px 18px;background:rgba(184,153,104,0.1);border-left:3px solid #8a6d3b;font-style:italic;font-size:15px;line-height:1.5">${escEm(b.body)}</div>
+<div style="margin-top:24px;font-size:11px;color:#888">Triage at <a href="http://127.0.0.1:9780/feedback.html">/feedback.html</a> · mark reviewed when handled.</div>
+</div>`;
+      try {
+        const georgeAuth = 'Basic ' + Buffer.from('admin:DWSecure2024!').toString('base64');
+        await fetch('http://localhost:9850/api/send', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json', Authorization: georgeAuth },
+          body: JSON.stringify({
+            to: 'steve@designerwallcoverings.com',
+            subject: `🪶 Corridor reader ${kind}${context.headline ? ' · ' + context.headline.slice(0, 60) : ''}`,
+            body: html,
+            isHtml: true,
+            account: 'info'
+          })
+        });
+      } catch (e) { /* never block feedback intake on email send */ }
+    }
+
+    res.json({ ok: true, id: fbId });
   } catch (e: any) {
     res.status(500).json({ error: e.message });
   }

← 6ffa8b0 iter 141: category emoji map on /scoreboard — 36 verticals m  ·  back to Ventura Corridor  ·  iter 144: cover-image upload — multer endpoint + /covers sta 4c4257c →