{"slug":"ventura-corridor","total":192,"limit":100,"offset":0,"since":null,"commits":[{"hash":"28b0f74","date":"2026-07-13 10:54:32 -0700","author":"Steve Abrams","subject":"auto-save: 2026-07-13T10:54:28 (1 files) — scripts/push-news-snapshot.sh","body":""},{"hash":"e4101a6","date":"2026-07-13 10:40:33 -0700","author":"Steve Abrams","subject":"move tsx to runtime dependencies — prod launches via tsx, --omit=dev deploy pruned it","body":""},{"hash":"a49ac4f","date":"2026-07-13 10:29:36 -0700","author":"Steve Abrams","subject":"feat: explicit 'Source: <site>' credit on news + archive cards (Steve 2026-07-13: crawled items must credit the client site)","body":"Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"},{"hash":"d2e66de","date":"2026-07-13 09:47:54 -0700","author":"Steve Abrams","subject":"fix: Ollama fallbacks pointed at dead hosts (mac2 tailnet IP, offline Mac1 LAN) — default to local; preflight 15s->60s for cold model load","body":"Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"},{"hash":"9a05162","date":"2026-07-03 08:08:18 -0700","author":"Steve","subject":"chore: macstudio3 migration — reconcile from mac2 + repoint paths (stevestudio2→macstudio3, node/npm/npx→/opt/homebrew)","body":""},{"hash":"238031f","date":"2026-06-24 16:28:02 -0700","author":"Steve","subject":"LinkedIn dashboard: cap table at 500 rows for snappy 2,860-record list + confirm guard on Mark All Visible","body":""},{"hash":"ce8201c","date":"2026-06-24 07:50:13 -0700","author":"Steve","subject":"LinkedIn dashboard: add 2,591 DW trade clients as a source + source filter (Ventura/Clients)","body":""},{"hash":"45f5855","date":"2026-06-24 07:40:10 -0700","author":"Steve","subject":"LinkedIn dashboard: row select + paced batch-open follow + No-LinkedIn skip","body":""},{"hash":"10268df","date":"2026-06-23 15:44:23 -0700","author":"Steve","subject":"Add LinkedIn marketing calendar — 33 content pieces: 5 Events, 4 Newsletter issues, 8 Company posts, 8 Personal posts with full copy","body":""},{"hash":"c6254fa","date":"2026-06-23 15:30:05 -0700","author":"Steve","subject":"Add LinkedIn activity ledger — quick-entry log with stats, filters, CSV export","body":""},{"hash":"ff0ed97","date":"2026-06-23 15:15:26 -0700","author":"Steve","subject":"Add LinkedIn pitch library — posts, comment openers, InMails, objection responses by tier","body":""},{"hash":"d2f7687","date":"2026-06-23 15:01:43 -0700","author":"Steve","subject":"Add Ventura Blvd LinkedIn follow dashboard + InMail templates + DB export","body":"- exports/linkedin-follow-targets.csv: 269 Ventura Blvd businesses (NAICS 541400/541410/541310/541320/531210/236200/236100/237000) from ventura_corridor DB\n- 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\n- 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\n\nCo-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>"},{"hash":"b0d3e43","date":"2026-06-23 14:52:25 -0700","author":"Steve Abrams","subject":"auto-save: 2026-06-23T14:52:21 (2 files) — LINKEDIN_COMPLETE_MASTER_FOLLOW_LIST.md LINKEDIN_EXPANDED_CALENDAR_7TIERS.md","body":""},{"hash":"71bca45","date":"2026-06-23 14:22:14 -0700","author":"Steve Abrams","subject":"auto-save: 2026-06-23T14:22:11 (3 files) — LINKEDIN_3MONTH_CALENDAR.md LINKEDIN_INDUSTRY_MASTER_FOLLOW_LIST.md linkedin-3month-calendar.html","body":""},{"hash":"f441e59","date":"2026-06-23 13:52:06 -0700","author":"Steve Abrams","subject":"auto-save: 2026-06-23T13:52:03 (3 files) — LINKEDIN_OUTREACH_TRACKER.csv LINKEDIN_WEEKLY_SCHEDULE.md linkedin-tracker-dashboard.html","body":""},{"hash":"021eaf4","date":"2026-06-23 12:58:58 -0700","author":"Steve Abrams","subject":"Add LinkedIn strategy index/overview (complete package ready for action)","body":""},{"hash":"eecce2a","date":"2026-06-23 12:57:57 -0700","author":"Steve Abrams","subject":"Add LinkedIn start guide and quick-start reference (4 total strategy docs)","body":""},{"hash":"a8515aa","date":"2026-06-23 12:57:22 -0700","author":"Steve Abrams","subject":"Add comprehensive LinkedIn strategy package: 28 accounts to follow, discovery targets, post templates, daily cadence","body":""},{"hash":"c1039b4","date":"2026-06-15 07:28:16 -0700","author":"Steve Abrams","subject":"ad-signals: canonical-firm dedup pass (fix Panera double-listing); 482/127 -> 446/119","body":"Root cause: pass-1 analyzer keyed firms by raw/firm-*.html file, so multi-location\nchains (Panera 2x, CVS 3x, etc.) counted once per Ventura Blvd storefront.\nNew ad_signals_dedup.cjs collapses by registrable-domain (name/file fallback),\ndrops 5 failed-crawl interstitials, and guards against domain mis-mappings.\nRead-only over the existing artifact; original 06-14 files untouched."},{"hash":"057d452","date":"2026-06-14 20:14:32 -0700","author":"Steve Abrams","subject":"ad-signals: read-only pass-1 ad/social tracker over crawled firm HTML (482 businesses, 127 running paid ads)","body":""},{"hash":"7004f12","date":"2026-06-02 12:44:03 -0700","author":"SteveStudio2","subject":"fix: gallery-pieces 3am job — require('pg') → import (package is type:module, was throwing ReferenceError nightly since May 12)","body":"The launchd com.steve.ventura-gallery-3am step 3 (generate-gallery-pieces.js) crashed\nimmediately on 'require is not defined in ES module scope'. Converted to ESM import;\nkeeps the filename so the plist reference is unaffected. Writes to LOCAL ventura_corridor\nDB (/tmp socket), not prod. Next 3am run will resume gallery generation.\n\nCo-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"},{"hash":"b261b8f","date":"2026-05-31 16:56:05 -0700","author":"SteveStudio2","subject":"Link favicon.svg in head (use branded SVG, not .ico fallback)","body":""},{"hash":"6912794","date":"2026-05-31 00:06:41 -0700","author":"SteveStudio2","subject":"gitignore: add *.orig *.save *.old *~ copy-of-* backup patterns (standing-rule completeness)","body":""},{"hash":"19b5427","date":"2026-05-27 08:42:28 -0700","author":"SteveStudio2","subject":"chore: gitignore all .env/.bak/.plist-backups + drop tracked backup files","body":""},{"hash":"e1392fb","date":"2026-05-24 21:33:52 -0700","author":"SteveStudio2","subject":"ventura-corridor: ship 'POSSIBLY NO BUSINESS LICENSE' badge","body":"3 pieces:\n\n1. db/migrations/016_btrc_match.sql — adds has_btrc_match BOOL,\n   btrc_match_score REAL, btrc_matched_id BIGINT, btrc_match_at on businesses.\n   Partial index on (has_btrc_match) WHERE on_corridor AND source != 'la_btrc'.\n\n2. src/enrich/btrc_match.ts — populates the flag. For each non-la_btrc\n   corridor business with a numeric street_number, find best la_btrc match\n   at same street_number ± 5 (same zip), score = addr_score*0.4 + name_trgm*0.6.\n   has_btrc_match = (score >= 0.5). No candidate → false.\n\n3. src/server/index.ts + public/index.html — surface has_btrc_match in the\n   /api/businesses payload; render a red badge '⚠ POSSIBLY NO BUSINESS LICENSE'\n   next to the business name when source != 'la_btrc' AND has_btrc_match = false.\n\nInitial run on 224 OSM-sourced corridor businesses: 149 matched, 75 flagged.\n\nCensus working files at data/census/:\n  - la-licenses-ventura-corridor.jsonl (17,344 licensed businesses)\n  - yelp-corridor-all.jsonl (372 yelp/google enumerated across 8 zips)\n  - tarzana-91356-yelp.json (34 Tarzana samples from initial probe)\n  - census-corridor.jsonl (joined 406 with license_status per row)\n  - match-census.py (offline cross-match script, mirrors the SQL logic)\n\nTriggered by Steve's directive 2026-05-23 \"using exa, make sure\nventuracorridor has the database of ALL businesses along ventura bl using\nall knowledge out there. many will not have the business license. Mention\nthat in red on the page.\""},{"hash":"cf9f180","date":"2026-05-12 14:23:52 -0700","author":"SteveStudio2","subject":"YOLO tick 6: qwen3 fills tier marketing_copy + /admin/upgrade-leads board","body":"scripts/fill-tier-copy.cjs — backfills marketing_copy JSONB on\ntier_pricing rows where it's NULL. Per-row qwen3:14b generation:\n- System prompt frames qwen3 as a Ventura Corridor copywriter with\n  tone constraints (confident, friendly, no buzzwords, no AI talk)\n- TIER_HINTS guide what features each tier-level (basic/featured/\n  verified/spotlight) typically unlocks so copy stays consistent\n- Strict-JSON output: {title, subtitle, bullets[3-6], cta}\n- Idempotent — only fills NULL rows, re-run safe\n\nFilled all 24 NULL rows in 3:14 (avg ~8s/row). Sample outputs:\n- Hotel Spotlight : 'Stand out with video, priority placement, and\n  exclusive booking tools to drive more guests.'\n- Cafe Featured : 'Showcase your cafe with a logo, photos, and a\n  special badge that makes you stand out in searches.'\n- Salon Spotlight : 'Get featured with video, bookings, and priority\n  placement to stand out on Ventura Boulevard.'\n\nThe /upgrade/:bizId page now hydrates with category-specific bullets\ninstead of the generic TIER_COPY fallback strings.\n\nNEW admin pages, both behind the existing Basic-auth gate (ADMIN_PATHS\nregex already protects /signals, /pitches, /linkedin — added matching\n/admin/upgrade-leads + /api/admin/upgrade-leads):\n\nGET /api/admin/upgrade-leads — JSON list of upgrade_intents joined\n  with businesses + tier_pricing. Last 500.\n\nGET /admin/upgrade-leads — SSR HTML board:\n- Stats: total intents · pipeline MRR if all convert · average ticket\n- Table: when · business (link to /upgrade/:id) · requested tier badge\n  · contact name + mailto: link · notes excerpt · tier price right-aligned\n- Empty state: explains how leads land here\n\nSmoke tests:\n- /admin/upgrade-leads without auth → 401\n- Marketing copy verified populated for all 24 tier rows\n- /api/upgrade/3 (Shanghai Rose) returns all 3 tiers with filled copy\n\nReversible: UPDATE tier_pricing SET marketing_copy = NULL rolls back\nthe copy fill. Routes can be unmounted by removing the regex from\nADMIN_PATHS + the handlers.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"e0407a5","date":"2026-05-12 13:45:26 -0700","author":"SteveStudio2","subject":"YOLO tick 5: /upgrade/:bizId tier ladder page + intent capture","body":"Public per-business upgrade page that reads tier_pricing seeded by the\n3am gallery job. URL pattern: /upgrade/123 (numeric :bizId only — regex\n([0-9]+) in the route prevents catching /upgrade/foo paths).\n\nAPI:\n- GET  /api/upgrade/:bizId — business row + tier_pricing rows for its\n  category, ordered by price_cents ASC\n- POST /api/upgrade/:bizId/intent — captures email + name + notes\n  for buyers wanting to be notified when paid tiers open; persists\n  to upgrade_intents (new table: business_id FK, requested_tier,\n  contact_email, contact_name, notes, ip_addr, created_at)\n\nFrontend (public/upgrade.html):\n- Standard Ventura Corridor chrome: gold logo UL, hamburger UR\n- Business header: eyebrow, name (Playfair), category chip, address chip,\n  optional website chip with deeplink\n- 3-tier cards grid (auto-fit 1-col mobile, 3-col desktop)\n  · Basic: 'Already on Basic' disabled CTA\n  · Featured: gold-bordered card with 'MOST POPULAR' ribbon\n  · Spotlight: gold CTA, Playfair italic accent on tier name\n- Tier marketing_copy reads from JSONB in DB; falls back to TIER_COPY\n  defaults per tier name (basic/featured/spotlight/verified)\n- 'Notify me when this opens' CTA expands an inline intent form (no\n  page navigation, smooth-scroll into view, accessible)\n- Form: email (required), name (optional), notes (textarea)\n- POST → green ✓ toast on success, red ✕ on failure\n- Current-tier badge if biz.tier already matches (green pill)\n\nStripe Connect path NOT wired yet — that's gated on Steve enabling\nConnect on the Designer Wallcoverings account. Intent capture is the\npragmatic stand-in: photographers/restaurateurs sign up now, we email\nthem the moment Stripe is live. No false 'Buy Now' button that 404s.\n\nEnd-to-end smoke test (Shanghai Rose, biz 3, amenity: restaurant):\n- /api/upgrade/3 returns business + 3 tiers (basic free, featured ,\n  spotlight )\n- /upgrade/3 renders the page (HTTP 200)\n- POST intent → upgrade_intents V1 inserted ok\n\nReversible: DROP TABLE upgrade_intents + revert routes if pricing model\nchanges. No external side-effects.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"e648a17","date":"2026-05-12 12:39:34 -0700","author":"SteveStudio2","subject":"ventura-corridor: add /marketplace.html local listings board","body":"Local-only marketplace for the Ventura Boulevard corridor (Sherman Oaks →\nWoodland Hills). Mirrors the spirit of facebook.com/marketplace but is NOT\na mirror of FB data — Meta's TOS, BrandTotal/Voyager precedent, and image\ncopyright on user-uploaded photos rule out scraping. CHECK constraint\nlocks source_platform = 'local' so a future scraper can't silently bypass\nthe compliance gate.\n\nWhat's in:\n- db/migrations/023_marketplace.sql — marketplace_listings table + 16\n  seed rows across 9 categories, source_platform locked to 'local'\n- src/server/index.ts — GET /api/marketplace (filter/sort/search),\n  GET /api/marketplace/categories (counts), POST /api/marketplace/post\n  (neighbor submissions, validation only — no recaptcha yet)\n- public/marketplace.html — dark noir/metal theme matching index.html,\n  logo UL + hamburger UR, grid-search + sort + density slider per\n  standing rule, category chip bar, detail modal, post-listing modal,\n  FB-Marketplace deeplink footer (plain hyperlinks — allowed)\n- public/index.html — added Marketplace link to the homepage nav stack\n\nPublic, no admin gate. Server stays bound to 127.0.0.1.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"4598d33","date":"2026-05-12 11:25:51 -0700","author":"SteveStudio2","subject":"YOLO tick 1: /gallery — geo-walk horizontal scroll of qwen3 art pieces","body":"Public page at /gallery.html consumes /api/gallery/pieces (also new).\nMounts BEFORE the admin gate so it's reachable without auth.\n\nAPI:\n- GET /api/gallery/pieces?limit=N&neighborhood=...\n  Returns gallery_pieces joined with businesses for name/category/address,\n  ordered by position_pct ASC NULLS LAST (east-to-west: Encino → Studio City)\n- GET /api/gallery/stats\n  Returns total + count per neighborhood\n\nFrontend (gallery.html):\n- Sticky top bar: corridor logo UL + live counter (pieces / miles / current\n  neighborhood) + hamburger UR. Matches the standing 'logo UL + hamburger UR\n  on every page' rule.\n- 90vh hero with title 'The Blvd Gallery' in Playfair Display italic,\n  'Start the walk' CTA scrolls rail to leftmost piece.\n- Horizontal scroll rail with scroll-snap-type: x mandatory. Each piece is\n  380px wide on desktop, 78vw on mobile. Card layout: 4:5 canvas with\n  diagonal gradient from the 3-hex color_palette (placeholder until SDXL),\n  meta panel below with title (Playfair italic accent on key word), medium·\n  year subtitle, prose description, business attribution divider.\n- Neighborhood dividers: '1. Encino', '2. Sherman Oaks', '3. Studio City'\n  appear between zones with position_pct label.\n- Fixed bottom geo-bar: 'Encino — track — Studio City' with gold progress\n  filling left-to-right as user scrolls. URL updates to ?pos=N for\n  shareability. Header neighborhood label syncs on scroll.\n- Keyboard: arrow keys scroll the rail. Mobile: native swipe.\n\nSmoke test: /gallery.html → 200, /api/gallery/pieces → 200, returns the\n1 piece already generated (Crimson Blooms No. 12 · mural · Shanghai Rose\n· Studio City). 3am job will fill the rail with 2000 more.\n\nPer Steve's standing 'NEVER expose AI software' rule: piece cards display\nno vendor name; only the metadata + 'corridor design system' generator\nattribution (and even that is in the DB, not user-visible UI).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"f95cfa2","date":"2026-05-12 11:20:15 -0700","author":"SteveStudio2","subject":"Virtual Blvd Gallery + 3-tier business store · 3am autonomous build","body":"Per Steve via debate-team-fast: virtual art gallery aggregating imagined\nart per Ventura Blvd business + tiered listing store. Runs autonomously\nat 03:07 local tonight via launchd.\n\nNEW Postgres schema (002_gallery_tiers.sql, applied):\n- businesses extended: tier (default basic), stripe_customer_id,\n  stripe_subscription_id, tier_expires_at\n- gallery_pieces: one row per business, AI-generated art metadata\n  (art_title, art_medium, art_year_est, art_description, color_palette\n  JSONB, lon/lat, neighborhood, position_pct 0-100 east-to-west,\n  generator='corridor-design-system' — never names vendor per Steve's\n  'NEVER expose AI software' rule)\n- tier_pricing: per category × tier (basic/featured/spotlight) with\n  price_cents + display_name + marketing_copy JSONB\n- tier_assets: business uploads (logo, photo, video_url, chef_bio,\n  booking_url, bar_number)\n- tier_leads: premium-tier lead-magnet captures\n\nscripts/generate-gallery-pieces.js — qwen3:14b art generator:\n- Priority queue: restaurants > cafes > salons > hotels > lawyers > etc.\n- For each business: structured-JSON prompt → art_title, art_medium,\n  art_year_est, art_description, 3-hex color_palette\n- INSERT ON CONFLICT (business_id) DO NOTHING — idempotent\n- Position mapping: lon -118.510 (Encino) → 0%, -118.380 (Studio City)\n  → 100%, so the gallery scrolls east-to-west naturally\n- Neighborhood derived from lon thresholds\n\nscripts/build-virtual-gallery.sh — orchestrator:\n- Applies schema migration (idempotent IF NOT EXISTS)\n- Seeds tier_pricing (24+ rows, ON CONFLICT DO NOTHING)\n- Runs the qwen3 generator for top 2000 businesses by category priority\n- Logs everything to logs/build-virtual-gallery-<date>.log\n- Writes final stats to logs/build-virtual-gallery-summary.txt\n\nlaunchd/com.steve.ventura-gallery-3am.plist (installed to\n~/Library/LaunchAgents/ + loaded):\n- StartCalendarInterval: hour=3, minute=7 (avoid the :00 / :30 cron\n  herd per cron best practice)\n- PATH includes homebrew + postgresql@14 so psql + node + ollama all\n  resolve\n- StandardOut/Err logged to project logs/\n\nSmoke test result: 1 business generated end-to-end in ~10 seconds.\nSample: 'Crimson Blooms, No. 12' · mural · Studio City · pos 88.78%\n· palette [#9E1B32, #F06292, #F5F5F5].\n\nReversible: TRUNCATE gallery_pieces RESTART IDENTITY rolls back the\nentire art set. tier_pricing/tier_assets/tier_leads safe to drop too.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"0900593","date":"2026-05-12 06:50:24 -0700","author":"SteveStudio2","subject":"docs(appointments): Shopify embed snippet + copy-paste instructions","body":"Self-contained HTML page at exports/shopify-appointment-embed.html with\nthe <div id=appt-widget data-owner=...> + <script src=...embed/appointments.js>\nsnippet, plus admin-side instructions for Shopify Pages source view,\nsandbox/CSP notes, and a per-owner OAuth reminder."},{"hash":"b66894f","date":"2026-05-12 06:50:13 -0700","author":"SteveStudio2","subject":"feat(appointments): self-contained Smart Scheduling widget + Google Calendar OAuth","body":"- Schema (022_appointments.sql): appt_appointments, appt_availability,\n  appt_oauth_tokens, appt_oauth_state. Seeds info@designerwallcoverings.com\n  for Mon-Fri 9am-5pm PT @ 30-min slots.\n- Router mounted ahead of admin gate + static handler. Public routes:\n  /book, /embed/appointments.js, /appointments/:id, /api/appointments/{health,\n  availability,book,oauth/start,oauth/callback,oauth/status,oauth/revoke}.\n  /api/appointments/list is Basic-Auth gated.\n- OAuth helpers are fetch-based; no googleapis dep. Refresh logic auto-\n  rotates tokens 2 min before expiry. State nonce is single-use.\n- Slot computer honors America/Los_Angeles + filters both DB-booked and\n  Google freeBusy windows; race-protects book POST.\n- Widget HTML is pure vanilla JS; reuses the noir/metal-gold token palette\n  already used by public/index.html. Supports ?embed=1 chromeless mode.\n- Embed loader (/embed/appointments.js) lazy-mounts via IntersectionObserver,\n  sandboxes the iframe (allow-scripts allow-forms allow-same-origin allow-popups),\n  serves CORS:* + frame-ancestors:*.\n- Branding rule respected: 'Smart Scheduling' label everywhere except the\n  Google-hosted consent screen."},{"hash":"75c7314","date":"2026-05-10 00:48:31 -0700","author":"Steve","subject":"task #4: source CC0 glTF bus + car models (Kenney Car Kit + Quaternius Public Transport)","body":""},{"hash":"ff0e4d0","date":"2026-05-08 01:32:27 -0700","author":"SteveStudio2","subject":"fix: build-status ideas_rejected wiring + collateral damage from earlier replace_all","body":"Tick 80 added ideas_rejected to /api/build-status but my Edit failed\nthe \"string not found\" check, so out.ideas_rejected = ideaRejects was\nreferencing an undeclared variable. Fixed by adding the rejects.jsonl\nread alongside accepted in the same try-block.\n\nAlso un-broke the build-status handler that got collateral damage from\nthe tick-74 replace_all=true on `process.env.HOME ||` — that swap\ninserted a `return res.json({count: 0, rows: [], note: 'home dir\nunresolvable'})` line into a NON-ideas endpoint, killing /api/build-status.\nReplaced the dual-endpoint home-check with an inline `if (home)` guard\nthat doesn't early-return.\n\nBoth /api/ideas/{recent,rejects} verified still serving (24/104).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"b552dfe","date":"2026-05-08 01:31:28 -0700","author":"SteveStudio2","subject":"feat(coverage): expose ideas_rejected on homepage strip alongside accepted","body":""},{"hash":"e4ee322","date":"2026-05-08 01:12:23 -0700","author":"SteveStudio2","subject":"feat(coverage): \"tonight on the corridor\" glance footer","body":"One-line session summary at the bottom of /coverage.html showing\ncommits / insertion lines / news articles+biz / ideas accepted/total.\nReads /api/session-stats which aggregates git log + news_items +\nideas.jsonl in a single endpoint (60s cache).\n\nVisible at the dramatic black-and-gold footer panel below all the\nexisting analytics sections so Steve sees the run scorecard at the\nend of the page without scrolling out into other tools.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"be002e1","date":"2026-05-08 00:49:10 -0700","author":"SteveStudio2","subject":"fix(coverage): wire JS for rejects drawer (was empty since tick 73)","body":"Tick 73 added the <details> container for rejected ideas but the JS\nthat populates #idea-rejects didnt get inserted because the Edit\nstring match failed. Now properly fetches /api/ideas/rejects and\nrenders the same 3-column grid (score / type / title+body) but with\na red score color to visually distinguish from accepted.\n\nCurrently 91 rejects across types: 31 connection, 24 product, 19\nautomation, 16 rename, 1 kill. Top reject scores show how close they\ncame to acceptance (most are <26 of the 28 threshold).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"3853840","date":"2026-05-08 00:35:42 -0700","author":"SteveStudio2","subject":"fix: portable home dir in /api/ideas/{recent,rejects} (caught by Kimi K2.5)","body":"Direct one-shot Kimi audit on the same diff codex-2way reviewed flagged\none issue codex-2way missed: process.env.HOME || /Users/stevestudio2 is\na brittle fallback. On any machine where HOME is unset (and most\nnon-Steve users), the endpoint quietly read from a non-existent path\nor worse a different users directory.\n\nFix: prefer process.env.HOME, fall back to os.homedir(), then graceful\nempty response with a note instead of returning an error.\n\nBoth endpoints (replace-all of the two HOME lines) now share the\nhelper. Smoke confirms /api/ideas/recent + /rejects still serve under\ntheir normal flows.\n\nCross-validation between two AI auditors (codex-2way + standalone Kimi)\ngave us 4 real bugs to fix vs 6 false-positive nits to ignore.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"1229e88","date":"2026-05-08 00:28:29 -0700","author":"SteveStudio2","subject":"fix: 3 bugs caught by codex-2way audit at tick 73","body":"PROSECUTOR (Kimi K2.5 cloud) audit on the 17-commit batch since\ntick 49 found three real issues. All fixed:\n\n1. public/index.html:353 — fleet panel ternary \"escapeHtml ? escapeHtml(...)\"\n   was unnecessary but legal (escapeHtml is defined at line 204). Removed\n   the dead ternary; also wraps m.label since it comes from network so\n   defense in depth. PROSECUTOR misread this as undefined-fn but flagged\n   the right surface area to harden.\n\n2. src/server/index.ts:541-545 — RSS feed vertical filter was concatenating\n   user input into ILIKE pattern without escaping % or _ wildcards. So\n   ?vertical=% would match every category. Real injection but low impact\n   (read-only, no auth bypass). Now escapes \\%, \\_, \\\\ in tokens before\n   the ILIKE assembly.\n\n3. public/news.html:200 — Hot-only toggle stored Number(r.id) in Set but\n   filtered with String r.business_id — PG bigint always serializes as\n   string, so Set.has missed every row. Set.has now coerces both sides\n   to String. Also slices leaderboard rows to 1000 max as a tab-OOM guard.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"7d1fe85","date":"2026-05-08 00:22:21 -0700","author":"SteveStudio2","subject":"feat(coverage): rejected-ideas drawer below the idea-loop feed","body":"Collapsible <details> below accepted-ideas grid. Shows recently rejected\ndebate-panel ideas (red score, type pill, title + body). Editorial\nsanity-check: are we missing good ideas?\n\nRoutes:\n  GET /api/ideas/rejects   {count, rows[]} from rejects.jsonl, newest\n                           first, top 20. 60s cache.\n\nUI:\n  /coverage.html idea-section   collapsible \"show recently rejected ideas\"\n                                drawer; styled red-toned to differentiate\n                                from accepted gold.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"4a0ab3d","date":"2026-05-08 00:10:40 -0700","author":"SteveStudio2","subject":"feat(fleet): Mac1+Mac2 dashboard bridge on corridor homepage strip","body":"Live status of both Mac dashboards now shows on the always-visible /\nstrip alongside news + ideas counters. Format:\n  FLEET · Mac Studio 1 · qwen3:14b ⚓ · cpu 38% · ram 57%\n       · Mac Studio 2 · gemma3:12b · cpu 12% · ram 41%\n\nEach is clickable -> opens the corresponding 57 Bel Air / Eldorado\ndashboard. Soft-fails per machine — if Mac1 is unreachable, Mac2 still\nrenders (and shows red OFFLINE link).\n\nRoutes:\n  GET /api/fleet    {macs:[{label,cpu,ram_pct,ollama_models[],\n                           installed_count,ollama_proc,power_mode,\n                           ok,url|error}], ts}\n                    Proxies http://100.94.103.98:9930/api/status (Mac1)\n                    + http://localhost:9931/api/status (Mac2). 10s cache.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"4927f54","date":"2026-05-07 23:00:16 -0700","author":"SteveStudio2","subject":"feat(news): \"Hot only\" toggle on /news.html filter bar","body":"Editors can isolate the 10 hot publishers (fresh_7d >= 3) from the\nfirehose with one click. Toggle button next to the search/sort/vertical\ncontrols; reuses /api/news/leaderboard to fetch the hot biz ID set on\nfirst activation, then filters client-side. Restores to all-rows on\ntoggle off.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"df2073d","date":"2026-05-07 22:54:36 -0700","author":"SteveStudio2","subject":"feat(coverage): per-type accept-rate mini-bars in idea-loop section","body":"Replaces the one-line stats string with a grid: type / horizontal bar\nshowing accept share / \"N/total · pct%\" label. Shows the bias the panel\nhas toward \"connection\" ideas (6/21 = 40%) vs renames (1/11 = 9%) vs\nproducts/automations/kills.\n\nPure client-side render — same /api/ideas/stats endpoint.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"faa570d","date":"2026-05-07 22:49:49 -0700","author":"SteveStudio2","subject":"feat(coverage): /api/ideas/stats + accept-rate line on /coverage.html","body":"Sanity-check the idea-loop debate panel: are we accepting too generously\n(>50% rate) or too harshly (<5%)? Current 7/55 = 12.7% sits in the\nhealthy zone.\n\nRoutes:\n  GET /api/ideas/stats   {accepted, rejected, accept_rate, accepted_by_type{},\n                          rejected_by_type{}}. Reads both jsonl files in the\n                          idea-loop data dir; 60s cache.\n\nUI:\n  /coverage.html idea section   monospace stats line above the score-sorted\n                                feed: \"N accepted · M rejected · P%\n                                accept rate · by type: …\"\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"c9a00d6","date":"2026-05-07 22:44:57 -0700","author":"SteveStudio2","subject":"feat(coverage): ideas_accepted count in homepage build-status strip","body":"Adds the idea-loop accepted-count next to the news counts on the always-\nvisible / strip. Click \"Ideas accepted\" opens the standalone idea-loop\nviewer at :9920 (new tab).\n\nAPI change:\n  /api/build-status   +ideas_accepted (line count of ideas.jsonl in the\n                      idea-loop skill data dir)\n\nUI change:\n  public/index.html   one new b-strip cell next to news counts.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"8172d93","date":"2026-05-07 22:39:06 -0700","author":"SteveStudio2","subject":"feat(news): vertical filter dropdown on /news.html","body":"Adds an \"All categories\" select next to the search and sort controls\non /news.html. Categories are populated from the rendered data so the\ndropdown stays in sync with whatever the API returned (uses prefix\nmatch so picking \"amenity: restaurant\" matches that exact subcategory,\n\"shop\" matches every shop:* subcategory).\n\nPure client-side filter, no server change — /api/news/recent already\nreturns category per row.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"b8a21eb","date":"2026-05-07 22:28:09 -0700","author":"SteveStudio2","subject":"feat(news): news counts in homepage build-status strip","body":"Three new live counters in the always-visible strip across the top of /:\nNews articles | News biz | Fresh ‹7d. Click \"News articles\" to jump\nto /news.html.\n\nAPI change:\n  /api/build-status   +news_total, +news_biz, +news_fresh_7d in counts{}\n\nUI change:\n  public/index.html   3 new b-strip cells; setNews() helper handles\n                      the inner <a> wrapper so the link stays intact\n                      when the count refreshes.\n\nFirst-glance signal: 76 articles / 46 biz / 76 fresh in last 7 days\nright now. Drops automatically as articles age out of the 7-day window.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"dab6585","date":"2026-05-07 22:23:05 -0700","author":"SteveStudio2","subject":"feat(coverage): idea-loop feed bridge on /coverage.html","body":"Read-only bridge to the idea-loop skill so accepted ideas surface in\nthe corridor coverage page alongside the news analytics. Read from\n~/.claude/skills/idea-loop/data/ideas.jsonl on demand, sorted by\nscore_total DESC, top 20.\n\nRoutes:\n  GET /api/ideas/recent    {count, rows[]} from ideas.jsonl. 60s cache.\n                           Graceful \"idea-loop not yet running\" if file\n                           missing.\n\nUI:\n  /coverage.html new \"Idea-loop\" section above News scrape calendar.\n  3-column grid: score / TYPE (color-coded by CONNECTION/PRODUCT/etc.) /\n  title + body excerpt. Header links to the standalone viewer at :9920.\n\nBoth autonomous loops now have a single visual home — coverage.html\nshows news pipeline analytics + creative idea pipeline side by side.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"f9ee539","date":"2026-05-07 22:17:10 -0700","author":"SteveStudio2","subject":"feat(news): /api/news/by-day + 90-day calendar heatmap on /coverage.html","body":"GitHub-style daily scrape heatmap so editors can see at a glance whether\nthe nightly 03:15 cron is landing fresh content vs drifting silent.\n\nRoutes:\n  GET /api/news/by-day    90-day generate_series LEFT JOIN news_items\n                          by fetched_at::date in America/Los_Angeles tz.\n                          10-min cache.\n\nUI:\n  /coverage.html          new \"News scrape calendar\" section above the\n                          existing News heatmap. 14px gold-tinted squares,\n                          5 brightness levels keyed to max-day count.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"4035b3c","date":"2026-05-07 21:42:40 -0700","author":"SteveStudio2","subject":"feat(news): news-recency badge on /pitches.html name column","body":"Editors doing outreach can now spot active publishers in their pitch\nqueue without leaving the table. Three-tier badge inline with the\nbusiness name:\n\n  fresh_7d >= 3   solid gold-glow \"🔥3\" link to /business/<id>/news\n  fresh_7d >= 1   gold outline    \"📰N\" link\n  total >= 1      gray outline    \"📰N\" link  (had news but stale)\n  (no badge)      no scraped news\n\nAPI change:\n  /api/pitches    +news_count, +news_fresh_7d, +last_news_at per pitch row\n                  via single LATERAL aggregate from news_items\n                  (one extra COUNT/GROUP per row, not per item)\n\nUI change:\n  public/pitches.html row template   badge appended to .nm cell\n                                     using the same tier scheme as the\n                                     map detail panel for consistency\n\nCarries the editorial signal \"what's this business saying right now\"\ninto the place where Steve actually decides who to pitch tonight.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"ebcb5c8","date":"2026-05-07 21:31:29 -0700","author":"SteveStudio2","subject":"feat(news): /api/news/archive + /news/archive.html weekly history view","body":"11 distinct ISO weeks of content currently flowing — articles bucketed\nby published_guess (so a 2014 post lands in 2014s"},{"hash":"888783b","date":"2026-05-07 20:58:53 -0700","author":"SteveStudio2","subject":"feat(news): /api/news/sources + source-domain bar chart on /coverage.html","body":"Editorial signal: which platforms host the corridor's published news?\n40 distinct source domains currently flowing — solarunlimited.com,\nbluespa.com, encinomed.org, planetfitness.com, target.com, vbs.org etc.\n\nRoutes:\n  GET /api/news/sources    {host, articles, biz_count} per unique\n                           hostname extracted from news_items.source_url.\n                           Strips www., orders by articles DESC.\n                           Cap 40, 5-min cache.\n\nUI:\n  /coverage.html           new \"Source domains\" bar-section above the\n                           Top publishers leaderboard. Green-toned bars\n                           (vs gold heatmap, metal-glow leaderboard) so\n                           the three sections visually differentiate.\n                           Hostnames link out in new tab.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"4b74df3","date":"2026-05-07 20:26:34 -0700","author":"SteveStudio2","subject":"feat(news): summarizer pre-flight + 4-hour poll launchd","body":"Two changes that let the news summarizer drain naturally when Mac1\nfrees up, instead of burning a 62-min backoff per item or sleeping\nthrough the gap:\n\n1. Pre-flight probe in summarize_news.ts\n   - Sends a tiny \"reply OK\" call before opening the work queue.\n   - If Mac1 returns 503/429 immediately (i.e. the 2000-feature\n     generate_features batch is hammering it), exit 0 cleanly with a\n     \"DEFERRED\" log line.\n   - Saves 60+ minutes of pointless backoff per failed run.\n\n2. Poll launchd com.steve.ventura-corridor-news-summarize-poll\n   - StartInterval 14400 (every 4 hours).\n   - Pre-flight gates work; when Mac1 is busy this fires + immediately\n     exits, costing ~/bin/bash + ~50ms.\n   - The first run after magazine-gen winds down will catch the gap\n     and drain all 76 unsummarized items.\n\nExisting 5:30am daily summarize launchd unchanged — this poll is\nadditive coverage during the magazine-gen marathon (~ 15 more hours\nto go at current 40s/feature rate).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"0f24227","date":"2026-05-07 19:54:30 -0700","author":"SteveStudio2","subject":"feat(news): /api/news/leaderboard + Top publishers section on /coverage.html","body":"Surfaces the corridors most-active publishers — businesses whose blogs/\nnews pages are actually being maintained vs dormant. Pairs with the\nrecency badge from tick 52 — the same Hot threshold ( fresh_7d >= 3 )\ngets a red flame in front of the name on the leaderboard.\n\nRoutes:\n  GET /api/news/leaderboard    {id, name, city, category, total,\n                                fresh_7d, fresh_30d, last_at} per biz,\n                                sorted fresh_7d DESC, fresh_30d DESC,\n                                total DESC. 5-min cache. Cap 50.\n\nUI:\n  /coverage.html   new \"Top publishers\" bar-section after the heatmap.\n                   Shows rank, name (clickable -> /business/:id/news),\n                   city + category, 7d count, 30d count, last-fetched\n                   date. Top 20 displayed.\n\nReal numbers: 10 corridor businesses currently flag Hot, 36 more flag\nFresh. Leaderboard refreshes daily at 03:15 with the next scrape.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"67f7514","date":"2026-05-07 19:11:39 -0700","author":"SteveStudio2","subject":"feat(news): recency badge (Hot/Fresh/Stale) on map detail panel","body":"Editors can spot active corridor publishers at a glance instead of\nscrolling the full news rail. Tiered visual signal:\n\n  Hot    fresh_7d >= 3   solid gold-glow background, \"🔥 HOT\"\n  Fresh  fresh_7d >= 1   gold outline, \"📰 FRESH\"\n  Stale  fresh_30d >= 1  muted gray outline, \"STALE\"\n  (no badge if no news in last 30 days)\n\nAPI change:\n  /api/businesses/:id     +news_stats {fresh_7d, fresh_30d, last_at}\n                          single COUNT(*) FILTER query, no extra LATERAL\n\nUI change:\n  public/index.html showPanel    badge appended to .pn-name h3 inline\n\nAll 46 news-having corridor businesses currently flag — the nightly\n03:15 scrape lands fresh items in news_items so the badges decay\nnaturally as articles age out of the 7-day / 30-day windows.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"3b4d523","date":"2026-05-07 18:28:07 -0700","author":"SteveStudio2","subject":"feat(news): per-business RSS via /news/feed.xml?business=N","body":"Same RSS handler now optionally filters to a single business. Combines\nwith vertical: ?business=49&vertical=shop returns the AND.\n\nRoutes (all on the existing /news/feed.xml):\n  /news/feed.xml                       firehose, 50 most recent\n  /news/feed.xml?vertical=amenity      top-cat filter\n  /news/feed.xml?vertical=shop:bakery  exact subcategory filter\n  /news/feed.xml?business=N            single-business feed\n  /news/feed.xml?business=N&vertical=X intersection (AND)\n\nUI:\n  /business/:id/news <head>            <link rel=alternate type=rss>\n                                       so feed readers auto-subscribe\n                                       when the user opens the page\n  /business/:id/news header            \"📡 RSS subscribe\" anchor next\n                                       to the website link\n\nChannel <title> dynamically suffixes the business_name when filtered\nto one biz, so feed-reader panes label correctly.\n\nSmoke: biz 49 (Solar Unlimited Encino, 4 articles) feed validates\nwith xmllint, channel title contains the business name, /business/49/news\nincludes the subscribe button.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"52178df","date":"2026-05-07 17:56:09 -0700","author":"SteveStudio2","subject":"feat(news): per-vertical RSS feeds via /news/feed.xml?vertical=...","body":"Same RSS handler now optionally filters by OSM category. Steve plugs\ndistinct URLs into NetNewsWire / Feedbin and gets a feed-per-vertical\nwithout polluting his all-corridor stream.\n\nURL forms accepted (all normalized server-side):\n  ?vertical=amenity                  top-cat: matches all amenity:*\n  ?vertical=amenity:restaurant       exact subcategory\n  ?vertical=amenity-restaurant       hyphen alternative\n  ?vertical=amenity:fast_food        underscore allowed\n  ?vertical=amenity:fast%20food      space allowed (percent-encoded)\n\nTokenization: split on [:-_\\s], single token = top-cat ILIKE 'cat: %',\ntwo-or-more tokens = exact match against \"cat: rest of name\".\n\nChannel title gets a (vertical) suffix when filtered so feed-reader\npanes show the right feed name.\n\nDiscovery: 7 <link rel=alternate type=application/rss+xml> tags added\nto /news.html <head> for the popular categories so RSS readers\nauto-detect every flavor when subscribed to /news.html.\n\nReal numbers: amenity 41, fast_food 9, lawyers 8, restaurants 5,\nshop 17, dentists 5, etc. xmllint validates clean.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"840b1a4","date":"2026-05-07 17:47:17 -0700","author":"SteveStudio2","subject":"fix(news): hardenings caught by codex-2way round 1 audit","body":"Two real findings (PROSECUTOR-only, DEFENDER stayed silent both times):\n\n1. /business/:id/news LIMIT 200\n   The news_items query had no LIMIT, so a single business with\n   thousands of articles would be a DoS vector (slow query + huge\n   payload). Capped at 200; existing data tops out at 4 per biz so\n   this is purely future-proofing.\n\n2. baseUrl XML-escape in /sitemap.xml and /news/feed.xml\n   PUBLIC_BASE_URL is env-controlled but not validated. If it ever\n   contained ampersand/quotes/angle brackets, the emitted XML would\n   be invalid and could let an operator-supplied env value inject\n   attributes into <link>/<loc>. Defense in depth - shared escape\n   helper applied at the top of each route.\n\nBoth routes still validate via xmllint; smoke confirms /business/49/news\nreturns 200 with the LIMIT clause in place.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"9027832","date":"2026-05-07 17:06:52 -0700","author":"SteveStudio2","subject":"feat(seo): include news pages in /sitemap.xml","body":"The news pipeline shipped 5 new URL surfaces this session\n(/news.html, /this-week.html, /news/feed.xml-discovery via /news.html,\n/business/:id/news, /coverage.html news heatmap). Add them to the\nsitemap so search engines find them.\n\nStatic pages added:\n  /news.html        hourly  pri 0.9\n  /this-week.html   daily   pri 0.8\n  /coverage.html    daily   pri 0.5\n\nDynamic pages added:\n  /business/:id/news for every biz with at least one news_item\n  Cap 1000, ordered by most-recently-fetched, lastmod = MAX(fetched_at)\n\nReal numbers: 46 /business/:id/news entries flowing on first build.\nxmllint validates clean.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"33b09e9","date":"2026-05-07 16:34:13 -0700","author":"SteveStudio2","subject":"feat(news): \"Latest from their site\" section on /magazine/:id","body":"Closes the loop between editorial features and live scraped news.\nThe magazine article view (/magazine/:id) now includes a 5-item\nnews rail above the building-mates and category-related sections,\nshowing what the business has been actually publishing on their\nown website.\n\nImplementation:\n  - new newsBiz query in the /magazine/:id handler returns last 5\n    news_items for f.business_id\n  - rendered between the existing \"Pipeline events\" details and the\n    \"In the same building\" mates section\n  - safeUrl scheme-validation pattern carried forward from tick 43:\n    href is escaped + http(s)-only, falling back to \"#\"\n  - \"All news from <biz> -->\" deep-link goes to /business/:id/news\n  - Hidden when newsBiz.rows is empty so unpublished biz features\n    don't grow a blank section\n\nReal reach: 3 features currently on news-having biz (Manu Farrarons,\nCoffee Bean & Tea Leaf, $300 Data Recovery). Grows automatically\nwith the nightly scrape.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"ec89429","date":"2026-05-07 16:01:14 -0700","author":"SteveStudio2","subject":"feat(news): /api/news/by-vertical + heatmap on /coverage.html","body":"Surfaces which OSM categories are publishing the most blog/news/press\ncontent. New section on /coverage.html below the existing\n\"By vertical / By city\" coverage bars, but reflecting scraped news\nvolume not magazine-feature coverage.\n\nRoutes:\n  GET /api/news/by-vertical   {category, top_cat, news_count, biz_count}\n                              ordered by news_count DESC; CTE joins\n                              news_items + businesses; 5-min cache.\n\nUI changes:\n  public/coverage.html        new \"News heatmap\" section with bar rows\n                              (gold gradient fill); lazy-loaded after\n                              the existing coverage bars so the page\n                              still renders if news_items is empty;\n                              graceful empty state + error handler.\n\nReal numbers right now: amenity:restaurant 16 articles / 8 biz,\nshop:hairdresser 12 / 6, office:lawyer 9 / 4, etc.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"5afdd26","date":"2026-05-07 15:29:45 -0700","author":"SteveStudio2","subject":"feat(news): /business/:id/news per-business news firehose page","body":"Drill-down view from any news rail. When a viewer clicks the news badge\non /chains, /buildings, the map detail panel, or /news.html, they can\nland on a dedicated per-business page showing every article scraped\nfrom that business website (full body excerpts, dates, source domain,\nqwen3-summary tag when present).\n\nRoutes:\n  GET /business/:id/news    inline-rendered HTML, no template, paper-noir\n                            style matching /news.html. 404 on bad id,\n                            graceful empty state when news_items is zero\n                            (\"nightly 03:15 scrape will check again\").\n\nHardening:\n  - safeUrl() scheme-validates http(s) before <a href> interpolation\n  - esc() applied to all biz-attribute values (carry-over hardening\n    pattern from tick 43 XSS fix)\n\nDiscovery wired:\n  - public/index.html showPanel news rail: \"all -->\" link in header\n  - public/buildings.html badge: now points at /business/<id>/news\n    (was /news.html#biz-<id> which was a non-existent anchor)\n\nPath whitelisted in ADMIN_PATHS regex; smoke confirms 200 with content,\n404 on bogus id, empty-state on real id with no news.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"b329ecf","date":"2026-05-07 14:57:21 -0700","author":"SteveStudio2","subject":"feat(news): RSS feed at /news/feed.xml + auto-discovery on /news.html","body":"Steve plugs the URL into NetNewsWire / Feedbin / any feed reader and\ngets every fresh corridor news item without opening the magazine.\n\nRoutes:\n  GET /news/feed.xml      RSS 2.0, 50 most-recent items, 5-min cache\n                          Item description prefers qwen3 summary, falls\n                          back to body excerpt; pubDate prefers\n                          published_guess, falls back to fetched_at.\n                          Source URL scheme-validated (http/https only)\n                          before going into <link>; rejects javascript:\n                          falls back to /news.html.\n\nDiscovery:\n  public/news.html <head>  +link rel=alternate type=application/rss+xml\n                            so feed readers auto-detect when subscribed\n                            to /news.html.\n\nPath whitelisted in ADMIN_PATHS regex list. xmllint validates clean,\n50 items flowing on first smoke.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"4cada62","date":"2026-05-07 14:25:04 -0700","author":"SteveStudio2","subject":"feat(news): news_count badge on /chains.html bar rows","body":"Chain-store roster now flags any chain whose locations have scraped\nnews/blog posts on file. Aggregates news_count across every business_id\nthat maps to the chain's normalized name (so Coffee Bean across 2\nlocations = 2, not 1).\n\nAPI change:\n  /api/chains   +news_count INT per chain (sum across all chain locations)\n                via with_news CTE joining grouped.ids[] against news_items\n\nUI change:\n  public/chains.html bar-row    new gold-border .news-badge \"narrow N\"\n                                appended to chain name when news_count > 0\n                                clicks through to /news.html\n\nCache: 5-minute server-side cache means new news lands on the chains\nview within 5 minutes of summarizer/scraper completion.\n\n3 of 60 chains currently flag (Coffee Bean Tea Leaf, Leading Tax Group,\n300 Data Recovery). Grows automatically with the nightly 03:15 scrape.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"d444435","date":"2026-05-07 14:02:03 -0700","author":"Steve","subject":"fix(generate_features): tighten banned-phrase regex to catch bare 'X Meets Y'","body":"Tick-29 review found 'Skincare Meets Socializing' slipped through — the\nregex required a 'where' prefix before 'X meets Y', but the prompt's\nNEVER list bans the bare form too. Updated BANNED_HEADLINE_RE to match\nany '\\w+ meets \\w+' or '\\w+ rooted in \\w+' pattern, not just the\n'where'-prefixed cliché."},{"hash":"7594bb3","date":"2026-05-07 13:52:09 -0700","author":"SteveStudio2","subject":"fix(security): escape source_url + scheme-validate href in /news rail (XSS)","body":"Found by codex-2way round-1 PROSECUTOR (Kimi K2.5 cloud) auditing tick 41's\nper-business news rail on the corridor map. n.source_url was interpolated\nraw into href, allowing javascript: protocol injection or attribute breakout\nvia crafted URLs. Defense in depth - even though the scraper currently\ncontrols news_items inserts, never trust HTML scraped from third parties.\n\nAdds:\n  - safeUrl()   parses URL, rejects non-http(s) schemes, percent-encodes\n                special chars before interpolation\n  - escAttr()   shared HTML-attribute escaper covering ampersand, lt, gt, quote, apos\n\nBoth applied to href, title, and date inside the news rail block in\npublic/index.html showPanel(). DEFENDER (qwen3:14b@Mac2) had refuted the\nfinding twice; PROSECUTOR was right both times.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"0418331","date":"2026-05-07 13:12:06 -0700","author":"SteveStudio2","subject":"feat(news): news_count badge on /buildings.html tenant rows","body":"When Steve clicks an address on /buildings to expand its tenant\nroster, any tenant with scraped news_items now shows a small\ngold-border pill: 📰 N (where N = article count). Click goes to\n/news.html#biz-{id} for filterable access.\n\nAPI change:\n  /api/buildings/:bldg                +news_count INT per tenant row\n                                      via LATERAL count from news_items\n\nUI change:\n  public/buildings.html roster table  badge appended to tenant name\n                                      cell, only when news_count > 0\n\nVerified: 14622 Ventura Boulevard Sherman Oaks 91403 has 9 tenants,\n3 with news (Baja Fresh, Noah's Bagels, The Habit Burger Grill).\n\nContinues the discoverability theme from tick 41 (map detail panel\nnews rail) — every existing surface that shows businesses now has a\nquiet way to surface what their site has been publishing.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"fd03767","date":"2026-05-07 12:40:06 -0700","author":"SteveStudio2","subject":"feat: apply_exa_websites.ts — merge Exa-discovered website JSON into businesses table w/ aggregator filter (yelp/fb/yp/ig blocked)","body":""},{"hash":"8afd939","date":"2026-05-07 12:33:24 -0700","author":"SteveStudio2","subject":"feat(news): per-business news rail on map detail panel + news_count column","body":"Closes the discoverability loop: when Steve clicks any pin on the\ncorridor map, if that business has scraped news_items, a small rail\nshows the latest 4 article titles + dates with click-through to the\nsource URL. Subtle gold tint background keeps it editorial.\n\nAPI changes:\n  /api/businesses                     +news_count INT per row\n                                      (LATERAL count from news_items)\n  /api/businesses/:id                 +news[] last-10 articles\n                                      (id, source_url, title, excerpt,\n                                       summary, published_guess, fetched_at)\n\nUI changes:\n  public/index.html showPanel()       new \"News from their site\" rail\n                                      below contacts; only renders when\n                                      news_items > 0; capped at 4 visible\n                                      with count in the header\n\nReach: 45 of the 153 sited businesses currently have at least one\nscraped article (29 percent), so the rail surfaces meaningfully on\nevery fourth pin click. Grows automatically with the nightly scrape.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"00f39c0","date":"2026-05-07 12:29:49 -0700","author":"Steve","subject":"fix(generate_features): post-filter retry on banned headline phrases","body":"Negative prompt instructions weren't enough — qwen3:14b ignored the ban\nlist and produced 'Multi-Tenant Building and BJJ Discipline' on the\nfirst regen attempt with the patched prompt. Adding a hard gate: caller\nnow retries up to 3 times if BANNED_HEADLINE_RE matches, with escalating\ntemperature and an inline correction prompt. Throws if all 3 attempts\ntrip the filter (caller catches and skips). Tick-26."},{"hash":"549a1dd","date":"2026-05-07 12:29:33 -0700","author":"Steve","subject":"tighten .gitignore: add missing standing-rule patterns (dist/ build/ .next/)","body":""},{"hash":"e7c9465","date":"2026-05-07 12:20:07 -0700","author":"Steve","subject":"fix(generate_features): forbid 'multi-tenant building' boilerplate in headlines","body":"The prompt was injecting the literal phrase 'multi-tenant building' into\nthe user message, and qwen3:14b was dutifully reusing it — 36/167 (22%) of\nall generated headlines opened with that phrase. Reframed the building\ncontext as quiet body color, added explicit ban list with sibling\nphrasings, and re-flagged the 36 polluted rows back to draft for regen.\n\nAlso commits the audit-tabs.cjs Playwright sweep and migration 020 that\nunblocked the /pitches.html query (column p.website does not exist).\n\nCaught by the auto_dedupe_headlines trigram report (top cluster\n'multi tenant building' 36×). Tick-25 yolo loop."},{"hash":"e45334e","date":"2026-05-07 11:59:49 -0700","author":"SteveStudio2","subject":"feat(news): /this-week.html editorial digest + /api/news/digest","body":"Editorial front-door for the news pipeline. Where /news.html is the\nfirehose, /this-week.html is the curated read — 3 lead stories at the\ntop with serif/italic display type matching magazine.html, then a\ntwo-column \"also this week\" tail of up to 30 more dispatches.\n\nRoutes:\n  GET /api/news/digest    {lead:[3], tail:[<=30], generated_at}\n                          5-min cache, lead = articles with body >= 200\n                          chars + non-trivial title; tail = remainder\n                          most-recent-first\n  GET /this-week.html     paper aesthetic, double-rule masthead, mono\n                          kickers, Cormorant Garamond italic display\n\nBoth paths whitelisted in ADMIN_PATHS so basic-auth gates them\nconsistent with the rest of the corridor admin tools.\n\nPulls from the same news_items table populated by the news scraper,\nso it works today on the 76 articles already in DB and grows\nautomatically as the nightly 03:15 scrape lands more.\n\nNo external API deps.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"828ab33","date":"2026-05-07 11:26:56 -0700","author":"SteveStudio2","subject":"feat(enrich): website-discovery resilience + category prioritization","body":"Brave free tier hit its $15/mo cap mid-tick. Built two pieces of\nresilience for when the quota resets and for in-the-meantime work:\n\n1. website_via_brave.ts — added --high_value category filter. Default\n   queue now skips \"office: company\" / \"office: estate_agent\" /\n   \"office: lawyer\" shells (which are 70 percent of the corridor table\n   and ~0 percent of the websites) and prioritizes restaurants, cafes,\n   bars, retail, beauty, fitness, dentists, hotels, pet shops. Big\n   hit-rate boost when Brave comes back online.\n\n2. website_via_ddg.ts — new DuckDuckGo HTML fallback. No API key, no\n   monthly quota. Same domain-credibility logic as Brave/Exa.\n   Smoke-tested 1/15 hits before DDG started returning 202 anti-abuse\n   challenges; useful for bursts of <30 queries when nothing else\n   works. Wired as `enrich:websites:ddg` npm script.\n\n3. STATUS_NEWS_PIPELINE.md — frozen snapshot of what shipped and what\n   unblocks next. Highest-leverage move is Steve provisioning a\n   FOURSQUARE_API_KEY (free 100K/mo, returns websites explicitly) so\n   `npm run ingest:foursquare` can land thousands of new rows with URLs.\n\nNo data corruption: failed Brave attempts left the rows untouched\n(the script only UPDATEs on a winner).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"0a78f9e","date":"2026-05-07 10:38:50 -0700","author":"SteveStudio2","subject":"feat(news): nightly launchd jobs + patient summarizer backoff","body":"- launchd/com.steve.ventura-corridor-news-scrape.plist\n    Runs `npm run crawl:news` at 03:15 daily. Polite defaults\n    (4 concurrent, 500ms host gap, 10s timeout).\n- launchd/com.steve.ventura-corridor-news-summarize.plist\n    Runs `npm run news:summarize -- --limit=200` at 05:30 daily, two hours\n    after the scraper, so fresh items get qwen3:14b summaries before the\n    morning corpus-summary email at 09:00.\n- Both registered + loaded into ~/Library/LaunchAgents and copied into\n  the project's launchd/ dir for version-tracking + reproducibility.\n- summarize_news.ts backoff bumped from 8s/16s/32s/64s (max ~2min) to\n  60s/120s/240s/480s/960s/1920s (max ~62min). Magazine-gen 2000-feature\n  runs hold Mac1 for hours; the old backoff exhausted before capacity\n  opened. Patient backoff lets the 05:30 launchd drain even if the\n  generator runs long.\n\nVerified: launchctl list shows both plists loaded; Steve's 2000-feature\ngenerate_features will finish before 05:30 most nights.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"e8b2f73","date":"2026-05-07 10:04:31 -0700","author":"SteveStudio2","subject":"feat(news): /api/news/recent + /news.html magazine surface","body":"Closes the news-feed loop end-to-end:\n  scraper -> news_items -> /api/news/recent -> /news.html magazine rail\n\nRoutes:\n  GET /api/news/recent?limit=N    most-recent first across all businesses\n  GET /api/news/by-business/:id   per-business article list\n\nUI (/news.html):\n  - Noir editorial style matching /calendar.html and /coverage.html\n  - 4-stat header (articles, businesses, summarized, last 7 days)\n  - Live filter + sort (recent / biz A-Z / title A-Z)\n  - Card grid w/ source link, title, excerpt, host, date\n  - \"awaiting qwen3 summary...\" placeholder when summary missing\n\nBoth paths whitelisted in ADMIN_PATHS so basic-auth gates them like the\nrest of the corridor admin tools.\n\nBackground scrape across 153 sites: 76 articles inserted across 46\ndistinct businesses (50 percent biz hit rate, up from 26 percent in tick 36 smoke).\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"2422375","date":"2026-05-07 09:30:30 -0700","author":"SteveStudio2","subject":"feat(news): per-business news/blog/press scraper + qwen3 summarizer","body":"Closes the loop on the magazine's news-feed surface — 'what's new from\nevery vendor on Ventura Bl'.\n\nPipeline:\n  1. Migration 021 — news_items table (business_id, source_url, title, body,\n     content_hash UNIQUE, summary fields)\n  2. src/crawl/news_scraper.ts (cheerio + native fetch, no Playwright)\n     - For every business with a website, parses homepage for blog/news/press\n       links via 11-keyword match, fetches up to 3 same-origin candidates\n     - Polite: per-host serial queue + 500ms gap, 8s timeout, robots.txt\n       Disallow:/ check, custom UA with mailto contact\n     - Hashes title + body[0:1000] to dedupe across runs\n  3. src/jobs/summarize_news.ts — qwen3:14b on Mac1 (100.94.103.98:11434),\n     2-3 sentence editorial summary per item; 503-aware exponential backoff\n     so it self-throttles around the running magazine-gen 2000-feature batch\n\nSmoke results:\n  - 50-business scrape: 18 items inserted across 13 distinct businesses\n    (26% hit rate; chains/dead-sites the long tail of misses)\n  - Summarizer code-correct, queue-blocked by current magazine-gen load —\n    backoff handles it; will drain when MS1 frees up\n\nRun:\n  npm run crawl:news -- --limit=20\n  npm run news:summarize -- --limit=20\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"d687733","date":"2026-05-07 09:17:41 -0700","author":"Steve","subject":"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)","body":""},{"hash":"04185a9","date":"2026-05-07 09:16:43 -0700","author":"SteveStudio2","subject":"fix(launchd): strip broken 2>&amp;1 inline redirect from 11 plists","body":"Launchd does not unescape XML entities in ProgramArguments <string> values,\nso the literal text \"2>&amp;1\" reached bash and produced \"amp: ambiguous\nredirect\" / \"1: command not found\" errors. Every err.log was 67 bytes —\nthe size of those two messages — meaning every scheduled job (auto-publish,\nauto-dedupe, magazine-gen, corpus-summary, ig-dryrun, snapshot, standup,\nauto-cover, coverage-snapshot, digest, morning-email) had been silently\ndying since registration.\n\nMost damaging: auto-publish was a no-op for days, leaving 37 features in\ndraft and 0 published. Manual run after fix promoted top 10. The remaining\n27 drafts will drain on the next scheduled tick.\n\nFix: drop inline >> /path 2>&amp;1; both streams already captured via\nStandardOutPath + StandardErrorPath in every plist. Script is idempotent\nand re-runnable if plists get regenerated.\n\nCo-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"},{"hash":"c6a6ecd","date":"2026-05-07 09:15:04 -0700","author":"SteveStudio2","subject":"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","body":""},{"hash":"ee200d8","date":"2026-05-07 09:02:44 -0700","author":"SteveStudio2","subject":"feat: migration 019 + 5 starter LinkedIn post drafts (corridor commentary x2, project reveal x2, behind-the-scenes x1)","body":""},{"hash":"64555b1","date":"2026-05-07 09:00:32 -0700","author":"SteveStudio2","subject":"fix: migration 018 — adds magazine_features.taps + taps_breakdown for engagement tracking","body":""},{"hash":"d817e36","date":"2026-05-07 08:59:52 -0700","author":"SteveStudio2","subject":"feat: 9 AM daily corpus-summary email — totals + cover + top tapped + latest gen, complements 8:07 morning_feature_email","body":""},{"hash":"e145ce4","date":"2026-05-07 08:34:56 -0700","author":"SteveStudio2","subject":"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","body":""},{"hash":"d607f2b","date":"2026-05-06 20:47:26 -0700","author":"SteveStudio2","subject":"iter 177: SESSION_LOG addendum 2 — prompt-rewrite win confirmed (7/7 fresh headlines on new positive patterns, zero on blocked templates)","body":""},{"hash":"a52bd71","date":"2026-05-06 20:29:43 -0700","author":"SteveStudio2","subject":"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","body":""},{"hash":"5d90ce1","date":"2026-05-06 20:26:00 -0700","author":"SteveStudio2","subject":"iter 175: SESSION_LOG addendum — captures iters 171-175 + the regen demotion bug catch","body":""},{"hash":"02da0a2","date":"2026-05-06 20:25:15 -0700","author":"SteveStudio2","subject":"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","body":""},{"hash":"88857c9","date":"2026-05-06 20:22:50 -0700","author":"SteveStudio2","subject":"iter 173: auto-dedupe now also catches trigram-templated headlines (3+ shared 3-word phrases) on top of exact dupes","body":""},{"hash":"317fdcf","date":"2026-05-06 20:19:05 -0700","author":"SteveStudio2","subject":"iter 172: /api/magazine/similar/:id — pg_trgm word_similarity over first 480 chars; surfaces editorial-body templating beyond headline-level","body":""},{"hash":"a55be3f","date":"2026-05-06 20:09:18 -0700","author":"SteveStudio2","subject":"iter 171: SESSION_LOG_2026-05-06.md — 40-iter overnight debrief in plain English","body":""},{"hash":"59b0a5e","date":"2026-05-06 20:04:29 -0700","author":"SteveStudio2","subject":"iter 170: /calendar.html + /api/magazine/calendar — GitHub-style 90-day heatmap of feature gen rate","body":""},{"hash":"bfb00de","date":"2026-05-06 20:02:57 -0700","author":"SteveStudio2","subject":"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.)","body":""},{"hash":"59418a9","date":"2026-05-06 19:56:08 -0700","author":"SteveStudio2","subject":"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","body":""},{"hash":"6a0afbb","date":"2026-05-06 19:55:15 -0700","author":"SteveStudio2","subject":"iter 167: /api/magazine/headline-stems + /duplicates.html templated-phrases panel (5× 'precision care encino', 5× 'legal precision meets', etc.)","body":""},{"hash":"1efb250","date":"2026-05-06 19:52:15 -0700","author":"SteveStudio2","subject":"iter 166: /duplicates.html admin viewer — keeps oldest, click-regen runner-ups for variety","body":""},{"hash":"680bcb7","date":"2026-05-06 19:51:07 -0700","author":"SteveStudio2","subject":"iter 165: /api/magazine/duplicates + nav pill — surfaces qwen3 headline-templating (5 groups affecting 14 features today)","body":""},{"hash":"48b3342","date":"2026-05-06 19:46:39 -0700","author":"SteveStudio2","subject":"iter 164: morning_feature_email now includes 'Suggested next publish' block — draft-pick-of-the-night","body":""},{"hash":"280b679","date":"2026-05-06 19:45:36 -0700","author":"SteveStudio2","subject":"iter 163: /api/magazine/draft-pick-of-the-night + 'Publish next' rail on today.html (heuristic-scored)","body":""},{"hash":"fcadf64","date":"2026-05-06 19:42:39 -0700","author":"SteveStudio2","subject":"iter 162: /api/magazine/:id/social-pack.json — programmatic JSON twin of ad-pack.html (5 brief variants + tags + asset URLs)","body":""}]}