Ventura Corridor

repo: ~/Projects/ventura-corridor · 192 commits · 0 in last 24h, 0 in last 7d ·

Search the build

192 commits indexed

  1. 28b0f74 2026-07-13 auto-save: 2026-07-13T10:54:28 (1 files) — scripts/push-news-snapshot.sh
  2. e4101a6 2026-07-13 move tsx to runtime dependencies — prod launches via tsx, --omit=dev deploy pruned it
  3. a49ac4f 2026-07-13 feat: explicit 'Source: <site>' credit on news + archive cards (Steve 2026-07-13: crawled items must credit the client site)
  4. d2e66de 2026-07-13 fix: Ollama fallbacks pointed at dead hosts (mac2 tailnet IP, offline Mac1 LAN) — default to local; preflight 15s->60s for cold model load
  5. 9a05162 2026-07-03 chore: macstudio3 migration — reconcile from mac2 + repoint paths (stevestudio2→macstudio3, node/npm/npx→/opt/homebrew)
  6. 238031f 2026-06-24 LinkedIn dashboard: cap table at 500 rows for snappy 2,860-record list + confirm guard on Mark All Visible
  7. ce8201c 2026-06-24 LinkedIn dashboard: add 2,591 DW trade clients as a source + source filter (Ventura/Clients)
  8. 45f5855 2026-06-24 LinkedIn dashboard: row select + paced batch-open follow + No-LinkedIn skip
  9. 10268df 2026-06-23 Add LinkedIn marketing calendar — 33 content pieces: 5 Events, 4 Newsletter issues, 8 Company posts, 8 Personal posts with full copy
  10. c6254fa 2026-06-23 Add LinkedIn activity ledger — quick-entry log with stats, filters, CSV export
  11. ff0ed97 2026-06-23 Add LinkedIn pitch library — posts, comment openers, InMails, objection responses by tier
  12. d2f7687 2026-06-23 Add Ventura Blvd LinkedIn follow dashboard + InMail templates + DB export
  13. b0d3e43 2026-06-23 auto-save: 2026-06-23T14:52:21 (2 files) — LINKEDIN_COMPLETE_MASTER_FOLLOW_LIST.md LINKEDIN_EXPANDED_CALENDAR_7TIERS.md
  14. 71bca45 2026-06-23 auto-save: 2026-06-23T14:22:11 (3 files) — LINKEDIN_3MONTH_CALENDAR.md LINKEDIN_INDUSTRY_MASTER_FOLLOW_LIST.md linkedin-3month-calendar.html
  15. f441e59 2026-06-23 auto-save: 2026-06-23T13:52:03 (3 files) — LINKEDIN_OUTREACH_TRACKER.csv LINKEDIN_WEEKLY_SCHEDULE.md linkedin-tracker-dashboard.html
  16. 021eaf4 2026-06-23 Add LinkedIn strategy index/overview (complete package ready for action)
  17. eecce2a 2026-06-23 Add LinkedIn start guide and quick-start reference (4 total strategy docs)
  18. a8515aa 2026-06-23 Add comprehensive LinkedIn strategy package: 28 accounts to follow, discovery targets, post templates, daily cadence
  19. c1039b4 2026-06-15 ad-signals: canonical-firm dedup pass (fix Panera double-listing); 482/127 -> 446/119
  20. 057d452 2026-06-14 ad-signals: read-only pass-1 ad/social tracker over crawled firm HTML (482 businesses, 127 running paid ads)
  21. 7004f12 2026-06-02 fix: gallery-pieces 3am job — require('pg') → import (package is type:module, was throwing ReferenceError nightly since May 12)
  22. b261b8f 2026-05-31 Link favicon.svg in head (use branded SVG, not .ico fallback)
  23. 6912794 2026-05-31 gitignore: add *.orig *.save *.old *~ copy-of-* backup patterns (standing-rule completeness)
  24. 19b5427 2026-05-27 chore: gitignore all .env/.bak/.plist-backups + drop tracked backup files
  25. e1392fb 2026-05-24 ventura-corridor: ship 'POSSIBLY NO BUSINESS LICENSE' badge
  26. cf9f180 2026-05-12 YOLO tick 6: qwen3 fills tier marketing_copy + /admin/upgrade-leads board
  27. e0407a5 2026-05-12 YOLO tick 5: /upgrade/:bizId tier ladder page + intent capture
  28. e648a17 2026-05-12 ventura-corridor: add /marketplace.html local listings board
  29. 4598d33 2026-05-12 YOLO tick 1: /gallery — geo-walk horizontal scroll of qwen3 art pieces
  30. f95cfa2 2026-05-12 Virtual Blvd Gallery + 3-tier business store · 3am autonomous build
  31. 0900593 2026-05-12 docs(appointments): Shopify embed snippet + copy-paste instructions
  32. b66894f 2026-05-12 feat(appointments): self-contained Smart Scheduling widget + Google Calendar OAuth
  33. 75c7314 2026-05-10 task #4: source CC0 glTF bus + car models (Kenney Car Kit + Quaternius Public Transport)
  34. ff0e4d0 2026-05-08 fix: build-status ideas_rejected wiring + collateral damage from earlier replace_all
  35. b552dfe 2026-05-08 feat(coverage): expose ideas_rejected on homepage strip alongside accepted
  36. e4ee322 2026-05-08 feat(coverage): "tonight on the corridor" glance footer
  37. be002e1 2026-05-08 fix(coverage): wire JS for rejects drawer (was empty since tick 73)
  38. 3853840 2026-05-08 fix: portable home dir in /api/ideas/{recent,rejects} (caught by Kimi K2.5)
  39. 1229e88 2026-05-08 fix: 3 bugs caught by codex-2way audit at tick 73
  40. 7d1fe85 2026-05-08 feat(coverage): rejected-ideas drawer below the idea-loop feed
  41. 4a0ab3d 2026-05-08 feat(fleet): Mac1+Mac2 dashboard bridge on corridor homepage strip
  42. 4927f54 2026-05-07 feat(news): "Hot only" toggle on /news.html filter bar
  43. df2073d 2026-05-07 feat(coverage): per-type accept-rate mini-bars in idea-loop section
  44. faa570d 2026-05-07 feat(coverage): /api/ideas/stats + accept-rate line on /coverage.html
  45. c9a00d6 2026-05-07 feat(coverage): ideas_accepted count in homepage build-status strip
  46. 8172d93 2026-05-07 feat(news): vertical filter dropdown on /news.html
  47. b8a21eb 2026-05-07 feat(news): news counts in homepage build-status strip
  48. dab6585 2026-05-07 feat(coverage): idea-loop feed bridge on /coverage.html
  49. f9ee539 2026-05-07 feat(news): /api/news/by-day + 90-day calendar heatmap on /coverage.html
  50. 4035b3c 2026-05-07 feat(news): news-recency badge on /pitches.html name column
  51. ebcb5c8 2026-05-07 feat(news): /api/news/archive + /news/archive.html weekly history view
  52. 888783b 2026-05-07 feat(news): /api/news/sources + source-domain bar chart on /coverage.html
  53. 4b74df3 2026-05-07 feat(news): summarizer pre-flight + 4-hour poll launchd
  54. 0f24227 2026-05-07 feat(news): /api/news/leaderboard + Top publishers section on /coverage.html
  55. 67f7514 2026-05-07 feat(news): recency badge (Hot/Fresh/Stale) on map detail panel
  56. 3b4d523 2026-05-07 feat(news): per-business RSS via /news/feed.xml?business=N
  57. 52178df 2026-05-07 feat(news): per-vertical RSS feeds via /news/feed.xml?vertical=...
  58. 840b1a4 2026-05-07 fix(news): hardenings caught by codex-2way round 1 audit
  59. 9027832 2026-05-07 feat(seo): include news pages in /sitemap.xml
  60. 33b09e9 2026-05-07 feat(news): "Latest from their site" section on /magazine/:id
  61. ec89429 2026-05-07 feat(news): /api/news/by-vertical + heatmap on /coverage.html
  62. 5afdd26 2026-05-07 feat(news): /business/:id/news per-business news firehose page
  63. b329ecf 2026-05-07 feat(news): RSS feed at /news/feed.xml + auto-discovery on /news.html
  64. 4cada62 2026-05-07 feat(news): news_count badge on /chains.html bar rows
  65. d444435 2026-05-07 fix(generate_features): tighten banned-phrase regex to catch bare 'X Meets Y'
  66. 7594bb3 2026-05-07 fix(security): escape source_url + scheme-validate href in /news rail (XSS)
  67. 0418331 2026-05-07 feat(news): news_count badge on /buildings.html tenant rows
  68. fd03767 2026-05-07 feat: apply_exa_websites.ts — merge Exa-discovered website JSON into businesses table w/ aggregator filter (yelp/fb/yp/ig blocked)
  69. 8afd939 2026-05-07 feat(news): per-business news rail on map detail panel + news_count column
  70. 00f39c0 2026-05-07 fix(generate_features): post-filter retry on banned headline phrases
  71. 549a1dd 2026-05-07 tighten .gitignore: add missing standing-rule patterns (dist/ build/ .next/)
  72. e7c9465 2026-05-07 fix(generate_features): forbid 'multi-tenant building' boilerplate in headlines
  73. e45334e 2026-05-07 feat(news): /this-week.html editorial digest + /api/news/digest
  74. 828ab33 2026-05-07 feat(enrich): website-discovery resilience + category prioritization
  75. 0a78f9e 2026-05-07 feat(news): nightly launchd jobs + patient summarizer backoff
  76. e8b2f73 2026-05-07 feat(news): /api/news/recent + /news.html magazine surface
  77. 2422375 2026-05-07 feat(news): per-business news/blog/press scraper + qwen3 summarizer
  78. d687733 2026-05-07 yolo enrichment + audit: parallel enrichers shipped 129 ad-signals, 127 headlines, 100 chamber-memberships, 196 businesses w/contacts, 17.4k ownership-classified, 55 confirmed paying advertisers. Plus migration 020 (pitches.website/instagram/li_connection_status) caught by Playwright tab audit. Score-headlines batch=5 (was 50, choking Mac1 Ollama)
  79. 04185a9 2026-05-07 fix(launchd): strip broken 2>&amp;1 inline redirect from 11 plists
  80. c6a6ecd 2026-05-07 feat: bulk-seed 10,050 pitches across 10 archetypes (office-tenant 5.6K, medical 1.2K, restaurant 756, law 752, real-estate 644, beauty 503, retail 456, fitness 100, dental 54, hospitality 17) + /chains.html now shows all 60 chains not just top-30
  81. ee200d8 2026-05-07 feat: migration 019 + 5 starter LinkedIn post drafts (corridor commentary x2, project reveal x2, behind-the-scenes x1)
  82. 64555b1 2026-05-07 fix: migration 018 — adds magazine_features.taps + taps_breakdown for engagement tracking
  83. d817e36 2026-05-07 feat: 9 AM daily corpus-summary email — totals + cover + top tapped + latest gen, complements 8:07 morning_feature_email
  84. e145ce4 2026-05-07 fix: rebuild ventura_corridor schema — backfill ledger 003-009, fix migration 010 (add outreach_channel + dw_proximity), add migration 017 for magazine_issues/reader_feedback/sponsor_inquiries/coverage_snapshots, fix migrate.ts to actually record schema_migrations
  85. d607f2b 2026-05-06 iter 177: SESSION_LOG addendum 2 — prompt-rewrite win confirmed (7/7 fresh headlines on new positive patterns, zero on blocked templates)
  86. a52bd71 2026-05-06 iter 176: rewrite gen prompt — 6 positive headline patterns up front (verb-led/specific-noun/sensory/contrast/time-place/fragment), negative AVOID list now secondary; killed stale gen worker so watchdog respawns w/ new prompt
  87. 5d90ce1 2026-05-06 iter 175: SESSION_LOG addendum — captures iters 171-175 + the regen demotion bug catch
  88. 02da0a2 2026-05-06 iter 174: fix /api/magazine/:id/regen — no longer demotes published→draft (was un-publishing curated features); also bake template-blocks into regen prompt for parity w/ gen
  89. 88857c9 2026-05-06 iter 173: auto-dedupe now also catches trigram-templated headlines (3+ shared 3-word phrases) on top of exact dupes
  90. 317fdcf 2026-05-06 iter 172: /api/magazine/similar/:id — pg_trgm word_similarity over first 480 chars; surfaces editorial-body templating beyond headline-level
  91. a55be3f 2026-05-06 iter 171: SESSION_LOG_2026-05-06.md — 40-iter overnight debrief in plain English
  92. 59b0a5e 2026-05-06 iter 170: /calendar.html + /api/magazine/calendar — GitHub-style 90-day heatmap of feature gen rate
  93. bfb00de 2026-05-06 iter 169: gen prompt now blocks the 6 most-overused headline templates qwen3 fell into ('X in Y's Heart', 'X Meets Y', 'Rooted in', etc.)
  94. 59418a9 2026-05-06 iter 168: auto-dedupe headlines nightly job + 3:33 AM launchd — regens up to 5 runner-ups per night, keeps oldest of each templated group
  95. 6a0afbb 2026-05-06 iter 167: /api/magazine/headline-stems + /duplicates.html templated-phrases panel (5× 'precision care encino', 5× 'legal precision meets', etc.)
  96. 1efb250 2026-05-06 iter 166: /duplicates.html admin viewer — keeps oldest, click-regen runner-ups for variety
  97. 680bcb7 2026-05-06 iter 165: /api/magazine/duplicates + nav pill — surfaces qwen3 headline-templating (5 groups affecting 14 features today)
  98. 48b3342 2026-05-06 iter 164: morning_feature_email now includes 'Suggested next publish' block — draft-pick-of-the-night
  99. 280b679 2026-05-06 iter 163: /api/magazine/draft-pick-of-the-night + 'Publish next' rail on today.html (heuristic-scored)
  100. fcadf64 2026-05-06 iter 162: /api/magazine/:id/social-pack.json — programmatic JSON twin of ad-pack.html (5 brief variants + tags + asset URLs)
  101. b39e8cf 2026-05-06 iter 161: /health.html — pipeline-wide one-page status w/ pipeline+top-tapped+growth panels, refresh 30s
  102. 4fc18b8 2026-05-06 iter 160: /issue.txt — plain-text dump of full issue (50KB, accessible/AI-ingestible)
  103. 4b6a73d 2026-05-06 iter 159: /api/magazine/gen-queue + 'Up next' rail on today.html — surfaces what watchdog will tackle
  104. 52e164c 2026-05-06 iter 158: coverage_snapshots table + nightly 11:55 PM job + /api/coverage/history + SVG growth line on /coverage.html
  105. 8fee706 2026-05-06 iter 157: /api/magazine/feature-of-the-month — manual cover wins, falls back to auto-pick heuristic
  106. 37c543b 2026-05-06 iter 156: /api/magazine/quality-flags audit + nav-pill on today.html (green ✓ when zero flags)
  107. 1feb0f9 2026-05-06 iter 155: /coverage.html + /api/coverage — corridor coverage report (0.4% so far), 14-day spark, by-vertical/by-city bars
  108. a80fd42 2026-05-06 iter 154: /magazine/:id/ad-pack.html — short/medium/long copy-ready brief variants for biz owners + reader-page link
  109. b1955e0 2026-05-06 iter 153: today.html ticker rail — '/api/magazine/activity' last-12 with icons + relative-time
  110. fa49655 2026-05-06 iter 152: /api/magazine/activity unified event feed across gen/publish/cover/feedback/inquiries
  111. 0c9c392 2026-05-06 iter 151: /quotes — typographic 3-col wall of every editorial pull quote w/ size variation
  112. 6dc50d0 2026-05-06 iter 150: /magazine.html 🆕 since-last filter — localStorage stamp + unseen-count badge
  113. f050969 2026-05-06 iter 149: per-feature editor margin notes — collapsible "add note" textarea on /magazine.html w/ blur-save
  114. a9953cd 2026-05-06 iter 148: /api/magazine/lottery?n=N&cat= — random N features for rotators/IG backup picks
  115. af14cb7 2026-05-06 iter 147: /api/magazine.json now accepts ?cat= and ?limit= for per-vertical JSON Feed consumers
  116. a4769a4 2026-05-06 iter 146: auto-cover job — promotes top-tapped feature to cover when none set; nightly 2:42 AM
  117. 3f9a762 2026-05-06 iter 145: /issue/archive — back-issues index w/ cover-image previews + Archive link in toolbar
  118. 4c4257c 2026-05-06 iter 144: cover-image upload — multer endpoint + /covers static + cover-picker.html + image-aware /issue hero
  119. d1f4883 2026-05-06 iter 143: feedback POST → George email Steve on typo/correction/tip/complaint (skip praise/note)
  120. 6ffa8b0 2026-05-06 iter 141: category emoji map on /scoreboard — 36 verticals mapped, fallback ✦
  121. 548f05d 2026-05-06 iter 140: daily-standup audio summary — Samantha TTS + m4a + /standup viewer + 7:50 AM launchd
  122. 4aa9741 2026-05-06 iter 139: /issue.epub export — pure-zip EPUB 2 with cover, TOC, 50 chapters, archiver dep
  123. 8dc6d37 2026-05-06 iter 138: monthly cover picker — magazine_issues table + GET/PATCH /api/issues + ⛧ Cover button on /magazine.html + cover hero on /issue
  124. 80f8f9f 2026-05-06 iter 137: /admin/ig-drafts mount + today.html nav link to browse generated drafts
  125. 9b315a5 2026-05-06 iter 136: launchd for ig-dryrun at 8:02 AM daily — preview tomorrow's IG draft before manual post
  126. 85d3731 2026-05-06 iter 135: IG-autopost dry-run job — writes daily JSON+HTML drafts under tmp/ig-drafts/, no posting yet
  127. f40d96e 2026-05-06 iter 134: /api/magazine/today unified payload — feature + caption + tags + share/audio for IG-autopost
  128. 97c9f08 2026-05-06 iter 133: 404 page now suggests a random published feature
  129. 030b0b1 2026-05-06 iter 131-132: /feedback.html admin viewer + today.html unread pill + PATCH endpoint
  130. 04c6a95 2026-05-06 iter 129+130: /about masthead page + /api/feedback POST + reader widget — /about.html public 'About the desk' page (Cormorant 84px italic, 'What this is' + 'How it's made' + 'Why we started' sections w/ drop-cap lede + colophon dl-grid showing editor/model/typeface/stack/source/frequency/volume + ink-on-paper contact card mailto:steve); CREATE TABLE reader_feedback (id, feature_id, kind, body, contact, source_path, received_at, reviewed); POST /api/feedback validates kind in (note/typo/correction/tip/complaint/praise) + 4-2000 char body; GET /api/feedback returns last 100 w/ feature joined; /magazine/:id reader gets a collapsible '<details> Notice a typo' feedback form at the bottom (kind dropdown + body textarea + optional email) that POSTs ajax-style and replaces with green 'Got it. Thanks for the note.' on success
  131. 5af0acf 2026-05-06 iter 127+128: /magazine/:id/print.html print-only view + sponsor-inquiry status flips — /magazine/:id/print.html renders a single-feature pure print layout (Letter @page, 0.75in margins, no toolbar, Cormorant 36pt italic headline, drop-cap lede, copper pull-quote, 2-col colophon w/ business/address/trade/issue, auto-fires window.print() on load); PATCH /api/sponsor/inquiries/:id flips status (new/responded/closed/spam); /today.html inquiry rows grow ↩ replied + ✓ done buttons + status pill (new=accent / responded=metal / closed=green / spam=mute)
  132. 26b38e1 2026-05-06 iter 125+126: /sponsor.html public landing + sponsor-inquiry persistence + /today.html inquiries card — /sponsor.html lists all 200 most-recent published features as sponsorship slots, w/ category-filter chips + 'paid-ad runners only' toggle, sponsor-candidate (paid_ads_count > 0) cards highlighted w/ accent border + ad badge; each card has 'Read draft' + '⭐ Sponsor →' CTAs; CREATE TABLE sponsor_inquiries (id, feature_id, biz_name, contact, tier, notes, received_at, status, email_message_id); POST /api/sponsor/inquiry now persists rows alongside emailing Steve via George; GET /api/sponsor/inquiries lists last 50; /today.html grows '⭐ Sponsor inquiries · last 7' card (accent-tinted bg) auto-refreshing 60s, showing tier · biz · contact · email · ts · view→ link to /magazine/:id; auto-hides when no inquiries
  133. ead33e9 2026-05-06 iter 123+124: AIFF→M4A audio conversion + scoreboard top-tap leaderboard — /api/magazine/:id/audio.(aiff|m4a|mp3) renders TTS via macOS 'say -v Samantha -r 180' and converts via afconvert (m4a, AAC 64kbps, ~10× smaller than AIFF) or lame if available (mp3); falls back to AIFF for raw if afconvert/lame missing; cached by editorial-hash + format extension; aiff stays as the source of truth, m4a/mp3 derived on demand. /api/magazine/scoreboard now also returns top_tapped (top-10 features with mf.taps > 0); /scoreboard.html grows 'Top tapped · features driving outbound clicks' board with taps_breakdown chip showing which destinations got the click (sponsor:N share:N etc)
  134. bd681e7 2026-05-06 iter 121+122: og+JSON-LD on per-vertical /issue/:cat + per-feature TTS audio — /issue/:cat now emits og:type/title/description/url/site_name + twitter:card summary_large_image (image=first feature's /share/:id) + JSON-LD CollectionPage with hasPart NewsArticle list (top 30) + RSS alternate link + JSON Feed alternate link, so per-vertical pages share-render with proper social cards. /api/magazine/:id/audio.aiff renders TTS via macOS 'say -v Samantha -r 180' from headline+subhead+editorial+pull-quote, caches by editorial-hash to data/magazine-audio/, 60s timeout, 24h browser cache; /magazine/:id reader toolbar gets 🔊 Listen link
  135. 93a2f1a 2026-05-06 iter 120: public /find — reader-facing finder by suite/street/biz-name/trade. /api/find?q= tokenizes the query into numeric tokens (street numbers, suite numbers) + alphabetic word tokens (skips stop-words like 'the/in/at/blvd/ave/suite'), scores results 1-4 (4 = address numeric match, 3 = name word match, 2 = address word match, 1 = city or NAICS word match), returns top 30 ordered by score then unfeatured-last. /find.html serves a centered Cormorant 64px italic 'Who's where?' page w/ 28px italic search input + 5 example tap-to-fill chips + result rows showing #suite + biz + headline + addr + ★ feature/building badge; clicking a result with feature → /magazine/:id, otherwise → /buildings/:bldg/issue. ADMIN_PATHS extended
  136. e2338c1 2026-05-06 iter 118+119: /magazine.html admin meta strip + /buildings/:bldg/issue per-building mini-issue — /api/magazine SELECT now includes mf.taps + mf.taps_breakdown so cards can show outbound-click counts; /magazine.html cards add a JetBrains Mono meta strip showing ⏱ generated date + 👁 views (metal) + ↗ taps (metal-glow) + Nc editorial length + model name (visible only when values > 0); GET /buildings/:bldg/issue renders a centered Cormorant 54px italic landing page for one building w/ all its featured tenants (cat tag · suite · headline-link · subhead · biz meta) above an 'Also at this address · not yet featured' tail listing un-featured tenants w/ suite numbers; /magazine/:id reader's building-mates section now includes 'Building issue ↗' link
  137. cfd5c5a 2026-05-06 iter 115+116+117: og/twitter/JSON-LD meta + /api/magazine.json + tap counter — reader page emits og:type/title/description/url/image (1080×1920 from /share/:id) + twitter:card summary_large_image + JSON-LD NewsArticle schema (author=The Corridor desk, publisher logo, articleSection, LocalBusiness about); /api/magazine.json renders JSON Feed 1.1 with id/url/title/summary/content_text/date_published/tags/image/_corridor for external rotators; magazine_features.taps + taps_breakdown JSONB columns track outbound clicks; /api/magazine/:id/tap?to=&dest= increments taps + redirects 302 to dest if provided; /magazine/:id reader toolbar 📱 share + ⭐ Sponsor links now route through tap counter for analytics
  138. 0bc6fd6 2026-05-06 iter 113+114: pitch_event_log audit panel on /magazine/:id + feature-of-the-week — reader page now appends a collapsible <details> 'Editorial dossier · N pipeline events' that lists last 10 audit events from pitch_event_log for the underlying business (timestamp · event_type · old → new) in JetBrains Mono with subtle italic footnote ('From the DW outreach pipeline that produces the magazine — every walk, every reply, every status change'); /api/magazine/feature-of-the-week picks deterministic-by-week-of-year (id*WK%count) feature requiring editorial>300 chars + status IN (published, reviewed); /today.html grows '📰 Feature of the week' compact pill below FOTD
  139. 97d5a13 2026-05-06 iter 111+112: /api/magazine/:id/embed.html iframe-friendly mini-card + ad-signal vertical heatmap on /scoreboard — embed renders 480px-max card w/ category-gradient hero + Cormorant 22px italic headline + subhead + biz/address footer + 'read more →' brand bar; X-Frame-Options:ALLOWALL so it can be iframed from Steve's other sites/blogs/IG; /api/magazine/ad-heatmap aggregates magazine_features × business_enrichment.ad_signals into per-vertical sponsor counts (google_ads, meta_pixel, tiktok_pixel, linkedin_insight, pinterest_tag) + sponsor_rate_pct; /scoreboard.html grows '$ Ad-signal heatmap by vertical' table below the 5 leaderboards (sponsor_rate>=30% glows metal-glow, individual platform counts highlighted at thresholds)
  140. 715edc1 2026-05-06 iter 109+110: building-mates on /magazine/:id reader + scoreboard CSV export — reader page now also pulls v_building_canonical for the same canonical building (strip suite from address) and renders 'In the same building · <bldg_address>' section above 'More in <category>' carousel; lists up to 8 building-mates each as either a Cormorant-italic feature link (if featured) or 'not yet featured' grayed line w/ suite (if not); /api/magazine.csv full export of every feature with id/headline/subhead/category/status/model/views/generated_pt/published_pt/editorial_len/biz/address/city/zip/paid_ads_count, RFC-safe quoting, dated filename; /scoreboard.html nav grows '⬇ CSV' button
  141. a45aad1 2026-05-06 fix: move RSS route ahead of /issue/:cat handler — Express was matching 'feed.xml' as a category and serving the HTML issue page; RSS route now defined first; /issue/:cat/feed.xml correctly returns RSS XML for vertical-specific feeds
  142. fa94a30 2026-05-06 iter 104+105+106+107+108: regen-thin job + sitemap + robots + random + RSS + friendly 404 — src/jobs/regen_thin.ts calls /api/magazine/:id/regen for any feature with editorial<200 chars (default 50/run); /sitemap.xml lists /issue + per-vertical /issue/:cat + every published /magazine/:id with lastmod; /robots.txt blocks all crawlers (loopback build); /api/magazine/random?cat= returns one published feature for fleet-tv-style rotators; /issue/feed.xml + /issue/:cat/feed.xml RSS 2.0 with content:encoded full editorial; magazine-themed 404 ('Not in this issue') with Cormorant 84px italic + suggested nav (issue/search/scoreboard/words/rates)
  143. 3bb691b 2026-05-06 iter 102+103: nightly auto-publish + /words page — src/jobs/auto_publish_top.ts picks top-N draft/reviewed features by views (default 10, min editorial 160 chars), bulk-flips status='published' + reviewed_at=NOW(); npm run magazine:auto-publish [N] wired; com.steve.ventura-corridor-auto-publish launchd loaded fires nightly 2:22 AM PT so the public issue grows without manual curation. /words renders typographic word cloud of top-100 most-used words across all editorials minus a 200-token stop-word list (corridor neighborhood names included to avoid 'ventura/sherman/oaks' dominating); top-5 are accent-colored bold (tier-1), 6-20 are metal (tier-2), 21+ are body-color faded; words sized 13-69px by frequency, opacity 0.45-1.0; toolbar links Issue + Scoreboard
  144. 18a9a29 2026-05-06 iter 101: /sponsor/:id owner-claim form — public-facing per-feature sponsorship inquiry page (Cormorant 'Make this a spread' header, feature-summary card w/ headline+biz+ad-pixel badge, 3-tier picker Spread/Cover/Ask radio cards, name/email/phone/notes form, sleek noir submit button); POST /api/sponsor/inquiry composes Georgia-serif HTML email + sends via George Gmail to steve@designerwallcoverings.com (account=info), subject '🪶 Corridor sponsor inquiry · <biz> · <tier>'; /magazine/:id reader toolbar gets ⭐ Sponsor link in copper accent; ADMIN_PATHS extended for /sponsor/:id and /api/sponsor/*
  145. b3c77b4 2026-05-06 iter 97+98+99+100: PDF export + reader-search + IG share-card + nav surfacing — /issue.pdf?cat= renders /issue or /issue/:cat via Playwright headless chromium with httpCredentials auth, prints Letter format with 0.5/0.4in margins, attachment download w/ dated filename; /search.html standalone reader-facing search w/ Cormorant 54px italic 'Search the issue' masthead + vertical nav + 24px italic input + highlight-matched substrings + 180-char snippet + per-card category + sponsor-candidate marker; /share/:id 1080×1920 9:16 IG-story share-card w/ category-gradient bg + 140px italic Cormorant headline + 42px subhead + 48px pull-quote + biz/address/city footer + The Corridor mast bottom-right (auto-scales to viewport for preview, full-size for screenshot/print); /magazine/:id reader toolbar gets '📱 IG share-card' link; /magazine.html nav grows '💵 rate card' + '🔍 search' shortcuts
  146. 07a36d7 2026-05-06 iter 94+95+96: sponsorship rate-card + author byline + monthly-issue archive — /rate-card.html public sponsorship page (Cormorant 84px italic 'Be in the issue', 3 tiers Listing/Spread/Cover $0/$240/$1800-per-issue, vs-Meta-boost comparison table, blockquote pull, mailto:steve@designerwallcoverings.com CTA, live stats from /api/magazine/stats and /api/magazine/scoreboard); /magazine/:id reader page now shows byline strip 'By The Corridor desk · drafted by qwen3:14b on Mac Studio 1' + reviewed/published timestamps (ink-mute color, monospace model code chip); /issue/:cat now also accepts /issue/YYYY-MM as a monthly archive route filtered by published_at AT TIME ZONE LA, masthead kicker switches to 'Archive · 2026-05' format
  147. 21beb48 2026-05-06 iter 93: sponsor-candidate-first generation order — generate_features.ts pickCandidates() now ORDERs by (paid_ads_count > 0 DESC, has_pitch DESC, random()) so businesses already running paid ads (proven ad buyers, monetization targets) get featured first; the next batch of overnight gens will surface sponsor-ready features for /scoreboard's 'sponsor candidates' board and the '$' badge filter on /magazine.html
  148. bdb5ece 2026-05-06 iter 91+92: related-features carousel on /magazine/:id + morning feature-of-the-day email — reader page now appends a 'More in <category>' 3-column section above the footer with random-pick same-category siblings (excludes self), each clickable to its own reader page; src/jobs/morning_feature_email.ts pulls FOTD via same DOY-deterministic SQL, builds Georgia-serif HTML email (kicker date · 36px italic headline · subhead · 200+ char editorial · pull quote · biz meta footer · admin links), POSTs to George Gmail localhost:9850 (account=info, isHtml=true) with subject 'The Corridor · <headline>'; com.steve.ventura-corridor-morning-email launchd loaded fires daily 8:07 AM PT; npm run magazine:morning-email --dry-run for preview; --dry-run flag prints HTML to stdout
  149. 082198f 2026-05-06 iter 89+90: per-vertical /issue nav strip + bulk-review-drafts action — /issue renders a centered category nav strip directly under the masthead listing each published vertical with feature count (medical 14 · professional 14 · salon 9 · hospitality 7 · real-estate 4 · fitness 1 · shop 1), active-vertical bold; /issue/:cat already supported by route now usable by readers; POST /api/magazine/review-drafts bulk-flips drafts → reviewed honoring optional cat filter and min_len gate (default 120 chars to skip thin generations); /magazine.html toolbar gets '⚙→✓ Review drafts' button (uses currently selected category chip as scope) + '★ Publish reviewed'; tested end-to-end: 50 drafts → reviewed → published in 2 clicks, /issue now shows full Volume I
  150. e6c8c01 2026-05-06 iter 87+88: stale-magazine alert + deterministic category normalizer — /api/magazine/stale?days=N returns drafts older than N days (default 7) sorted oldest-first; src/jobs/normalize_categories.ts deterministically re-maps category_tag from pitch_type using a 30-key lookup (restaurant/food-service/cafe/bar→restaurant, salon-spa/beauty-salon→salon, medical-clinic/dental/chiropractic/therapy→medical, fitness-studio/gym/yoga→fitness, law-office/accountant/cpa→professional, etc.); applied: 21 of 49 features re-categorized — fixed qwen3's over-tagging of hospitality as restaurant (1→7) and miscategorization of medical (4→14); npm run magazine:normalize
  151. c666532 2026-05-06 iter 86: nav surfacing for /issue + /scoreboard — adds links from /magazine.html and /today.html nav rails (★ public issue + 🏆 scoreboard) so the curation flow is one click away from any admin page
  152. 3f4099c 2026-05-06 iter 85: feature-of-the-day on /today.html — /api/magazine/feature-of-the-day picks deterministic-by-day-of-year feature (id*DOY%count modulo so it shifts daily but stable within the day) requiring editorial>200 chars; /today.html grows '📜 Feature of the day' card directly above the magazine progress strip showing headline (32px Cormorant italic, click-to-reader-page), subhead, first 320 chars of editorial, pull-quote callout, biz/address/city/category meta strip; auto-hides if no editorial-rich features exist yet
  153. e460f06 2026-05-06 iter 83+84: magazine watchdog launchd + scoreboard wall — src/jobs/magazine_watchdog.ts checks every 15min via com.steve.ventura-corridor-magazine-watchdog launchd whether (a) any generate_features process is alive AND (b) magazine_features count grew in last 10min; if stalled, kills the hung pid and re-spawns nohup gen 200 fresh; logs to logs/magazine-watchdog.log; npm run magazine:watchdog wired. /api/magazine/scoreboard returns 5 leaderboards (top_viewed by views, hot_verticals by feature count, latest 10 by generated_at, longest_editorial by character count, sponsor_candidates with ads_count > 0); /scoreboard.html newsroom-themed grid of 5 boards each with rank-numbered ol list, auto-refresh 60s; nav rail links today / magazine / issue / scoreboard / buildings
  154. 076d509 2026-05-06 iter 82: per-category mood gradients on /magazine.html photo-frames — replaces 'photo TBD' placeholder with 11 distinct CSS-only category gradients (restaurant=warm sunset, professional=blue-gray slate, medical=teal mint, beauty=rose plum, salon=purple, fitness=olive green, shop=copper, real-estate=ocean blue, hospitality=amber, automotive=charcoal) + decorative category glyph (🍴⚖+✦§△◊◮※⊙) at 48-60px and 18-20% opacity bottom-right; abandoned Wikimedia Commons photo search after testing — small businesses don't appear, neighborhood searches return PDFs/biological journals; CSS-only solution looks intentional and renders instantly without external API risk; cleaned 8 garbage Wikimedia URLs from DB
  155. f5e15fe 2026-05-06 iter 81: published-only public /issue page + bulk-publish action — GET /issue (also /issue/:cat for vertical-specific issues) renders status='published' features in 3-column magazine layout (Cormorant 84px italic masthead 'The Corridor', double-rule, drop-cap ledes, copper pull-quotes, sponsor-candidate badge for paid-ad runners), print stylesheet collapses to 2-col + clean spacing; floating top-right toolbar (admin/print) hidden in print; deck pulls business address + city; POST /api/magazine/publish-reviewed bulk-flips all reviewed → published in one click; /magazine.html toolbar grows '★ Publish all reviewed' button + '↗ Public issue' link in copper accent
  156. 3836ac0 2026-05-06 rename: magazine masthead The Boulevard → The Corridor — Steve's preferred title; updates /magazine.html h1, /today.html magazine card heading, /magazine/:id reader page title+breadcrumb+footer; descriptive prose about 'the boulevard' as a road kept as-is
  157. d7cd146 2026-05-06 iter 80: search box + table-of-contents view on /magazine.html — search-box input filters by name/headline/editorial/address/city/naics with 200ms debounce, '📑 Table of contents' toggle reorganizes the issue into a centered single-column TOC grouped by category_tag with alphabetized list of features inside each group, dotted-underline rows linking to /magazine/:id reader, paid-ads-runners get a copper '$' marker; reverting iter 77 dual-model approach — empirically MS1 Ollama serializes hard at NUM_PARALLEL=1 and the retry-backoff was ballooning latency to 232s/feature (vs 25-30s baseline); now single-stream qwen3:14b only, fail-fast on 503; ~120 features/hour overnight = ~1500 by morning
  158. 7037e0a 2026-05-06 iter 79: ad-signal sponsored markers — /api/magazine joins business_enrichment.ad_signals JSONB and exposes paid_ads_count per feature; /magazine.html cards get a copper-tone '$ N× ads' badge top-right of the photo-frame for businesses with detected paid-ad pixels (Google Ads/Meta/TikTok/etc), tooltip explains 'proven ad buyer · candidate sponsored placement'; new chips bar adds toggle '$ paid-ad runners only' that filters the issue to businesses already buying ads (the natural sponsored-feature targets); 128 corridor businesses have ad-signal data so far
  159. 34f78f5 2026-05-06 iter 77+78: dual-model alternating generation + /magazine/:id full-page reader — generator now rotates between qwen3:14b (~25s) and qwen3:8b (~12s) per feature for ~30% throughput gain without server-config changes; retry-with-exponential-backoff handles MS1 503 'server busy' (8 attempts, 2s base * 1.6^n + jitter); 2000-feature overnight run kicked off; /magazine/:id reader page renders one feature in full-page magazine layout (Cormorant Garamond 64px italic headline, drop-cap lede, copper-tone pull-quote, dl meta with business/address/trade/status/generated_at/model/views), back-to-issue + print + pipeline-link toolbar; views counter auto-increments on each load; click any headline on /magazine.html now opens its reader page
  160. c229baf 2026-05-06 iter 76: category filter chips on /magazine.html — /api/magazine accepts cat= query param to filter by category_tag; viewer adds 11-chip vertical-filter rail under the status chips (All / 🍴 Restaurants / ⚖️ Professional / ✨ Beauty / 💇 Salon / 🩺 Medical / 💪 Fitness / 🛍 Shops / 🏠 Real estate / 🏨 Hospitality / 🚗 Automotive); current 37 features distribute as 17 professional / 4 medical / 4 real-estate / 4 restaurant / 3 salon / 3 shop / 1 fitness / 1 beauty; click any chip filters the issue instantly
  161. e31d8bb 2026-05-06 iter 75: feature regen-via-API endpoint — POST /api/magazine/:id/regen calls Mac1 Ollama qwen3:14b directly via HTTP fetch (no shell-out, no child process), uses same SYSTEM prompt + JSON output schema as src/jobs/generate_features.ts, updates magazine_features row in place setting status='draft' and bumping generated_at; /magazine.html ↻ Regen button now calls the endpoint, shows '⟳ regenerating…' spinner state on the clicked card while waiting (~30-45s), then reloads the issue; passes feature.id (not business_id) so URL is stable; smoke-tested: feature #1 regen returned headline 'Where Modern Elegance Meets Woodland Hills Charm' in 43.7s; works without ever opening terminal
  162. 78b1bea 2026-05-06 iter 74: bulk-generate 50 features now + nightly magazine launchd — kicked off background nohup npm run magazine:gen 50 (PID 58664, ~25min for 50 features at ~25-31s/each on Mac1 Ollama qwen3:14b); com.steve.ventura-corridor-magazine-gen launchd loaded fires daily 1:32am for 20 fresh features (so the magazine grows organically overnight); /api/magazine/stats endpoint returns total/draft/reviewed/published/spiked/gen_24h + corridor_total for coverage %; /today.html grows a 📖 'The Boulevard · corridor magazine' progress card with stat strip + horizontal coverage bar (currently 6/17,462 = 0.03%) auto-refreshing every 30s; 📖 magazine link added to /today.html nav with metal-glow color
  163. 1daefa6 2026-05-06 iter 73: PIVOT to corridor magazine MVP — migration 016 adds magazine_features table (business_id UNIQUE, headline, subhead, editorial, pull_quote, category_tag, photo_url, status draft/reviewed/published/spiked, model, generated_at, reviewed_at, published_at, views, notes); src/jobs/generate_features.ts uses Mac1 Ollama qwen3:14b (per Steve's local-LLM rule) to write 80-130-word magazine features w/ JSON output (headline + subhead + editorial + pull_quote + category_tag), Conde-Nast-Traveler tone, no superlatives, lean on address as sense of place; npm run magazine:gen [N] generates N fresh; --biz=ID regenerates one specific; /api/magazine + PATCH endpoints; /magazine.html viewer w/ Cormorant Garamond serif masthead 'The Boulevard' Vol I Spring 2026, double-rule under masthead, 4-column grid of feature cards, photo-frame placeholder w/ category-tag overlay + first-letter drop cap on lede + pull-quote callouts in copper accent + per-card actions (Mark reviewed → Publish → Spike, ↻ Regen); 5 first features generated successfully (Le Petit Corner, Kelli Rust, Point to Wellness, etc.) at ~25-31s each on MS1 Ollama
  164. 67f2256 2026-05-06 iter 72: weekly digest email job — src/jobs/weekly_digest.ts pulls last-7d activity counts (sent/replied/closed/followups/touched), pipeline totals, top-5 priority bldgs, stale leads (sent>=14d no reply), channel funnel; composes HTML email with bordered tables and metal-tone branding; POSTs to George Gmail localhost:9850/api/send (account=info, isHtml=true) to steveabramsdesigns@gmail.com; --dry-run flag prints HTML to stdout instead; npm run digest wired; com.steve.ventura-corridor-digest launchd loaded fires Monday 9:03 AM PT
  165. 1482b8f 2026-05-06 iter 71: stale-pitch alerts on /today.html — /api/stale-pitches?days=&limit= returns pitches sent >N days ago with no reply, no close, status NOT IN (skip/won/lost), sorted oldest-first; days_silent computed inline; /today.html grows a 'Cold leads · re-touch or close' card (red-tinted background) below the priority strip, auto-hides when empty, shows N most-stale rows with days-silent count + biz name + pitch_type + channel + sent date + log→ shortcut to /responses.html?id=; auto-refresh every 5 min; threshold 14d default; closes the loop after iter 70's bulk-followup — Steve sees both 'who hasn't replied yet' and one-click follow-up scheduling on the same page
  166. d0c4c91 2026-05-06 iter 70: /pitches.html bulk-set-followup — extends iter 66 multi-select toolbar with ⏰ Schedule follow-up field option; selecting it hides the value-select and reveals a date input + 4 preset buttons (+3d / +1w / +2w / +1mo) that auto-populate the date; bulkApply detects next_followup_at field and uses date input value; /api/pitches/bulk allowlist extended to accept next_followup_at + followup_notes; setFollowupDate(N) helper computes ISO date N days out; tested end-to-end: walk 30 doors → tick visible rows → ⏰ + +2w → all 30 follow-ups scheduled in one click, trigger fires followup_scheduled events for each via iter-51 audit pipeline
  167. 140c1a6 2026-05-06 iter 69: BTRC data freshness pill on /today.html — /api/data-freshness reads MAX(first_seen_at) for source LIKE '%btrc%' rows, computes days_since_ingest, classifies status as fresh (≤7d) / stale (≤30d) / very_stale / never; pill in /today.html header shows colored ✓/⚠/✕ icon + days-since label + BTRC row count, hover reveals last_btrc_ingest timestamp + source URL (data.lacity.org/6rrh-rzua) + recommended npm command (ingest:la_btrc); auto-refresh every 5 min; currently shows '✓ today · 17,238 rows' in green (last ingest 2026-05-06 15:14 UTC, fresh)
  168. 43adaa8 2026-05-06 iter 68: per-vertical conversion stats — /api/responses/by-vertical aggregates pitch_type → (pitches, sent, replied, won, lost, reply_rate_pct, win_rate_pct, won_value_usd, avg_won_value); /responses.html grows a 'Conversion by vertical' table directly under the channel funnel showing 9 columns per pitch_type, reply rate >=10% colored metal-glow, win rate >=5% colored green; surfaces strategy data once Steve starts getting replies — currently 13 verticals with real-estate (1603 pitches), medical-clinic (1191), beauty-salon (919), law-office (851), hospitality (555) leading the cohort sizes
  169. eb0c998 2026-05-06 iter 67: 'Walk here next' priority strip on /today.html — top-5 walkable buildings rendered as compact cards directly under the day-hero (before velocity chart, before activity feed); each card shows order # + miles + bldg address + unpitched count + total tenants; click → /buildings.html?bldg= deep-link auto-expands the roster; DW building auto-highlights with ⚡ + metal-glow border; reads from existing /api/buildings/priority?max_miles=0.5&min_unpitched=2&limit=5; surfaces the daily action queue at the top of the home page (top 5 = 15821/15260/15456/15303/15300 with 1181 unpitched doors total)
  170. 4557347 2026-05-06 iter 66: /pitches.html multi-select checkboxes for surgical bulk actions — header gets a 'select all visible' master checkbox (with indeterminate state when partial); each row gets a checkbox cell at left; SELECTED Set tracks pitch ids in client state; clicking a row checkbox no longer expands the row (event.stopPropagation + handler-skip); bulkApply now uses SELECTED if non-empty, else falls back to 'all visible'; mass-toolbar gets Select-all-visible + Clear buttons + dynamic label that switches between 'applies to N selected' and 'applies to all N visible'; selected rows highlight with metal-tinted bg (.tr.sel); SELECTED clears after a successful bulk update
  171. 10506e0 2026-05-06 iter 65: /api/buildings.csv export + activity-feed event-type filter chips — /api/buildings.csv?min_tenants=&max_miles= returns full v_building_roster as RFC-safe CSV with bldg_address/city/zip/lat/lng/dw_miles/tenants/pitched/walked/replied/won/skipped/draft/unpitched + computed priority_score + dominant_pitch_type, Content-Disposition attachment with dated filename; /buildings.html stats strip gets ⬇ Buildings CSV button alongside the merge-sweep buttons; /today.html activity card gains 9 filter chips (All / Sent / Replied / Closed / Status / Channel / Reply / Follow-up / Created) — clicking a chip refilters via /api/activity?event_type=, active state shown with metal-tone outline + tinted bg; ADMIN_PATHS regex tightened to match /api/buildings.csv
  172. e654bdf 2026-05-06 iter 64: building pitch-type heatmap — /api/buildings/:bldg/pitch-types returns counts per pitch_type for one building (no-pitch counted as its own bucket); /buildings.html building card head gains a 'dominant' pill (italic serif) showing the most-common pitch_type from v_building_roster.dominant_pitch_type; expanded roster prepends a horizontal vertical-mix bar with per-type colors (law-office=tan, medical=teal, real-estate=blue, restaurant=red, etc.) + legend showing top 6 buckets with counts; reveals at-a-glance that 16133 Ventura is 78 law + 74 real-estate + 26 medical out of 405 tenants (224 unpitched), 15303 is law-dominant
  173. 1c3e46f 2026-05-06 iter 63: 🏇 crawl derby — racetrack-style live progress viewer for the corridor enrichment pipeline; /api/crawl/status returns 5 lanes (geocode/website/phone/audit/enrich) with done/total/pct + per-lane runtime status (idle/running/done/error); /api/crawl/start spawns the lane's npm script as a child process (safety gate: unsafe lanes like Brave-website require {force:true}); /api/crawl/stop kills it; /crawl-derby.html shows horses on a turf oval (start gate, finish line w/ checkered pattern, repeating-stripe rake marks), each lane has emoji label + pct pill + horse positioned by % across the track + galloping animation when running; roster table below with 🏇 start / ⏹ stop buttons + last-log tail; marquee announcer text changes from 'and they're at the post' to 'down the stretch they come' when any lane runs; auto-refresh 5s; derby link wired into all 8 admin nav bars
  174. 31c4243 2026-05-06 iter 62: walk-route priority-aware ordering — /api/pitches/walk-route?sort=(geographic|priority); priority sort joins through v_building_roster and orders by inline-computed priority_score (unpitched / GREATEST(miles, 0.05)), groups all doors at same building together so Steve walks highest-leverage building floor-by-floor before moving on; also filters out merged_into businesses; /walk-route.html gets a 'route order' selector at the top of the controls — Geographic (E↔W lap) vs ⚡ Priority (highest-leverage bldgs first); direction selector auto-hides when in priority mode (irrelevant); top 8 priority slots all from 15821 Ventura (score 1183, 556 unpitched doors)
  175. 40a7c21 2026-05-06 iter 61: building prioritization 'walk here next' — /api/buildings/priority?max_miles=&min_unpitched=&limit= ranks unwalked-but-walkable buildings by priority_score = unpitched × (1/miles), proximity boost so close-by small buildings beat distant towers; /buildings.html gains a hero priority section above the main list with 15-card grid (top 15 by default, radius selector 0.25/0.5/1.0/2.0 mi), each card shows order # + address + city + miles + unpitched count + score + 🚶 walk + 🏢 roster shortcuts; surfaces next-action queue (top 5 today: 15821=556, 15260=292, 15456=38, 15303=172, 15300=123 unpitched doors)
  176. 6dbbf08 2026-05-06 iter 60: recent activity feed on /today.html — /api/activity?limit=&event_type= returns last N events from pitch_event_log JOINed with pitch + business; aggregate stats: last_24h, last_7d, pitches_touched_7d, sent_7d, replied_7d; /today.html gains an Activity card directly under the velocity chart with 30 most-recent events (icon + business name + event type + old→new arrow + time-ago) auto-refreshing every 60s; click any row to open the pitch; surfaces the value of iter-51's audit trail at a glance
  177. 82a93bd 2026-05-06 iter 59: pitch 1-pager — GET /api/pitches/:id/sheet.md renders printable Markdown for any pitch (business + pitch metadata + outreach copy + LinkedIn DM + contact block + reply quote + internal notes); GET /pitch/:id wraps it in a Cormorant Garamond serif HTML viewer w/ print stylesheet + ⬇ Markdown / 🖨 Print / Edit links; /pitches.html cross-action bar gets 📄 1-pager + ⬇ .md buttons; ADMIN_PATHS extended for /pitch/:id
  178. c92b71f 2026-05-06 iter 58: /pitches.html mass-action toolbar — POST /api/pitches/bulk applies one field update to N ids (allowlist: status/outreach_channel/priority/pitch_type/notes; auto-stamps lifecycle timestamps); UI toolbar above the grid: field-picker · value-picker · Preview · ⚡ Apply; live count of visible filtered cohort; lets Steve triage 4,237 draft pitches in batches instead of one-by-one
  179. ae06419 2026-05-06 iter 56+57: same-tenant merge/dismiss + bulk-merge sweep — POST /api/buildings/dupes/{merge,dismiss,bulk-merge}; bulk-merge supports min_confidence + bldg_address scope + dry_run, picks pitch-bearing biz as primary, chains-aware (skips pairs touching already-merged ids); /buildings.html dupe rows get ✓ Merge / ✕ Dismiss buttons + corridor-wide Preview · merge all 1.00 / ⚡ Sweep merge 1.00 buttons in stats strip; 150 confidence=1.00 pairs ready to bulk-collapse
  180. db3c21d 2026-05-06 iter 56: same-tenant merge + dismiss actions — migration 015 adds businesses.merged_into FK + merged_at timestamp + partial idx; dupe_pair_decisions table tracks merge/dismiss decisions; v_building_canonical now filters merged_into IS NULL so consolidated dupes drop from roster; POST /api/buildings/dupes/merge picks primary (the one with a pitch wins, lower id tiebreak), moves any pitch from secondary to primary (or merges notes if both have pitches), stamps merged_into, refreshes mv_building_dupes; POST /api/buildings/dupes/dismiss for false positives (pair stays separate, just hides from dupe-list); /api/buildings/:bldg/dupes excludes already-decided pairs; /api/buildings/dupe-stats also reports already_merged + already_dismissed counts; /buildings.html roster now shows full dupe-pair cards above tenant table with side-by-side names + suites + match-type + confidence + ✓ Merge / ✕ Dismiss buttons; collapsed pairs fade with confirmation strip
  181. 65148eb 2026-05-06 iter 55: same-tenant detection — migration 014 adds pg_trgm, v_business_normalized (strips LLC/INC/CORP suffixes, normalizes phone digits, extracts bare website domain), mv_building_dupes materialized view that finds within-building entity pairs via 3 heuristics (name similarity > 0.5, shared phone, shared website); 1077 pair candidates flagged across 238 buildings; refresh_building_dupes() function; /api/buildings/dupe-stats (cheap count) + /api/buildings/refresh-dupes (full recompute) + /api/buildings/:bldg/dupes; /buildings.html shows corridor-wide dupe count in stats strip (clickable to refresh) + 🔗 dupe badges on tenant rows with hover-tooltip listing partners + dupe-pair note above tenant table; surfaces same-firm-two-suites cases like RAYMOND JAMES FINANCIAL (700 + 530) and INC-vs-LLP shells
  182. 8620d26 2026-05-06 iter 54: cross-surface deep-linking — /pitches.html expanded detail panel gets 5 cross-action buttons (📮 Postcard preview, 🏢 Building roster, 📝 Log response, 🚶 Walk route, 📜 Timeline JSON); /postcards.html accepts ?id= URL param + falls back to /api/pitches?id= when pitch isn't in postcard cohort; /buildings.html accepts ?bldg= param + auto-expands matching card with smooth scroll; /responses.html accepts ?id= param + auto-opens edit modal after 250ms; /walk-route.html accepts ?focus=<pitchId> + scrolls and flashes matching tenant row, widens cohort if needed
  183. 410b896 2026-05-06 iter 53: building 360° tenant roster — migration 013 adds v_building_canonical (strips SUITE/STE/UNIT/# from address to canonical bldg_address) + v_building_roster (groups corridor businesses by building, computes pitched/walked/replied/won/skipped/unpitched buckets + dw_miles distance from 15442 Ventura, filtered to LA-basin coords to avoid bad geocoded outliers); /api/buildings (filterable by min_tenants, sortable by distance/tenants/pitched_pct/walked) + /api/buildings/:bldg returns full tenant roster with pitch state; /buildings.html admin viewer with filter bar, summary stats, expandable building cards showing 5-segment progress bar (walked/replied/pitched/skipped/unpitched), DW building auto-highlighted with metal-glow + ⚡, click-to-expand tenant table with status pills + suite + NAICS + pitch detail link; buildings link wired into all 7 admin nav bars
  184. c5a0c97 2026-05-06 fix(ops): rewrite hawk to query pm2 status not http — only restart when status!=online; restores correct watchdog semantics modeled on sf-hawk; export PATH so node is reachable under launchd's minimal env
  185. dbf656b 2026-05-06 iter 52: postcard preview + Lob template generator — public/postcard-template-front.html (4×6 noir front w/ DW brand, hero serif, signature, mergefield footer) + public/postcard-template-back.html (left half message zone, right half indicia + Lob-spec address block at 1.125" from bottom + 0.5" from right) ready to upload to Lob; /api/pitches/:id/postcard-preview/(front|back).html injects real PG data; /postcards.html admin viewer with side-by-side iframe preview + pitch dropdown + manual ID input + random-pitch + Lob CSV download + per-postcard cost calc + 6-step Lob upload guide; /api/pitches accepts channel/id/limit query params; postcards link wired into all 6 admin nav bars; ADMIN_PATHS regex covers /postcards
  186. 69ba8fb 2026-05-06 iter 51: pitch_event_log audit trail + responses CSV export + cycle-time analytics — migration 012 adds pitch_event_log table + AFTER INSERT/UPDATE trigger that captures status_change/channel_change/sent/replied/closed/reply_text/followup_scheduled/created events; v_pitch_cycle_time view; /api/pitches/:id/timeline endpoint; /api/responses.csv (Content-Disposition attachment, PT timezone, CSV-safe escapes); /api/responses/cycle-time aggregates avg/median hours-to-reply + days-to-close per channel; /responses.html gets Download-CSV pill + collapsible Lifecycle-timeline section in edit modal; ADMIN_PATHS regex tightened to match /api/responses.csv
  187. 368cc0c 2026-05-06 iter 50: daily corridor activity snapshot + 14-day velocity chart on /today.html — migration 011 adds pitch_daily_snapshot table (snap_date,dim,bucket,count) + v_pitch_today_status helper; src/jobs/daily_snapshot.ts UPSERTs today's distribution by status/outreach_channel/dw_proximity/totals; npm run snapshot wired; com.steve.ventura-corridor-snapshot launchd job runs nightly at 23:57; backfilled 7 days for chart baseline; /api/snapshots?dim=&days= endpoint pivots into {dates, series}; SVG line chart on /today.html plots sent/replied/walked/won w/ dotted-line for won + delta-7 stat strip
  188. f3c43c0 2026-05-06 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)
  189. ce21119 2026-05-06 iter 49: response tracking — migration 010 adds reply_text/reply_channel/won_value_usd/lost_reason/next_followup_at/followup_notes; v_response_funnel + v_followups_due views; /responses.html viewer with funnel stats per channel, response list with edit modal (status/reply text/won $/lost reason/follow-up date), follow-ups-due section; PATCH endpoint extended + auto-backfills sent_at when reply lands so funnel math holds
  190. 36dcc74 2026-05-06 iter 48: ship /walk-route.html — printable door-by-door route with stop grouping, walk-up scripts, hangtag templates, in-place 'mark walked' status updates, and clean print stylesheet
  191. b340c7a 2026-05-05 Ship v1 public · UI-agent fixes + screenshot billboards + wall + cam tune
  192. 91c0ed0 2026-05-05 Initial commit · Ventura corridor v1 · 224 businesses · 113 audits · 3 surfaces (2D/3D/wall)

Authors

Agents used

  • debate-team-fast1

Skills used

  • /magazine81
  • /news70
  • /buildings34
  • /issue30
  • /coverage22
  • /today21
  • /pitches18
  • /feed15
  • /business14
  • /index13
  • /scoreboard12
  • /upgrade11
  • /ideas10
  • /jobs10
  • /sponsor9
  • /responses9
  • /address8
  • /gallery7
  • /upgrade-leads6
  • /marketplace6
  • /stats6
  • /appointments6
  • /status6
  • /recent6
  • /replied6
  • /this-week5
  • /chains5
  • /feedback5
  • /city5
  • /won5

Creative ideas + design notes

Commits with substantial prose (≥120 chars) — the rationale behind each move.

d2f7687 · 2026-06-23 · Add Ventura Blvd LinkedIn follow dashboard + InMail templates + DB export
- exports/linkedin-follow-targets.csv: 269 Ventura Blvd businesses (NAICS 541400/541410/541310/541320/531210/236200/236100/237000) from ventura_corridor DB
- linkedin-ventura-follow-dashboard.html: standalone follow tracker — dark navy/gold DW branding, tier badges, filter pills, search, localStorage checkbox persistence, LinkedIn search URL generation, progress bar, export-unfollowed CSV
- LINKEDIN_VENTURA_DW_INMAILS.md: 4 DW-focused InMail templates (Interior Designer, Architect, Facility Manager, Design/Build) with warm-up comment examples and tracking guide

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
c1039b4 · 2026-06-15 · ad-signals: canonical-firm dedup pass (fix Panera double-listing); 482/127 -> 446/119
Root cause: pass-1 analyzer keyed firms by raw/firm-*.html file, so multi-location
chains (Panera 2x, CVS 3x, etc.) counted once per Ventura Blvd storefront.
New ad_signals_dedup.cjs collapses by registrable-domain (name/file fallback),
drops 5 failed-crawl interstitials, and guards against domain mis-mappings.
Read-only over the existing artifact; original 06-14 files untouched.
7004f12 · 2026-06-02 · fix: gallery-pieces 3am job — require('pg') → import (package is type:module, was throwing ReferenceError nightly since May 12)
The launchd com.steve.ventura-gallery-3am step 3 (generate-gallery-pieces.js) crashed
immediately on 'require is not defined in ES module scope'. Converted to ESM import;
keeps the filename so the plist reference is unaffected. Writes to LOCAL ventura_corridor
DB (/tmp socket), not prod. Next 3am run will resume gallery generation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e1392fb · 2026-05-24 · ventura-corridor: ship 'POSSIBLY NO BUSINESS LICENSE' badge
3 pieces:

1. db/migrations/016_btrc_match.sql — adds has_btrc_match BOOL,
   btrc_match_score REAL, btrc_matched_id BIGINT, btrc_match_at on businesses.
   Partial index on (has_btrc_match) WHERE on_corridor AND source != 'la_btrc'.

2. src/enrich/btrc_match.ts — populates the flag. For each non-la_btrc
   corridor business with a numeric street_number, find best la_btrc match
   at same street_number ± 5 (same zip), score = addr_score*0.4 + name_trgm*0.6.
   has_btrc_match = (score >= 0.5). No candidate → false.

3. src/server/index.ts + public/index.html — surface has_btrc_match in the
   /api/businesses payload; render a red badge '⚠ POSSIBLY NO BUSINESS LICENSE'
   next to the business name when source != 'la_btrc' AND has_btrc_match = false.

Initial run on 224 OSM-sourced corridor businesses: 149 matched, 75 flagged.

Census working files at data/census/:
  - la-licenses-ventura-corridor.jsonl (17,344 licensed businesses)
  - yelp-corridor-all.jsonl (372 yelp/google enumerated across 8 zips)
  - tarzana-91356-yelp.json (34 Tarzana samples from initial probe)
  - census-corridor.jsonl (joined 406 with license_status per row)
  - match-census.py (offline cross-match script, mirrors the SQL logic)

Triggered by Steve's directive 2026-05-23 "using exa, make sure
venturacorridor has the database of ALL businesses along ventura bl using
all knowledge out there. many will not have the business license. Mention
that in red on the page."
cf9f180 · 2026-05-12 · YOLO tick 6: qwen3 fills tier marketing_copy + /admin/upgrade-leads board
scripts/fill-tier-copy.cjs — backfills marketing_copy JSONB on
tier_pricing rows where it's NULL. Per-row qwen3:14b generation:
- System prompt frames qwen3 as a Ventura Corridor copywriter with
  tone constraints (confident, friendly, no buzzwords, no AI talk)
- TIER_HINTS guide what features each tier-level (basic/featured/
  verified/spotlight) typically unlocks so copy stays consistent
- Strict-JSON output: {title, subtitle, bullets[3-6], cta}
- Idempotent — only fills NULL rows, re-run safe

Filled all 24 NULL rows in 3:14 (avg ~8s/row). Sample outputs:
- Hotel Spotlight : 'Stand out with video, priority placement, and
  exclusive booking tools to drive more guests.'
- Cafe Featured : 'Showcase your cafe with a logo, photos, and a
  special badge that makes you stand out in searches.'
- Salon Spotlight : 'Get featured with video, bookings, and priority
  placement to stand out on Ventura Boulevard.'

The /upgrade/:bizId page now hydrates with category-specific bullets
instead of the generic TIER_COPY fallback strings.

NEW admin pages, both behind the existing Basic-auth gate (ADMIN_PATHS
regex already protects /signals, /pitches, /linkedin — added matching
/admin/upgrade-leads + /api/admin/upgrade-leads):

GET /api/admin/upgrade-leads — JSON list of upgrade_intents joined
  with businesses + tier_pricing. Last 500.

GET /admin/upgrade-leads — SSR HTML board:
- Stats: total intents · pipeline MRR if all convert · average ticket
- Table: when · business (link to /upgrade/:id) · requested tier badge
  · contact name + mailto: link · notes excerpt · tier price right-aligned
- Empty state: explains how leads land here

Smoke tests:
- /admin/upgrade-leads without auth → 401
- Marketing copy verified populated for all 24 tier rows
- /api/upgrade/3 (Shanghai Rose) returns all 3 tiers with filled copy

Reversible: UPDATE tier_pricing SET marketing_copy = NULL rolls back
the copy fill. Routes can be unmounted by removing the regex from
ADMIN_PATHS + the handlers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e0407a5 · 2026-05-12 · YOLO tick 5: /upgrade/:bizId tier ladder page + intent capture
Public per-business upgrade page that reads tier_pricing seeded by the
3am gallery job. URL pattern: /upgrade/123 (numeric :bizId only — regex
([0-9]+) in the route prevents catching /upgrade/foo paths).

API:
- GET  /api/upgrade/:bizId — business row + tier_pricing rows for its
  category, ordered by price_cents ASC
- POST /api/upgrade/:bizId/intent — captures email + name + notes
  for buyers wanting to be notified when paid tiers open; persists
  to upgrade_intents (new table: business_id FK, requested_tier,
  contact_email, contact_name, notes, ip_addr, created_at)

Frontend (public/upgrade.html):
- Standard Ventura Corridor chrome: gold logo UL, hamburger UR
- Business header: eyebrow, name (Playfair), category chip, address chip,
  optional website chip with deeplink
- 3-tier cards grid (auto-fit 1-col mobile, 3-col desktop)
  · Basic: 'Already on Basic' disabled CTA
  · Featured: gold-bordered card with 'MOST POPULAR' ribbon
  · Spotlight: gold CTA, Playfair italic accent on tier name
- Tier marketing_copy reads from JSONB in DB; falls back to TIER_COPY
  defaults per tier name (basic/featured/spotlight/verified)
- 'Notify me when this opens' CTA expands an inline intent form (no
  page navigation, smooth-scroll into view, accessible)
- Form: email (required), name (optional), notes (textarea)
- POST → green ✓ toast on success, red ✕ on failure
- Current-tier badge if biz.tier already matches (green pill)

Stripe Connect path NOT wired yet — that's gated on Steve enabling
Connect on the Designer Wallcoverings account. Intent capture is the
pragmatic stand-in: photographers/restaurateurs sign up now, we email
them the moment Stripe is live. No false 'Buy Now' button that 404s.

End-to-end smoke test (Shanghai Rose, biz 3, amenity: restaurant):
- /api/upgrade/3 returns business + 3 tiers (basic free, featured ,
  spotlight )
- /upgrade/3 renders the page (HTTP 200)
- POST intent → upgrade_intents V1 inserted ok

Reversible: DROP TABLE upgrade_intents + revert routes if pricing model
changes. No external side-effects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e648a17 · 2026-05-12 · ventura-corridor: add /marketplace.html local listings board
Local-only marketplace for the Ventura Boulevard corridor (Sherman Oaks →
Woodland Hills). Mirrors the spirit of facebook.com/marketplace but is NOT
a mirror of FB data — Meta's TOS, BrandTotal/Voyager precedent, and image
copyright on user-uploaded photos rule out scraping. CHECK constraint
locks source_platform = 'local' so a future scraper can't silently bypass
the compliance gate.

What's in:
- db/migrations/023_marketplace.sql — marketplace_listings table + 16
  seed rows across 9 categories, source_platform locked to 'local'
- src/server/index.ts — GET /api/marketplace (filter/sort/search),
  GET /api/marketplace/categories (counts), POST /api/marketplace/post
  (neighbor submissions, validation only — no recaptcha yet)
- public/marketplace.html — dark noir/metal theme matching index.html,
  logo UL + hamburger UR, grid-search + sort + density slider per
  standing rule, category chip bar, detail modal, post-listing modal,
  FB-Marketplace deeplink footer (plain hyperlinks — allowed)
- public/index.html — added Marketplace link to the homepage nav stack

Public, no admin gate. Server stays bound to 127.0.0.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4598d33 · 2026-05-12 · YOLO tick 1: /gallery — geo-walk horizontal scroll of qwen3 art pieces
Public page at /gallery.html consumes /api/gallery/pieces (also new).
Mounts BEFORE the admin gate so it's reachable without auth.

API:
- GET /api/gallery/pieces?limit=N&neighborhood=...
  Returns gallery_pieces joined with businesses for name/category/address,
  ordered by position_pct ASC NULLS LAST (east-to-west: Encino → Studio City)
- GET /api/gallery/stats
  Returns total + count per neighborhood

Frontend (gallery.html):
- Sticky top bar: corridor logo UL + live counter (pieces / miles / current
  neighborhood) + hamburger UR. Matches the standing 'logo UL + hamburger UR
  on every page' rule.
- 90vh hero with title 'The Blvd Gallery' in Playfair Display italic,
  'Start the walk' CTA scrolls rail to leftmost piece.
- Horizontal scroll rail with scroll-snap-type: x mandatory. Each piece is
  380px wide on desktop, 78vw on mobile. Card layout: 4:5 canvas with
  diagonal gradient from the 3-hex color_palette (placeholder until SDXL),
  meta panel below with title (Playfair italic accent on key word), medium·
  year subtitle, prose description, business attribution divider.
- Neighborhood dividers: '1. Encino', '2. Sherman Oaks', '3. Studio City'
  appear between zones with position_pct label.
- Fixed bottom geo-bar: 'Encino — track — Studio City' with gold progress
  filling left-to-right as user scrolls. URL updates to ?pos=N for
  shareability. Header neighborhood label syncs on scroll.
- Keyboard: arrow keys scroll the rail. Mobile: native swipe.

Smoke test: /gallery.html → 200, /api/gallery/pieces → 200, returns the
1 piece already generated (Crimson Blooms No. 12 · mural · Shanghai Rose
· Studio City). 3am job will fill the rail with 2000 more.

Per Steve's standing 'NEVER expose AI software' rule: piece cards display
no vendor name; only the metadata + 'corridor design system' generator
attribution (and even that is in the DB, not user-visible UI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f95cfa2 · 2026-05-12 · Virtual Blvd Gallery + 3-tier business store · 3am autonomous build
Per Steve via debate-team-fast: virtual art gallery aggregating imagined
art per Ventura Blvd business + tiered listing store. Runs autonomously
at 03:07 local tonight via launchd.

NEW Postgres schema (002_gallery_tiers.sql, applied):
- businesses extended: tier (default basic), stripe_customer_id,
  stripe_subscription_id, tier_expires_at
- gallery_pieces: one row per business, AI-generated art metadata
  (art_title, art_medium, art_year_est, art_description, color_palette
  JSONB, lon/lat, neighborhood, position_pct 0-100 east-to-west,
  generator='corridor-design-system' — never names vendor per Steve's
  'NEVER expose AI software' rule)
- tier_pricing: per category × tier (basic/featured/spotlight) with
  price_cents + display_name + marketing_copy JSONB
- tier_assets: business uploads (logo, photo, video_url, chef_bio,
  booking_url, bar_number)
- tier_leads: premium-tier lead-magnet captures

scripts/generate-gallery-pieces.js — qwen3:14b art generator:
- Priority queue: restaurants > cafes > salons > hotels > lawyers > etc.
- For each business: structured-JSON prompt → art_title, art_medium,
  art_year_est, art_description, 3-hex color_palette
- INSERT ON CONFLICT (business_id) DO NOTHING — idempotent
- Position mapping: lon -118.510 (Encino) → 0%, -118.380 (Studio City)
  → 100%, so the gallery scrolls east-to-west naturally
- Neighborhood derived from lon thresholds

scripts/build-virtual-gallery.sh — orchestrator:
- Applies schema migration (idempotent IF NOT EXISTS)
- Seeds tier_pricing (24+ rows, ON CONFLICT DO NOTHING)
- Runs the qwen3 generator for top 2000 businesses by category priority
- Logs everything to logs/build-virtual-gallery-<date>.log
- Writes final stats to logs/build-virtual-gallery-summary.txt

launchd/com.steve.ventura-gallery-3am.plist (installed to
~/Library/LaunchAgents/ + loaded):
- StartCalendarInterval: hour=3, minute=7 (avoid the :00 / :30 cron
  herd per cron best practice)
- PATH includes homebrew + postgresql@14 so psql + node + ollama all
  resolve
- StandardOut/Err logged to project logs/

Smoke test result: 1 business generated end-to-end in ~10 seconds.
Sample: 'Crimson Blooms, No. 12' · mural · Studio City · pos 88.78%
· palette [#9E1B32, #F06292, #F5F5F5].

Reversible: TRUNCATE gallery_pieces RESTART IDENTITY rolls back the
entire art set. tier_pricing/tier_assets/tier_leads safe to drop too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0900593 · 2026-05-12 · docs(appointments): Shopify embed snippet + copy-paste instructions
Self-contained HTML page at exports/shopify-appointment-embed.html with
the <div id=appt-widget data-owner=...> + <script src=...embed/appointments.js>
snippet, plus admin-side instructions for Shopify Pages source view,
sandbox/CSP notes, and a per-owner OAuth reminder.
b66894f · 2026-05-12 · feat(appointments): self-contained Smart Scheduling widget + Google Calendar OAuth
- Schema (022_appointments.sql): appt_appointments, appt_availability,
  appt_oauth_tokens, appt_oauth_state. Seeds info@designerwallcoverings.com
  for Mon-Fri 9am-5pm PT @ 30-min slots.
- Router mounted ahead of admin gate + static handler. Public routes:
  /book, /embed/appointments.js, /appointments/:id, /api/appointments/{health,
  availability,book,oauth/start,oauth/callback,oauth/status,oauth/revoke}.
  /api/appointments/list is Basic-Auth gated.
- OAuth helpers are fetch-based; no googleapis dep. Refresh logic auto-
  rotates tokens 2 min before expiry. State nonce is single-use.
- Slot computer honors America/Los_Angeles + filters both DB-booked and
  Google freeBusy windows; race-protects book POST.
- Widget HTML is pure vanilla JS; reuses the noir/metal-gold token palette
  already used by public/index.html. Supports ?embed=1 chromeless mode.
- Embed loader (/embed/appointments.js) lazy-mounts via IntersectionObserver,
  sandboxes the iframe (allow-scripts allow-forms allow-same-origin allow-popups),
  serves CORS:* + frame-ancestors:*.
- Branding rule respected: 'Smart Scheduling' label everywhere except the
  Google-hosted consent screen.
ff0e4d0 · 2026-05-08 · fix: build-status ideas_rejected wiring + collateral damage from earlier replace_all
Tick 80 added ideas_rejected to /api/build-status but my Edit failed
the "string not found" check, so out.ideas_rejected = ideaRejects was
referencing an undeclared variable. Fixed by adding the rejects.jsonl
read alongside accepted in the same try-block.

Also un-broke the build-status handler that got collateral damage from
the tick-74 replace_all=true on `process.env.HOME ||` — that swap
inserted a `return res.json({count: 0, rows: [], note: 'home dir
unresolvable'})` line into a NON-ideas endpoint, killing /api/build-status.
Replaced the dual-endpoint home-check with an inline `if (home)` guard
that doesn't early-return.

Both /api/ideas/{recent,rejects} verified still serving (24/104).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e4ee322 · 2026-05-08 · feat(coverage): "tonight on the corridor" glance footer
One-line session summary at the bottom of /coverage.html showing
commits / insertion lines / news articles+biz / ideas accepted/total.
Reads /api/session-stats which aggregates git log + news_items +
ideas.jsonl in a single endpoint (60s cache).

Visible at the dramatic black-and-gold footer panel below all the
existing analytics sections so Steve sees the run scorecard at the
end of the page without scrolling out into other tools.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
be002e1 · 2026-05-08 · fix(coverage): wire JS for rejects drawer (was empty since tick 73)
Tick 73 added the <details> container for rejected ideas but the JS
that populates #idea-rejects didnt get inserted because the Edit
string match failed. Now properly fetches /api/ideas/rejects and
renders the same 3-column grid (score / type / title+body) but with
a red score color to visually distinguish from accepted.

Currently 91 rejects across types: 31 connection, 24 product, 19
automation, 16 rename, 1 kill. Top reject scores show how close they
came to acceptance (most are <26 of the 28 threshold).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3853840 · 2026-05-08 · fix: portable home dir in /api/ideas/{recent,rejects} (caught by Kimi K2.5)
Direct one-shot Kimi audit on the same diff codex-2way reviewed flagged
one issue codex-2way missed: process.env.HOME || /Users/stevestudio2 is
a brittle fallback. On any machine where HOME is unset (and most
non-Steve users), the endpoint quietly read from a non-existent path
or worse a different users directory.

Fix: prefer process.env.HOME, fall back to os.homedir(), then graceful
empty response with a note instead of returning an error.

Both endpoints (replace-all of the two HOME lines) now share the
helper. Smoke confirms /api/ideas/recent + /rejects still serve under
their normal flows.

Cross-validation between two AI auditors (codex-2way + standalone Kimi)
gave us 4 real bugs to fix vs 6 false-positive nits to ignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1229e88 · 2026-05-08 · fix: 3 bugs caught by codex-2way audit at tick 73
PROSECUTOR (Kimi K2.5 cloud) audit on the 17-commit batch since
tick 49 found three real issues. All fixed:

1. public/index.html:353 — fleet panel ternary "escapeHtml ? escapeHtml(...)"
   was unnecessary but legal (escapeHtml is defined at line 204). Removed
   the dead ternary; also wraps m.label since it comes from network so
   defense in depth. PROSECUTOR misread this as undefined-fn but flagged
   the right surface area to harden.

2. src/server/index.ts:541-545 — RSS feed vertical filter was concatenating
   user input into ILIKE pattern without escaping % or _ wildcards. So
   ?vertical=% would match every category. Real injection but low impact
   (read-only, no auth bypass). Now escapes \%, \_, \\ in tokens before
   the ILIKE assembly.

3. public/news.html:200 — Hot-only toggle stored Number(r.id) in Set but
   filtered with String r.business_id — PG bigint always serializes as
   string, so Set.has missed every row. Set.has now coerces both sides
   to String. Also slices leaderboard rows to 1000 max as a tab-OOM guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7d1fe85 · 2026-05-08 · feat(coverage): rejected-ideas drawer below the idea-loop feed
Collapsible <details> below accepted-ideas grid. Shows recently rejected
debate-panel ideas (red score, type pill, title + body). Editorial
sanity-check: are we missing good ideas?

Routes:
  GET /api/ideas/rejects   {count, rows[]} from rejects.jsonl, newest
                           first, top 20. 60s cache.

UI:
  /coverage.html idea-section   collapsible "show recently rejected ideas"
                                drawer; styled red-toned to differentiate
                                from accepted gold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4a0ab3d · 2026-05-08 · feat(fleet): Mac1+Mac2 dashboard bridge on corridor homepage strip
Live status of both Mac dashboards now shows on the always-visible /
strip alongside news + ideas counters. Format:
  FLEET · Mac Studio 1 · qwen3:14b ⚓ · cpu 38% · ram 57%
       · Mac Studio 2 · gemma3:12b · cpu 12% · ram 41%

Each is clickable -> opens the corresponding 57 Bel Air / Eldorado
dashboard. Soft-fails per machine — if Mac1 is unreachable, Mac2 still
renders (and shows red OFFLINE link).

Routes:
  GET /api/fleet    {macs:[{label,cpu,ram_pct,ollama_models[],
                           installed_count,ollama_proc,power_mode,
                           ok,url|error}], ts}
                    Proxies http://100.94.103.98:9930/api/status (Mac1)
                    + http://localhost:9931/api/status (Mac2). 10s cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4927f54 · 2026-05-07 · feat(news): "Hot only" toggle on /news.html filter bar
Editors can isolate the 10 hot publishers (fresh_7d >= 3) from the
firehose with one click. Toggle button next to the search/sort/vertical
controls; reuses /api/news/leaderboard to fetch the hot biz ID set on
first activation, then filters client-side. Restores to all-rows on
toggle off.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
df2073d · 2026-05-07 · feat(coverage): per-type accept-rate mini-bars in idea-loop section
Replaces the one-line stats string with a grid: type / horizontal bar
showing accept share / "N/total · pct%" label. Shows the bias the panel
has toward "connection" ideas (6/21 = 40%) vs renames (1/11 = 9%) vs
products/automations/kills.

Pure client-side render — same /api/ideas/stats endpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
faa570d · 2026-05-07 · feat(coverage): /api/ideas/stats + accept-rate line on /coverage.html
Sanity-check the idea-loop debate panel: are we accepting too generously
(>50% rate) or too harshly (<5%)? Current 7/55 = 12.7% sits in the
healthy zone.

Routes:
  GET /api/ideas/stats   {accepted, rejected, accept_rate, accepted_by_type{},
                          rejected_by_type{}}. Reads both jsonl files in the
                          idea-loop data dir; 60s cache.

UI:
  /coverage.html idea section   monospace stats line above the score-sorted
                                feed: "N accepted · M rejected · P%
                                accept rate · by type: …"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c9a00d6 · 2026-05-07 · feat(coverage): ideas_accepted count in homepage build-status strip
Adds the idea-loop accepted-count next to the news counts on the always-
visible / strip. Click "Ideas accepted" opens the standalone idea-loop
viewer at :9920 (new tab).

API change:
  /api/build-status   +ideas_accepted (line count of ideas.jsonl in the
                      idea-loop skill data dir)

UI change:
  public/index.html   one new b-strip cell next to news counts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8172d93 · 2026-05-07 · feat(news): vertical filter dropdown on /news.html
Adds an "All categories" select next to the search and sort controls
on /news.html. Categories are populated from the rendered data so the
dropdown stays in sync with whatever the API returned (uses prefix
match so picking "amenity: restaurant" matches that exact subcategory,
"shop" matches every shop:* subcategory).

Pure client-side filter, no server change — /api/news/recent already
returns category per row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b8a21eb · 2026-05-07 · feat(news): news counts in homepage build-status strip
Three new live counters in the always-visible strip across the top of /:
News articles | News biz | Fresh ‹7d. Click "News articles" to jump
to /news.html.

API change:
  /api/build-status   +news_total, +news_biz, +news_fresh_7d in counts{}

UI change:
  public/index.html   3 new b-strip cells; setNews() helper handles
                      the inner <a> wrapper so the link stays intact
                      when the count refreshes.

First-glance signal: 76 articles / 46 biz / 76 fresh in last 7 days
right now. Drops automatically as articles age out of the 7-day window.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dab6585 · 2026-05-07 · feat(coverage): idea-loop feed bridge on /coverage.html
Read-only bridge to the idea-loop skill so accepted ideas surface in
the corridor coverage page alongside the news analytics. Read from
~/.claude/skills/idea-loop/data/ideas.jsonl on demand, sorted by
score_total DESC, top 20.

Routes:
  GET /api/ideas/recent    {count, rows[]} from ideas.jsonl. 60s cache.
                           Graceful "idea-loop not yet running" if file
                           missing.

UI:
  /coverage.html new "Idea-loop" section above News scrape calendar.
  3-column grid: score / TYPE (color-coded by CONNECTION/PRODUCT/etc.) /
  title + body excerpt. Header links to the standalone viewer at :9920.

Both autonomous loops now have a single visual home — coverage.html
shows news pipeline analytics + creative idea pipeline side by side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
f9ee539 · 2026-05-07 · feat(news): /api/news/by-day + 90-day calendar heatmap on /coverage.html
GitHub-style daily scrape heatmap so editors can see at a glance whether
the nightly 03:15 cron is landing fresh content vs drifting silent.

Routes:
  GET /api/news/by-day    90-day generate_series LEFT JOIN news_items
                          by fetched_at::date in America/Los_Angeles tz.
                          10-min cache.

UI:
  /coverage.html          new "News scrape calendar" section above the
                          existing News heatmap. 14px gold-tinted squares,
                          5 brightness levels keyed to max-day count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4035b3c · 2026-05-07 · feat(news): news-recency badge on /pitches.html name column
Editors doing outreach can now spot active publishers in their pitch
queue without leaving the table. Three-tier badge inline with the
business name:

  fresh_7d >= 3   solid gold-glow "🔥3" link to /business/<id>/news
  fresh_7d >= 1   gold outline    "📰N" link
  total >= 1      gray outline    "📰N" link  (had news but stale)
  (no badge)      no scraped news

API change:
  /api/pitches    +news_count, +news_fresh_7d, +last_news_at per pitch row
                  via single LATERAL aggregate from news_items
                  (one extra COUNT/GROUP per row, not per item)

UI change:
  public/pitches.html row template   badge appended to .nm cell
                                     using the same tier scheme as the
                                     map detail panel for consistency

Carries the editorial signal "what's this business saying right now"
into the place where Steve actually decides who to pitch tonight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
888783b · 2026-05-07 · feat(news): /api/news/sources + source-domain bar chart on /coverage.html
Editorial signal: which platforms host the corridor's published news?
40 distinct source domains currently flowing — solarunlimited.com,
bluespa.com, encinomed.org, planetfitness.com, target.com, vbs.org etc.

Routes:
  GET /api/news/sources    {host, articles, biz_count} per unique
                           hostname extracted from news_items.source_url.
                           Strips www., orders by articles DESC.
                           Cap 40, 5-min cache.

UI:
  /coverage.html           new "Source domains" bar-section above the
                           Top publishers leaderboard. Green-toned bars
                           (vs gold heatmap, metal-glow leaderboard) so
                           the three sections visually differentiate.
                           Hostnames link out in new tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4b74df3 · 2026-05-07 · feat(news): summarizer pre-flight + 4-hour poll launchd
Two changes that let the news summarizer drain naturally when Mac1
frees up, instead of burning a 62-min backoff per item or sleeping
through the gap:

1. Pre-flight probe in summarize_news.ts
   - Sends a tiny "reply OK" call before opening the work queue.
   - If Mac1 returns 503/429 immediately (i.e. the 2000-feature
     generate_features batch is hammering it), exit 0 cleanly with a
     "DEFERRED" log line.
   - Saves 60+ minutes of pointless backoff per failed run.

2. Poll launchd com.steve.ventura-corridor-news-summarize-poll
   - StartInterval 14400 (every 4 hours).
   - Pre-flight gates work; when Mac1 is busy this fires + immediately
     exits, costing ~/bin/bash + ~50ms.
   - The first run after magazine-gen winds down will catch the gap
     and drain all 76 unsummarized items.

Existing 5:30am daily summarize launchd unchanged — this poll is
additive coverage during the magazine-gen marathon (~ 15 more hours
to go at current 40s/feature rate).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0f24227 · 2026-05-07 · feat(news): /api/news/leaderboard + Top publishers section on /coverage.html
Surfaces the corridors most-active publishers — businesses whose blogs/
news pages are actually being maintained vs dormant. Pairs with the
recency badge from tick 52 — the same Hot threshold ( fresh_7d >= 3 )
gets a red flame in front of the name on the leaderboard.

Routes:
  GET /api/news/leaderboard    {id, name, city, category, total,
                                fresh_7d, fresh_30d, last_at} per biz,
                                sorted fresh_7d DESC, fresh_30d DESC,
                                total DESC. 5-min cache. Cap 50.

UI:
  /coverage.html   new "Top publishers" bar-section after the heatmap.
                   Shows rank, name (clickable -> /business/:id/news),
                   city + category, 7d count, 30d count, last-fetched
                   date. Top 20 displayed.

Real numbers: 10 corridor businesses currently flag Hot, 36 more flag
Fresh. Leaderboard refreshes daily at 03:15 with the next scrape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67f7514 · 2026-05-07 · feat(news): recency badge (Hot/Fresh/Stale) on map detail panel
Editors can spot active corridor publishers at a glance instead of
scrolling the full news rail. Tiered visual signal:

  Hot    fresh_7d >= 3   solid gold-glow background, "🔥 HOT"
  Fresh  fresh_7d >= 1   gold outline, "📰 FRESH"
  Stale  fresh_30d >= 1  muted gray outline, "STALE"
  (no badge if no news in last 30 days)

API change:
  /api/businesses/:id     +news_stats {fresh_7d, fresh_30d, last_at}
                          single COUNT(*) FILTER query, no extra LATERAL

UI change:
  public/index.html showPanel    badge appended to .pn-name h3 inline

All 46 news-having corridor businesses currently flag — the nightly
03:15 scrape lands fresh items in news_items so the badges decay
naturally as articles age out of the 7-day / 30-day windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3b4d523 · 2026-05-07 · feat(news): per-business RSS via /news/feed.xml?business=N
Same RSS handler now optionally filters to a single business. Combines
with vertical: ?business=49&vertical=shop returns the AND.

Routes (all on the existing /news/feed.xml):
  /news/feed.xml                       firehose, 50 most recent
  /news/feed.xml?vertical=amenity      top-cat filter
  /news/feed.xml?vertical=shop:bakery  exact subcategory filter
  /news/feed.xml?business=N            single-business feed
  /news/feed.xml?business=N&vertical=X intersection (AND)

UI:
  /business/:id/news <head>            <link rel=alternate type=rss>
                                       so feed readers auto-subscribe
                                       when the user opens the page
  /business/:id/news header            "📡 RSS subscribe" anchor next
                                       to the website link

Channel <title> dynamically suffixes the business_name when filtered
to one biz, so feed-reader panes label correctly.

Smoke: biz 49 (Solar Unlimited Encino, 4 articles) feed validates
with xmllint, channel title contains the business name, /business/49/news
includes the subscribe button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
52178df · 2026-05-07 · feat(news): per-vertical RSS feeds via /news/feed.xml?vertical=...
Same RSS handler now optionally filters by OSM category. Steve plugs
distinct URLs into NetNewsWire / Feedbin and gets a feed-per-vertical
without polluting his all-corridor stream.

URL forms accepted (all normalized server-side):
  ?vertical=amenity                  top-cat: matches all amenity:*
  ?vertical=amenity:restaurant       exact subcategory
  ?vertical=amenity-restaurant       hyphen alternative
  ?vertical=amenity:fast_food        underscore allowed
  ?vertical=amenity:fast%20food      space allowed (percent-encoded)

Tokenization: split on [:-_\s], single token = top-cat ILIKE 'cat: %',
two-or-more tokens = exact match against "cat: rest of name".

Channel title gets a (vertical) suffix when filtered so feed-reader
panes show the right feed name.

Discovery: 7 <link rel=alternate type=application/rss+xml> tags added
to /news.html <head> for the popular categories so RSS readers
auto-detect every flavor when subscribed to /news.html.

Real numbers: amenity 41, fast_food 9, lawyers 8, restaurants 5,
shop 17, dentists 5, etc. xmllint validates clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
840b1a4 · 2026-05-07 · fix(news): hardenings caught by codex-2way round 1 audit
Two real findings (PROSECUTOR-only, DEFENDER stayed silent both times):

1. /business/:id/news LIMIT 200
   The news_items query had no LIMIT, so a single business with
   thousands of articles would be a DoS vector (slow query + huge
   payload). Capped at 200; existing data tops out at 4 per biz so
   this is purely future-proofing.

2. baseUrl XML-escape in /sitemap.xml and /news/feed.xml
   PUBLIC_BASE_URL is env-controlled but not validated. If it ever
   contained ampersand/quotes/angle brackets, the emitted XML would
   be invalid and could let an operator-supplied env value inject
   attributes into <link>/<loc>. Defense in depth - shared escape
   helper applied at the top of each route.

Both routes still validate via xmllint; smoke confirms /business/49/news
returns 200 with the LIMIT clause in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9027832 · 2026-05-07 · feat(seo): include news pages in /sitemap.xml
The news pipeline shipped 5 new URL surfaces this session
(/news.html, /this-week.html, /news/feed.xml-discovery via /news.html,
/business/:id/news, /coverage.html news heatmap). Add them to the
sitemap so search engines find them.

Static pages added:
  /news.html        hourly  pri 0.9
  /this-week.html   daily   pri 0.8
  /coverage.html    daily   pri 0.5

Dynamic pages added:
  /business/:id/news for every biz with at least one news_item
  Cap 1000, ordered by most-recently-fetched, lastmod = MAX(fetched_at)

Real numbers: 46 /business/:id/news entries flowing on first build.
xmllint validates clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
33b09e9 · 2026-05-07 · feat(news): "Latest from their site" section on /magazine/:id
Closes the loop between editorial features and live scraped news.
The magazine article view (/magazine/:id) now includes a 5-item
news rail above the building-mates and category-related sections,
showing what the business has been actually publishing on their
own website.

Implementation:
  - new newsBiz query in the /magazine/:id handler returns last 5
    news_items for f.business_id
  - rendered between the existing "Pipeline events" details and the
    "In the same building" mates section
  - safeUrl scheme-validation pattern carried forward from tick 43:
    href is escaped + http(s)-only, falling back to "#"
  - "All news from <biz> -->" deep-link goes to /business/:id/news
  - Hidden when newsBiz.rows is empty so unpublished biz features
    don't grow a blank section

Real reach: 3 features currently on news-having biz (Manu Farrarons,
Coffee Bean & Tea Leaf, $300 Data Recovery). Grows automatically
with the nightly scrape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ec89429 · 2026-05-07 · feat(news): /api/news/by-vertical + heatmap on /coverage.html
Surfaces which OSM categories are publishing the most blog/news/press
content. New section on /coverage.html below the existing
"By vertical / By city" coverage bars, but reflecting scraped news
volume not magazine-feature coverage.

Routes:
  GET /api/news/by-vertical   {category, top_cat, news_count, biz_count}
                              ordered by news_count DESC; CTE joins
                              news_items + businesses; 5-min cache.

UI changes:
  public/coverage.html        new "News heatmap" section with bar rows
                              (gold gradient fill); lazy-loaded after
                              the existing coverage bars so the page
                              still renders if news_items is empty;
                              graceful empty state + error handler.

Real numbers right now: amenity:restaurant 16 articles / 8 biz,
shop:hairdresser 12 / 6, office:lawyer 9 / 4, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5afdd26 · 2026-05-07 · feat(news): /business/:id/news per-business news firehose page
Drill-down view from any news rail. When a viewer clicks the news badge
on /chains, /buildings, the map detail panel, or /news.html, they can
land on a dedicated per-business page showing every article scraped
from that business website (full body excerpts, dates, source domain,
qwen3-summary tag when present).

Routes:
  GET /business/:id/news    inline-rendered HTML, no template, paper-noir
                            style matching /news.html. 404 on bad id,
                            graceful empty state when news_items is zero
                            ("nightly 03:15 scrape will check again").

Hardening:
  - safeUrl() scheme-validates http(s) before <a href> interpolation
  - esc() applied to all biz-attribute values (carry-over hardening
    pattern from tick 43 XSS fix)

Discovery wired:
  - public/index.html showPanel news rail: "all -->" link in header
  - public/buildings.html badge: now points at /business/<id>/news
    (was /news.html#biz-<id> which was a non-existent anchor)

Path whitelisted in ADMIN_PATHS regex; smoke confirms 200 with content,
404 on bogus id, empty-state on real id with no news.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
b329ecf · 2026-05-07 · feat(news): RSS feed at /news/feed.xml + auto-discovery on /news.html
Steve plugs the URL into NetNewsWire / Feedbin / any feed reader and
gets every fresh corridor news item without opening the magazine.

Routes:
  GET /news/feed.xml      RSS 2.0, 50 most-recent items, 5-min cache
                          Item description prefers qwen3 summary, falls
                          back to body excerpt; pubDate prefers
                          published_guess, falls back to fetched_at.
                          Source URL scheme-validated (http/https only)
                          before going into <link>; rejects javascript:
                          falls back to /news.html.

Discovery:
  public/news.html <head>  +link rel=alternate type=application/rss+xml
                            so feed readers auto-detect when subscribed
                            to /news.html.

Path whitelisted in ADMIN_PATHS regex list. xmllint validates clean,
50 items flowing on first smoke.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4cada62 · 2026-05-07 · feat(news): news_count badge on /chains.html bar rows
Chain-store roster now flags any chain whose locations have scraped
news/blog posts on file. Aggregates news_count across every business_id
that maps to the chain's normalized name (so Coffee Bean across 2
locations = 2, not 1).

API change:
  /api/chains   +news_count INT per chain (sum across all chain locations)
                via with_news CTE joining grouped.ids[] against news_items

UI change:
  public/chains.html bar-row    new gold-border .news-badge "narrow N"
                                appended to chain name when news_count > 0
                                clicks through to /news.html

Cache: 5-minute server-side cache means new news lands on the chains
view within 5 minutes of summarizer/scraper completion.

3 of 60 chains currently flag (Coffee Bean Tea Leaf, Leading Tax Group,
300 Data Recovery). Grows automatically with the nightly 03:15 scrape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d444435 · 2026-05-07 · fix(generate_features): tighten banned-phrase regex to catch bare 'X Meets Y'
Tick-29 review found 'Skincare Meets Socializing' slipped through — the
regex required a 'where' prefix before 'X meets Y', but the prompt's
NEVER list bans the bare form too. Updated BANNED_HEADLINE_RE to match
any '\w+ meets \w+' or '\w+ rooted in \w+' pattern, not just the
'where'-prefixed cliché.
7594bb3 · 2026-05-07 · fix(security): escape source_url + scheme-validate href in /news rail (XSS)
Found by codex-2way round-1 PROSECUTOR (Kimi K2.5 cloud) auditing tick 41's
per-business news rail on the corridor map. n.source_url was interpolated
raw into href, allowing javascript: protocol injection or attribute breakout
via crafted URLs. Defense in depth - even though the scraper currently
controls news_items inserts, never trust HTML scraped from third parties.

Adds:
  - safeUrl()   parses URL, rejects non-http(s) schemes, percent-encodes
                special chars before interpolation
  - escAttr()   shared HTML-attribute escaper covering ampersand, lt, gt, quote, apos

Both applied to href, title, and date inside the news rail block in
public/index.html showPanel(). DEFENDER (qwen3:14b@Mac2) had refuted the
finding twice; PROSECUTOR was right both times.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0418331 · 2026-05-07 · feat(news): news_count badge on /buildings.html tenant rows
When Steve clicks an address on /buildings to expand its tenant
roster, any tenant with scraped news_items now shows a small
gold-border pill: 📰 N (where N = article count). Click goes to
/news.html#biz-{id} for filterable access.

API change:
  /api/buildings/:bldg                +news_count INT per tenant row
                                      via LATERAL count from news_items

UI change:
  public/buildings.html roster table  badge appended to tenant name
                                      cell, only when news_count > 0

Verified: 14622 Ventura Boulevard Sherman Oaks 91403 has 9 tenants,
3 with news (Baja Fresh, Noah's Bagels, The Habit Burger Grill).

Continues the discoverability theme from tick 41 (map detail panel
news rail) — every existing surface that shows businesses now has a
quiet way to surface what their site has been publishing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8afd939 · 2026-05-07 · feat(news): per-business news rail on map detail panel + news_count column
Closes the discoverability loop: when Steve clicks any pin on the
corridor map, if that business has scraped news_items, a small rail
shows the latest 4 article titles + dates with click-through to the
source URL. Subtle gold tint background keeps it editorial.

API changes:
  /api/businesses                     +news_count INT per row
                                      (LATERAL count from news_items)
  /api/businesses/:id                 +news[] last-10 articles
                                      (id, source_url, title, excerpt,
                                       summary, published_guess, fetched_at)

UI changes:
  public/index.html showPanel()       new "News from their site" rail
                                      below contacts; only renders when
                                      news_items > 0; capped at 4 visible
                                      with count in the header

Reach: 45 of the 153 sited businesses currently have at least one
scraped article (29 percent), so the rail surfaces meaningfully on
every fourth pin click. Grows automatically with the nightly scrape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
00f39c0 · 2026-05-07 · fix(generate_features): post-filter retry on banned headline phrases
Negative prompt instructions weren't enough — qwen3:14b ignored the ban
list and produced 'Multi-Tenant Building and BJJ Discipline' on the
first regen attempt with the patched prompt. Adding a hard gate: caller
now retries up to 3 times if BANNED_HEADLINE_RE matches, with escalating
temperature and an inline correction prompt. Throws if all 3 attempts
trip the filter (caller catches and skips). Tick-26.
e7c9465 · 2026-05-07 · fix(generate_features): forbid 'multi-tenant building' boilerplate in headlines
The prompt was injecting the literal phrase 'multi-tenant building' into
the user message, and qwen3:14b was dutifully reusing it — 36/167 (22%) of
all generated headlines opened with that phrase. Reframed the building
context as quiet body color, added explicit ban list with sibling
phrasings, and re-flagged the 36 polluted rows back to draft for regen.

Also commits the audit-tabs.cjs Playwright sweep and migration 020 that
unblocked the /pitches.html query (column p.website does not exist).

Caught by the auto_dedupe_headlines trigram report (top cluster
'multi tenant building' 36×). Tick-25 yolo loop.
e45334e · 2026-05-07 · feat(news): /this-week.html editorial digest + /api/news/digest
Editorial front-door for the news pipeline. Where /news.html is the
firehose, /this-week.html is the curated read — 3 lead stories at the
top with serif/italic display type matching magazine.html, then a
two-column "also this week" tail of up to 30 more dispatches.

Routes:
  GET /api/news/digest    {lead:[3], tail:[<=30], generated_at}
                          5-min cache, lead = articles with body >= 200
                          chars + non-trivial title; tail = remainder
                          most-recent-first
  GET /this-week.html     paper aesthetic, double-rule masthead, mono
                          kickers, Cormorant Garamond italic display

Both paths whitelisted in ADMIN_PATHS so basic-auth gates them
consistent with the rest of the corridor admin tools.

Pulls from the same news_items table populated by the news scraper,
so it works today on the 76 articles already in DB and grows
automatically as the nightly 03:15 scrape lands more.

No external API deps.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
828ab33 · 2026-05-07 · feat(enrich): website-discovery resilience + category prioritization
Brave free tier hit its $15/mo cap mid-tick. Built two pieces of
resilience for when the quota resets and for in-the-meantime work:

1. website_via_brave.ts — added --high_value category filter. Default
   queue now skips "office: company" / "office: estate_agent" /
   "office: lawyer" shells (which are 70 percent of the corridor table
   and ~0 percent of the websites) and prioritizes restaurants, cafes,
   bars, retail, beauty, fitness, dentists, hotels, pet shops. Big
   hit-rate boost when Brave comes back online.

2. website_via_ddg.ts — new DuckDuckGo HTML fallback. No API key, no
   monthly quota. Same domain-credibility logic as Brave/Exa.
   Smoke-tested 1/15 hits before DDG started returning 202 anti-abuse
   challenges; useful for bursts of <30 queries when nothing else
   works. Wired as `enrich:websites:ddg` npm script.

3. STATUS_NEWS_PIPELINE.md — frozen snapshot of what shipped and what
   unblocks next. Highest-leverage move is Steve provisioning a
   FOURSQUARE_API_KEY (free 100K/mo, returns websites explicitly) so
   `npm run ingest:foursquare` can land thousands of new rows with URLs.

No data corruption: failed Brave attempts left the rows untouched
(the script only UPDATEs on a winner).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0a78f9e · 2026-05-07 · feat(news): nightly launchd jobs + patient summarizer backoff
- launchd/com.steve.ventura-corridor-news-scrape.plist
    Runs `npm run crawl:news` at 03:15 daily. Polite defaults
    (4 concurrent, 500ms host gap, 10s timeout).
- launchd/com.steve.ventura-corridor-news-summarize.plist
    Runs `npm run news:summarize -- --limit=200` at 05:30 daily, two hours
    after the scraper, so fresh items get qwen3:14b summaries before the
    morning corpus-summary email at 09:00.
- Both registered + loaded into ~/Library/LaunchAgents and copied into
  the project's launchd/ dir for version-tracking + reproducibility.
- summarize_news.ts backoff bumped from 8s/16s/32s/64s (max ~2min) to
  60s/120s/240s/480s/960s/1920s (max ~62min). Magazine-gen 2000-feature
  runs hold Mac1 for hours; the old backoff exhausted before capacity
  opened. Patient backoff lets the 05:30 launchd drain even if the
  generator runs long.

Verified: launchctl list shows both plists loaded; Steve's 2000-feature
generate_features will finish before 05:30 most nights.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e8b2f73 · 2026-05-07 · feat(news): /api/news/recent + /news.html magazine surface
Closes the news-feed loop end-to-end:
  scraper -> news_items -> /api/news/recent -> /news.html magazine rail

Routes:
  GET /api/news/recent?limit=N    most-recent first across all businesses
  GET /api/news/by-business/:id   per-business article list

UI (/news.html):
  - Noir editorial style matching /calendar.html and /coverage.html
  - 4-stat header (articles, businesses, summarized, last 7 days)
  - Live filter + sort (recent / biz A-Z / title A-Z)
  - Card grid w/ source link, title, excerpt, host, date
  - "awaiting qwen3 summary..." placeholder when summary missing

Both paths whitelisted in ADMIN_PATHS so basic-auth gates them like the
rest of the corridor admin tools.

Background scrape across 153 sites: 76 articles inserted across 46
distinct businesses (50 percent biz hit rate, up from 26 percent in tick 36 smoke).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

File tree

234 files tracked. Click any to browse the source at HEAD.

Other build journals

← Ventura Claw Leads  ·  all 4 projects  ·  Ventura Dog Walking →

Export

commits.csv · feed.atom · project.json · commits.json

rendered in 1ms