← 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
M public/today.htmlM src/server/index.ts
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 →