[object Object]

← back to Ventura Corridor

iter 156: /api/magazine/quality-flags audit + nav-pill on today.html (green ✓ when zero flags)

37c543b6995845cf654c05a3cd2ed5a2522aa6ab · 2026-05-06 19:27:14 -0700 · SteveStudio2

Files touched

Diff

commit 37c543b6995845cf654c05a3cd2ed5a2522aa6ab
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 6 19:27:14 2026 -0700

    iter 156: /api/magazine/quality-flags audit + nav-pill on today.html (green ✓ when zero flags)
---
 public/today.html   | 20 ++++++++++++++++++++
 src/server/index.ts | 45 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 65 insertions(+)

diff --git a/public/today.html b/public/today.html
index d87275f..5be098a 100644
--- a/public/today.html
+++ b/public/today.html
@@ -125,6 +125,7 @@
     <a href="/standup">🎙 standup</a>
     <a href="/cover-picker.html">⛧ cover</a>
     <a href="/coverage.html">▰ coverage</a>
+    <a href="/magazine.html?quality=1" id="nav-quality" style="color:var(--ink-mute)">🔧 quality</a>
     <a href="/crawl-derby.html">🏇 derby</a>
     <a href="/today.html" class="active">today</a>
     <span class="theme-toggle-host" style="display:inline-flex;align-items:center;margin-left:6px"></span>
@@ -705,6 +706,25 @@ async function patchInq(id, status) {
 loadInquiries();
 setInterval(loadInquiries, 60_000);
 
+async function loadQualityPill() {
+  try {
+    const d = await fetch('/api/magazine/quality-flags').then(r => r.json());
+    const a = document.getElementById('nav-quality');
+    if (!a) return;
+    if (d.count > 0) {
+      a.style.color = 'var(--metal-glow)';
+      a.innerHTML = `🔧 quality <span style="background:var(--metal);color:#0a0a0c;padding:0 6px;margin-left:4px;font-size:8px;letter-spacing:.1em;font-weight:600">${d.count}</span>`;
+      a.title = `${d.summary.thin} thin · ${d.summary.no_pull_quote} no pull-quote · ${d.summary.stale_draft} stale draft · ${d.summary.no_category} uncategorized`;
+    } else {
+      a.style.color = 'var(--green)';
+      a.textContent = '🔧 quality ✓';
+      a.title = 'all features pass quality checks';
+    }
+  } catch {}
+}
+loadQualityPill();
+setInterval(loadQualityPill, 5 * 60_000);
+
 async function loadMagActivity() {
   try {
     const d = await fetch('/api/magazine/activity?limit=12').then(r => r.json());
diff --git a/src/server/index.ts b/src/server/index.ts
index 5fbb5ee..65c5072 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1895,6 +1895,51 @@ app.get('/api/magazine/random', async (req, res) => {
   }
 });
 
+// Quality-flag audit — surfaces features with fixable issues for the curation queue.
+// Each flag is independent (a feature can have multiple). Used by /magazine.html
+// for a "🔧 quality" chip + by the standup script to count cleanup opportunities.
+app.get('/api/magazine/quality-flags', async (_req, res) => {
+  try {
+    const r = await query(`
+      SELECT mf.id, mf.headline, mf.status, mf.category_tag,
+             length(mf.editorial) AS editorial_len,
+             mf.pull_quote IS NULL OR length(mf.pull_quote) < 16 AS no_pull_quote,
+             mf.subhead IS NULL OR length(mf.subhead) < 16 AS no_subhead,
+             length(mf.editorial) < 240 AS thin,
+             (mf.status='draft' AND mf.generated_at < NOW() - INTERVAL '7 days') AS stale_draft,
+             (mf.status='reviewed' AND mf.reviewed_at < NOW() - INTERVAL '4 days') AS stale_reviewed,
+             (mf.status='published' AND mf.published_at IS NULL) AS published_no_timestamp,
+             (mf.category_tag IS NULL OR mf.category_tag IN ('misc','uncategorized','feature')) AS no_category,
+             b.name AS biz_name
+      FROM magazine_features mf
+      JOIN businesses b ON b.id = mf.business_id
+      WHERE
+        length(mf.editorial) < 240
+        OR mf.pull_quote IS NULL OR length(mf.pull_quote) < 16
+        OR mf.subhead IS NULL OR length(mf.subhead) < 16
+        OR (mf.status='draft' AND mf.generated_at < NOW() - INTERVAL '7 days')
+        OR (mf.status='reviewed' AND mf.reviewed_at < NOW() - INTERVAL '4 days')
+        OR (mf.status='published' AND mf.published_at IS NULL)
+        OR mf.category_tag IS NULL OR mf.category_tag IN ('misc','uncategorized','feature')
+      ORDER BY mf.generated_at DESC
+      LIMIT 200
+    `);
+    const summary = {
+      total_flagged: r.rowCount,
+      thin: r.rows.filter((x: any) => x.thin).length,
+      no_pull_quote: r.rows.filter((x: any) => x.no_pull_quote).length,
+      no_subhead: r.rows.filter((x: any) => x.no_subhead).length,
+      stale_draft: r.rows.filter((x: any) => x.stale_draft).length,
+      stale_reviewed: r.rows.filter((x: any) => x.stale_reviewed).length,
+      no_category: r.rows.filter((x: any) => x.no_category).length,
+      published_no_timestamp: r.rows.filter((x: any) => x.published_no_timestamp).length,
+    };
+    res.json({ count: r.rowCount, summary, rows: r.rows });
+  } catch (e: any) {
+    res.status(500).json({ error: e.message });
+  }
+});
+
 // Coverage report — what % of corridor businesses have a feature yet, broken down
 // by vertical and city. Used for /coverage.html admin viewer + standup metrics.
 app.get('/api/coverage', async (_req, res) => {

← 1feb0f9 iter 155: /coverage.html + /api/coverage — corridor coverage  ·  back to Ventura Corridor  ·  iter 157: /api/magazine/feature-of-the-month — manual cover 8fee706 →