Ventura Corridor
Search the build
-
28b0f742026-07-13 auto-save: 2026-07-13T10:54:28 (1 files) — scripts/push-news-snapshot.sh -
e4101a62026-07-13 move tsx to runtime dependencies — prod launches via tsx, --omit=dev deploy pruned it -
a49ac4f2026-07-13 feat: explicit 'Source: <site>' credit on news + archive cards (Steve 2026-07-13: crawled items must credit the client site) -
d2e66de2026-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 -
9a051622026-07-03 chore: macstudio3 migration — reconcile from mac2 + repoint paths (stevestudio2→macstudio3, node/npm/npx→/opt/homebrew) -
238031f2026-06-24 LinkedIn dashboard: cap table at 500 rows for snappy 2,860-record list + confirm guard on Mark All Visible -
ce8201c2026-06-24 LinkedIn dashboard: add 2,591 DW trade clients as a source + source filter (Ventura/Clients) -
45f58552026-06-24 LinkedIn dashboard: row select + paced batch-open follow + No-LinkedIn skip -
10268df2026-06-23 Add LinkedIn marketing calendar — 33 content pieces: 5 Events, 4 Newsletter issues, 8 Company posts, 8 Personal posts with full copy -
c6254fa2026-06-23 Add LinkedIn activity ledger — quick-entry log with stats, filters, CSV export -
ff0ed972026-06-23 Add LinkedIn pitch library — posts, comment openers, InMails, objection responses by tier -
d2f76872026-06-23 Add Ventura Blvd LinkedIn follow dashboard + InMail templates + DB export -
b0d3e432026-06-23 auto-save: 2026-06-23T14:52:21 (2 files) — LINKEDIN_COMPLETE_MASTER_FOLLOW_LIST.md LINKEDIN_EXPANDED_CALENDAR_7TIERS.md -
71bca452026-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 -
f441e592026-06-23 auto-save: 2026-06-23T13:52:03 (3 files) — LINKEDIN_OUTREACH_TRACKER.csv LINKEDIN_WEEKLY_SCHEDULE.md linkedin-tracker-dashboard.html -
021eaf42026-06-23 Add LinkedIn strategy index/overview (complete package ready for action) -
eecce2a2026-06-23 Add LinkedIn start guide and quick-start reference (4 total strategy docs) -
a8515aa2026-06-23 Add comprehensive LinkedIn strategy package: 28 accounts to follow, discovery targets, post templates, daily cadence -
c1039b42026-06-15 ad-signals: canonical-firm dedup pass (fix Panera double-listing); 482/127 -> 446/119 -
057d4522026-06-14 ad-signals: read-only pass-1 ad/social tracker over crawled firm HTML (482 businesses, 127 running paid ads) -
7004f122026-06-02 fix: gallery-pieces 3am job — require('pg') → import (package is type:module, was throwing ReferenceError nightly since May 12) -
b261b8f2026-05-31 Link favicon.svg in head (use branded SVG, not .ico fallback) -
69127942026-05-31 gitignore: add *.orig *.save *.old *~ copy-of-* backup patterns (standing-rule completeness) -
19b54272026-05-27 chore: gitignore all .env/.bak/.plist-backups + drop tracked backup files -
e1392fb2026-05-24 ventura-corridor: ship 'POSSIBLY NO BUSINESS LICENSE' badge -
cf9f1802026-05-12 YOLO tick 6: qwen3 fills tier marketing_copy + /admin/upgrade-leads board -
e0407a52026-05-12 YOLO tick 5: /upgrade/:bizId tier ladder page + intent capture -
e648a172026-05-12 ventura-corridor: add /marketplace.html local listings board -
4598d332026-05-12 YOLO tick 1: /gallery — geo-walk horizontal scroll of qwen3 art pieces -
f95cfa22026-05-12 Virtual Blvd Gallery + 3-tier business store · 3am autonomous build -
09005932026-05-12 docs(appointments): Shopify embed snippet + copy-paste instructions -
b66894f2026-05-12 feat(appointments): self-contained Smart Scheduling widget + Google Calendar OAuth -
75c73142026-05-10 task #4: source CC0 glTF bus + car models (Kenney Car Kit + Quaternius Public Transport) -
ff0e4d02026-05-08 fix: build-status ideas_rejected wiring + collateral damage from earlier replace_all -
b552dfe2026-05-08 feat(coverage): expose ideas_rejected on homepage strip alongside accepted -
e4ee3222026-05-08 feat(coverage): "tonight on the corridor" glance footer -
be002e12026-05-08 fix(coverage): wire JS for rejects drawer (was empty since tick 73) -
38538402026-05-08 fix: portable home dir in /api/ideas/{recent,rejects} (caught by Kimi K2.5) -
1229e882026-05-08 fix: 3 bugs caught by codex-2way audit at tick 73 -
7d1fe852026-05-08 feat(coverage): rejected-ideas drawer below the idea-loop feed -
4a0ab3d2026-05-08 feat(fleet): Mac1+Mac2 dashboard bridge on corridor homepage strip -
4927f542026-05-07 feat(news): "Hot only" toggle on /news.html filter bar -
df2073d2026-05-07 feat(coverage): per-type accept-rate mini-bars in idea-loop section -
faa570d2026-05-07 feat(coverage): /api/ideas/stats + accept-rate line on /coverage.html -
c9a00d62026-05-07 feat(coverage): ideas_accepted count in homepage build-status strip -
8172d932026-05-07 feat(news): vertical filter dropdown on /news.html -
b8a21eb2026-05-07 feat(news): news counts in homepage build-status strip -
dab65852026-05-07 feat(coverage): idea-loop feed bridge on /coverage.html -
f9ee5392026-05-07 feat(news): /api/news/by-day + 90-day calendar heatmap on /coverage.html -
4035b3c2026-05-07 feat(news): news-recency badge on /pitches.html name column -
ebcb5c82026-05-07 feat(news): /api/news/archive + /news/archive.html weekly history view -
888783b2026-05-07 feat(news): /api/news/sources + source-domain bar chart on /coverage.html -
4b74df32026-05-07 feat(news): summarizer pre-flight + 4-hour poll launchd -
0f242272026-05-07 feat(news): /api/news/leaderboard + Top publishers section on /coverage.html -
67f75142026-05-07 feat(news): recency badge (Hot/Fresh/Stale) on map detail panel -
3b4d5232026-05-07 feat(news): per-business RSS via /news/feed.xml?business=N -
52178df2026-05-07 feat(news): per-vertical RSS feeds via /news/feed.xml?vertical=... -
840b1a42026-05-07 fix(news): hardenings caught by codex-2way round 1 audit -
90278322026-05-07 feat(seo): include news pages in /sitemap.xml -
33b09e92026-05-07 feat(news): "Latest from their site" section on /magazine/:id -
ec894292026-05-07 feat(news): /api/news/by-vertical + heatmap on /coverage.html -
5afdd262026-05-07 feat(news): /business/:id/news per-business news firehose page -
b329ecf2026-05-07 feat(news): RSS feed at /news/feed.xml + auto-discovery on /news.html -
4cada622026-05-07 feat(news): news_count badge on /chains.html bar rows -
d4444352026-05-07 fix(generate_features): tighten banned-phrase regex to catch bare 'X Meets Y' -
7594bb32026-05-07 fix(security): escape source_url + scheme-validate href in /news rail (XSS) -
04183312026-05-07 feat(news): news_count badge on /buildings.html tenant rows -
fd037672026-05-07 feat: apply_exa_websites.ts — merge Exa-discovered website JSON into businesses table w/ aggregator filter (yelp/fb/yp/ig blocked) -
8afd9392026-05-07 feat(news): per-business news rail on map detail panel + news_count column -
00f39c02026-05-07 fix(generate_features): post-filter retry on banned headline phrases -
549a1dd2026-05-07 tighten .gitignore: add missing standing-rule patterns (dist/ build/ .next/) -
e7c94652026-05-07 fix(generate_features): forbid 'multi-tenant building' boilerplate in headlines -
e45334e2026-05-07 feat(news): /this-week.html editorial digest + /api/news/digest -
828ab332026-05-07 feat(enrich): website-discovery resilience + category prioritization -
0a78f9e2026-05-07 feat(news): nightly launchd jobs + patient summarizer backoff -
e8b2f732026-05-07 feat(news): /api/news/recent + /news.html magazine surface -
24223752026-05-07 feat(news): per-business news/blog/press scraper + qwen3 summarizer -
d6877332026-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) -
04185a92026-05-07 fix(launchd): strip broken 2>&1 inline redirect from 11 plists -
c6a6ecd2026-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 -
ee200d82026-05-07 feat: migration 019 + 5 starter LinkedIn post drafts (corridor commentary x2, project reveal x2, behind-the-scenes x1) -
64555b12026-05-07 fix: migration 018 — adds magazine_features.taps + taps_breakdown for engagement tracking -
d817e362026-05-07 feat: 9 AM daily corpus-summary email — totals + cover + top tapped + latest gen, complements 8:07 morning_feature_email -
e145ce42026-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 -
d607f2b2026-05-06 iter 177: SESSION_LOG addendum 2 — prompt-rewrite win confirmed (7/7 fresh headlines on new positive patterns, zero on blocked templates) -
a52bd712026-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 -
5d90ce12026-05-06 iter 175: SESSION_LOG addendum — captures iters 171-175 + the regen demotion bug catch -
02da0a22026-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 -
88857c92026-05-06 iter 173: auto-dedupe now also catches trigram-templated headlines (3+ shared 3-word phrases) on top of exact dupes -
317fdcf2026-05-06 iter 172: /api/magazine/similar/:id — pg_trgm word_similarity over first 480 chars; surfaces editorial-body templating beyond headline-level -
a55be3f2026-05-06 iter 171: SESSION_LOG_2026-05-06.md — 40-iter overnight debrief in plain English -
59b0a5e2026-05-06 iter 170: /calendar.html + /api/magazine/calendar — GitHub-style 90-day heatmap of feature gen rate -
bfb00de2026-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.) -
59418a92026-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 -
6a0afbb2026-05-06 iter 167: /api/magazine/headline-stems + /duplicates.html templated-phrases panel (5× 'precision care encino', 5× 'legal precision meets', etc.) -
1efb2502026-05-06 iter 166: /duplicates.html admin viewer — keeps oldest, click-regen runner-ups for variety -
680bcb72026-05-06 iter 165: /api/magazine/duplicates + nav pill — surfaces qwen3 headline-templating (5 groups affecting 14 features today) -
48b33422026-05-06 iter 164: morning_feature_email now includes 'Suggested next publish' block — draft-pick-of-the-night -
280b6792026-05-06 iter 163: /api/magazine/draft-pick-of-the-night + 'Publish next' rail on today.html (heuristic-scored) -
fcadf642026-05-06 iter 162: /api/magazine/:id/social-pack.json — programmatic JSON twin of ad-pack.html (5 brief variants + tags + asset URLs) -
b39e8cf2026-05-06 iter 161: /health.html — pipeline-wide one-page status w/ pipeline+top-tapped+growth panels, refresh 30s -
4fc18b82026-05-06 iter 160: /issue.txt — plain-text dump of full issue (50KB, accessible/AI-ingestible) -
4b6a73d2026-05-06 iter 159: /api/magazine/gen-queue + 'Up next' rail on today.html — surfaces what watchdog will tackle -
52e164c2026-05-06 iter 158: coverage_snapshots table + nightly 11:55 PM job + /api/coverage/history + SVG growth line on /coverage.html -
8fee7062026-05-06 iter 157: /api/magazine/feature-of-the-month — manual cover wins, falls back to auto-pick heuristic -
37c543b2026-05-06 iter 156: /api/magazine/quality-flags audit + nav-pill on today.html (green ✓ when zero flags) -
1feb0f92026-05-06 iter 155: /coverage.html + /api/coverage — corridor coverage report (0.4% so far), 14-day spark, by-vertical/by-city bars -
a80fd422026-05-06 iter 154: /magazine/:id/ad-pack.html — short/medium/long copy-ready brief variants for biz owners + reader-page link -
b1955e02026-05-06 iter 153: today.html ticker rail — '/api/magazine/activity' last-12 with icons + relative-time -
fa496552026-05-06 iter 152: /api/magazine/activity unified event feed across gen/publish/cover/feedback/inquiries -
0c9c3922026-05-06 iter 151: /quotes — typographic 3-col wall of every editorial pull quote w/ size variation -
6dc50d02026-05-06 iter 150: /magazine.html 🆕 since-last filter — localStorage stamp + unseen-count badge -
f0509692026-05-06 iter 149: per-feature editor margin notes — collapsible "add note" textarea on /magazine.html w/ blur-save -
a9953cd2026-05-06 iter 148: /api/magazine/lottery?n=N&cat= — random N features for rotators/IG backup picks -
af14cb72026-05-06 iter 147: /api/magazine.json now accepts ?cat= and ?limit= for per-vertical JSON Feed consumers -
a4769a42026-05-06 iter 146: auto-cover job — promotes top-tapped feature to cover when none set; nightly 2:42 AM -
3f9a7622026-05-06 iter 145: /issue/archive — back-issues index w/ cover-image previews + Archive link in toolbar -
4c4257c2026-05-06 iter 144: cover-image upload — multer endpoint + /covers static + cover-picker.html + image-aware /issue hero -
d1f48832026-05-06 iter 143: feedback POST → George email Steve on typo/correction/tip/complaint (skip praise/note) -
6ffa8b02026-05-06 iter 141: category emoji map on /scoreboard — 36 verticals mapped, fallback ✦ -
548f05d2026-05-06 iter 140: daily-standup audio summary — Samantha TTS + m4a + /standup viewer + 7:50 AM launchd -
4aa97412026-05-06 iter 139: /issue.epub export — pure-zip EPUB 2 with cover, TOC, 50 chapters, archiver dep -
8dc6d372026-05-06 iter 138: monthly cover picker — magazine_issues table + GET/PATCH /api/issues + ⛧ Cover button on /magazine.html + cover hero on /issue -
80f8f9f2026-05-06 iter 137: /admin/ig-drafts mount + today.html nav link to browse generated drafts -
9b315a52026-05-06 iter 136: launchd for ig-dryrun at 8:02 AM daily — preview tomorrow's IG draft before manual post -
85d37312026-05-06 iter 135: IG-autopost dry-run job — writes daily JSON+HTML drafts under tmp/ig-drafts/, no posting yet -
f40d96e2026-05-06 iter 134: /api/magazine/today unified payload — feature + caption + tags + share/audio for IG-autopost -
97c9f082026-05-06 iter 133: 404 page now suggests a random published feature -
030b0b12026-05-06 iter 131-132: /feedback.html admin viewer + today.html unread pill + PATCH endpoint -
04c6a952026-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 -
5af0acf2026-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) -
26b38e12026-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 -
ead33e92026-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) -
bd681e72026-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 -
93a2f1a2026-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 -
e2338c12026-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 -
cfd5c5a2026-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 -
0bc6fd62026-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 -
97d5a132026-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) -
715edc12026-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 -
a45aad12026-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 -
fa94a302026-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) -
3bb691b2026-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 -
18a9a292026-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/* -
b3c77b42026-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 -
07a36d72026-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 -
21beb482026-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 -
bdb5ece2026-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 -
082198f2026-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 -
e6c8c012026-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 -
c6665322026-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 -
3f4099c2026-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 -
e460f062026-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 -
076d5092026-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 -
f5e15fe2026-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 -
3836ac02026-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 -
d7cd1462026-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 -
7037e0a2026-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 -
34f78f52026-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 -
c229baf2026-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 -
e31d8bb2026-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 -
78b1bea2026-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 -
1daefa62026-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 -
67f22562026-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 -
1482b8f2026-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 -
d0c4c912026-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 -
140c1a62026-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) -
43adaa82026-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 -
eb0c9982026-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) -
45573472026-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 -
10506e02026-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 -
e654bdf2026-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 -
1c3e46f2026-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 -
31c42432026-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) -
40a7c212026-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) -
6dbbf082026-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 -
82a93bd2026-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 -
c92b71f2026-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 -
ae064192026-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 -
db3c21d2026-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 -
65148eb2026-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 -
8620d262026-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 -
410b8962026-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 -
c5a0c972026-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 -
dbf656b2026-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 -
69ba8fb2026-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 -
368cc0c2026-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 -
f3c43c02026-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) -
ce211192026-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 -
36dcc742026-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 -
b340c7a2026-05-05 Ship v1 public · UI-agent fixes + screenshot billboards + wall + cam tune -
91c0ed02026-05-05 Initial commit · Ventura corridor v1 · 224 businesses · 113 audits · 3 surfaces (2D/3D/wall)
Authors
- Steve190
- ventura-corridor2
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
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
- link
- .env.example
- .gitignore
- LINKEDIN_3MONTH_CALENDAR.md
- LINKEDIN_COMPLETE_MASTER_FOLLOW_LIST.md
- LINKEDIN_EXPANDED_CALENDAR_7TIERS.md
- LINKEDIN_INDUSTRY_MASTER_FOLLOW_LIST.md
- LINKEDIN_OUTREACH_TRACKER.csv
- LINKEDIN_PITCH_LIBRARY.md
- LINKEDIN_POST_TEMPLATES.md
- LINKEDIN_QUICK_REFERENCE.md
- LINKEDIN_START_TODAY.md
- LINKEDIN_STRATEGY_ACTIONPLAN.md
- LINKEDIN_VENTURA_DW_INMAILS.md
- LINKEDIN_WEEKLY_SCHEDULE.md
- README.md
- README_LINKEDIN.md
- SESSION_LOG_2026-05-06.md
- STATUS_NEWS_PIPELINE.md
- data/3d-cam-fix.png
- data/3d-snapshot.png
- data/3d-with-screenshots.png
- data/ad-signals-2026-06-14.csv
- data/ad-signals-2026-06-14.json
- data/ad-signals-2026-06-15-deduped.csv
- data/ad-signals-2026-06-15-deduped.json
- data/after-review-2d.png
- data/after-review-3d-post-overlay.png
- data/after-review-3d.png
- data/after-review-wall.png
- data/census/census-corridor.jsonl
- data/census/census-tarzana-91356.jsonl
- data/census/la-licenses-ventura-corridor.jsonl
- data/census/la-licenses-ventura.json
- data/census/match-census.py
- data/census/tarzana-91356-yelp.json
- data/census/yelp-corridor-all.jsonl
- data/dw-master-list.csv
- data/dw-pitch-list.csv
- data/magazine-audio/1-6e3ce8d703a7.aiff
- data/magazine-audio/1-6e3ce8d703a7.m4a
- data/photos/ventura-films.json
- data/photos/wikimedia-manifest.json
- data/pitches/16051-manor-home-staging.md
- data/pitches/16442-all-flooring.md
- data/pitches/16625-nnl-home-remodeling.md
- data/pitches/17441-style-within.md
- data/pitches/3122-catalina-paint.md
- data/pitches/3693-all-american-softy.md
- data/pitches/4348-an-inside-job.md
- data/pitches/6538-alla-moda-furniture.md
- data/pitches/7364-a1-carpet-market.md
- data/pitches/7379-payandeh.md
- data/pitches/741-decor-art-galleries.md
- data/pitches/9129-blue-revolver.md
- data/pitches/9858-american-icon.md
- data/pitches/_TEMPLATE.md
- data/public-3d.png
- data/ventura-blvd-polyline.json
- data/wall-snapshot.png
- db/002_gallery_tiers.sql
- db/migrate.ts
- db/migrations/001_init.sql
- db/migrations/002_enrichment.sql
- db/migrations/003_chamber_memberships.sql
- db/migrations/004_headlines.sql
- db/migrations/005_headline_voice.sql
- db/migrations/006_crawl_blocked.sql
- db/migrations/007_ad_signals.sql
- db/migrations/008_pitches.sql
- db/migrations/009_linkedin.sql
- db/migrations/010_response_tracking.sql
- db/migrations/011_pitch_daily_snapshot.sql
- db/migrations/012_pitch_event_log.sql
- db/migrations/013_building_roster.sql
- db/migrations/014_building_dupes.sql
- db/migrations/015_dupe_merge.sql
- db/migrations/016_btrc_match.sql
- db/migrations/016_magazine.sql
- db/migrations/017_magazine_addons.sql
- db/migrations/018_magazine_engagement.sql
- db/migrations/019_linkedin_pitch_cols.sql
- db/migrations/020_pitches_query_columns.sql
- db/migrations/021_news_items.sql
- db/migrations/022_appointments.sql
- db/migrations/023_marketplace.sql
- db/pool.ts
- db/seed_segments.sql
- dw-shopify-clients.json
- ecosystem.config.cjs
- exports/linkedin-follow-targets.csv
- exports/paid-ads-targets-2026-05-05.csv
- exports/sales-pitch-2026-05-05.html
- exports/shopify-appointment-embed.html
- launchd/com.steve.ventura-corridor-news-scrape.plist
- launchd/com.steve.ventura-corridor-news-summarize-poll.plist
- launchd/com.steve.ventura-corridor-news-summarize.plist
- launchd/com.steve.ventura-gallery-3am.plist
- linkedin-3month-calendar.html
- linkedin-activity-ledger.html
- linkedin-marketing-calendar.html
- linkedin-tracker-dashboard.html
- linkedin-ventura-follow-dashboard.html
- package-lock.json
- package.json
- public/3d.html
- public/about.html
- public/atlas.html
- public/buildings.html
- public/calendar.html
- public/chains.html
- public/cover-picker.html
- public/coverage.html
- public/crawl-derby.html
- public/duplicates.html
- public/dw-radar.html
- public/eulogy.html
- public/favicon.svg
- public/feedback.html
- public/find.html
- public/gallery.html
- public/headlines.html
- public/health.html
- public/hero.html
- public/index.html
- public/linkedin.html
- public/magazine.html
- public/marketplace.html
- public/models/vehicles/LICENSE.md
- public/models/vehicles/bus-city.glb
- public/models/vehicles/bus-school.glb
- public/models/vehicles/delivery.glb
- public/models/vehicles/hatchback-sports.glb
- public/models/vehicles/sedan-sports.glb
- public/models/vehicles/sedan.glb
- public/models/vehicles/suv.glb
- public/models/vehicles/taxi.glb
- public/models/vehicles/van.glb
- public/news.html
- public/news/archive.html
- public/pantone.html
- public/pitches-map.html
- public/pitches.html
- public/postcard-template-back.html
- public/postcard-template-front.html
- public/postcards.html
- public/rate-card.html
- public/responses.html
- public/sales-nav-signup.html
- public/scoreboard.html
- public/search.html
- public/signals.html
- public/sponsor.html
- public/theme.css
- public/theme.js
- public/this-week.html
- public/timeline.html
- public/today.html
- public/tongues.html
- public/upgrade.html
- public/walk-route.html
- public/wall.html
- scripts/ad_signals_dedup.cjs
- scripts/ad_signals_report.cjs
- scripts/audit-tabs.cjs
- scripts/build-virtual-gallery.sh
- scripts/debug-social-bb.cjs
- scripts/fill-tier-copy.cjs
- scripts/find-social-bb.cjs
- scripts/fix-launchd-redirect-bug.sh
- scripts/foursquare_signup.cjs
- scripts/generate-gallery-pieces.js
- scripts/hawk-local.sh
- scripts/import_social_handles.cjs
- scripts/post_crawl_pipeline.sh
- scripts/probe_adst.ts
- scripts/probe_adst2.ts
- scripts/purelymail_alias.cjs
- scripts/push-news-snapshot.sh
- scripts/ventura_films.cjs
- scripts/verify_adst_patch.ts
- scripts/wikimedia_ventura_photos.cjs
- src/crawl/front_page.ts
- src/crawl/news_scraper.ts
- src/enrich/ad_signals.ts
- src/enrich/ads_transparency.ts
- src/enrich/apply_exa_websites.ts
- src/enrich/btrc_match.ts
- src/enrich/chambers.ts
- src/enrich/classify_ownership.ts
- src/enrich/extract_headlines.ts
- src/enrich/gtm_followthrough.ts
- src/enrich/linkedin_via_brave.ts
- src/enrich/restaurant_owners.ts
- src/enrich/score_headlines.ts
- src/enrich/score_headlines_batch.ts
- src/enrich/score_headlines_heur.ts
- src/enrich/scrape_contacts.ts
- src/enrich/website_via_brave.ts
- src/enrich/website_via_ddg.ts
- src/enrich/website_via_exa.ts
- src/ingest/foursquare.ts
- src/ingest/here_discover.ts
- src/ingest/la_btrc.ts
- src/ingest/osm_overpass.ts
- src/jobs/auto_cover.ts
- src/jobs/auto_dedupe_headlines.ts
- src/jobs/auto_publish_top.ts
- src/jobs/coverage_snapshot.ts
- src/jobs/daily_snapshot.ts
- src/jobs/daily_standup_audio.ts
- src/jobs/find_photos.ts
- src/jobs/generate_features.ts
- src/jobs/ig_autopost_dryrun.ts
- src/jobs/magazine_watchdog.ts
- src/jobs/morning_corpus_summary.ts
- src/jobs/morning_feature_email.ts
- src/jobs/normalize_categories.ts
- src/jobs/regen_thin.ts
- src/jobs/seed_pitches.ts
- src/jobs/summarize_news.ts
- src/jobs/weekly_digest.ts
- src/score/seo_score.ts
- src/server/appointments/calendar.ts
- src/server/appointments/embed-loader.ts
- src/server/appointments/oauth.ts
- src/server/appointments/router.ts
- src/server/appointments/slots.ts
- src/server/appointments/widget.ts
- src/server/index.ts
- tmp/ig-drafts/2026-05-07.html
- tmp/ig-drafts/2026-05-07.json
- tmp/ig-drafts/index.html
- tsconfig.json