Commit Graph

14 Commits

Author SHA1 Message Date
Claude (Phase B)
6feee16391 Phase B+: severity-aware quality gates (hard banned terms vs soft)
src/lib/ai/quality-gates.ts:
  - Read bb.banned_terms with the ban_level column so admins can mark
    a term as hard (instant fail) or soft (score nudge).
  - Hard hits join the hard-coded AI_TELLS list to instant-fail the
    rewrite (status=failed_quality, regenerate).
  - Soft hits each contribute +0.05 to the heuristic AI-detector score,
    capped at +0.30, so a borderline-slop article can be tipped over
    the 0.65 threshold without single-handedly being rejected.
  - Multi-word phrases are matched as substrings; single words use a
    boundary regex that handles hyphenated terms cleanly.
  - QualityGateResult now exposes softWordsFound alongside the existing
    bannedWordsFound; failure_reason text includes the soft list when
    the score trip is what failed.

44 terms seeded in bb.banned_terms (23 hard hype words like
"revolutionary", "industry-leading", "world-class", plus 7 PR-speak
openers like "pleased to announce"; 21 soft like "innovative",
"leverages", "robust", "transform").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:48:07 +00:00
Claude (Phase B)
85599d14cc Phase B E2E: UNION rewrites into /news, accept distribute-rmp payload shape
src/lib/articles/legacy-source.ts
  - getLegacyArticleBySlug, getLegacyArticlesBySection, searchLegacyArticles,
    getLegacyRecentSlugs, getLegacyRecentSitemapEntries: each now UNIONs
    bb.ai_rewritten_articles (status=published) with bb.wp_imported_posts.
    Rewrites carry persona attribution (author/avatar/title) via a
    persona:ai_personas(slug,name,beat,avatar_url) embed.
  - Section routing for rewrites is by category, not tags (rewrites do not
    use the WP tag taxonomy — they have category strings written by Claude).

src/app/api/ai/submit/route.ts
  - Bearer auth via BB_INGEST_BEARER (preferred) or x-internal-key (legacy)
  - Polymorphic payload: accepts native {rawTitle, rawContent, ...} OR the
    distribute-rmp shape {title, body, contributor:{...}, attachments,
    submission_id, ...}. Distribute attachments + featured image survive in
    bb.ai_original_submissions.metadata.

src/app/api/ai/rewrite/route.ts
  - Same bearer auth pattern as submit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:28:36 +00:00
Claude Code
3bd9a716b2 Phase B: AI press-release rewrite pipeline
Pluggable rewrite client (Anthropic default, ollama/openrouter stubs)
using claude-opus-4-7 with prompt caching on system prompt + style guide.
Routes the raw submission through:
  1. bb.ai_original_submissions  — raw never edited
  2. bb.ai_rewrite_jobs          — every attempt logged with token counts
  3. quality gates               — AI-tell banned-word check + heuristic
                                   AI-detector score (threshold 0.65)
  4. regenerate up to 3x         — on banned words or detector trip
  5. bb.ai_rewritten_articles    — status ai_rewritten_pending_review
  6. /admin/review-queue          — list + detail page with approve / reject /
                                   needs-human actions; on approve, the
                                   original submission is FK-linked to the
                                   published rewrite

New files only — no existing routes or contributor flow modified.
Persona auto-selection routes by beat keywords; 6 personas seeded
(marcus-halverson, elena-vasquez, darren-okafor, sarah-quinn, mike-trayton,
priya-rao) with SVG monogram avatars in public/assets/images/personas/.

Smoke test (Matrox / ST 2110 sample press release):
- persona auto-routed to darren-okafor (protocols-specs)
- anthropic 22.3s, in=2616 / out=1372 tokens
- gates passed first try, score=0.000, no banned words
- review_queue row created

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 18:38:27 +00:00
Claude Code
02b69f2252 P0-2 + Phase A: drop Adsanity iframes, fix SEO baseline, noindex while staging
P0-2 (Adsanity iframe path removed):
- AdImage.tsx: deleted the iframe branch and the ADSANITY_CAMPAIGN_MAP
  fallback. AdImage now renders local images only; banners without a
  src render nothing.
- ads.ts: removed Ad.adsanity_id from the interface. Pruned AJA, Zixi,
  Lectrosonics, Opengear, Studio Hero, Magewell 300x250 and Blackmagic
  300x600 — they relied on ads.broadcastbeat.com which does not resolve.
  Surviving live banners: LiveU 728x90, LiveU PAYG 300x250, Telycam 300x250.
- SidebarAdStack: key no longer references the removed field.
- No banner_analytics rows reference ads.broadcastbeat.com.

Phase A (SEO baseline):
- robots.ts: fallback base URL now broadcastbeat.com (was rocket staging URL).
- sitemap.ts: fallback base URL now broadcastbeat.com (was bb-staging).
- news/[slug]/page.tsx and articles/[slug]/page.tsx: same fallback fix.
  JSON-LD now uses ISO 8601 for datePublished, absolute image URL via
  SITE_URL prefix, and publisher.logo points at /assets/images/logo.png
  instead of the 404-ing /legacy/site/broadcastbeat-logo.png.
- layout.tsx: removed the /en /es /pt /fr /de /zh /ja /ar /hi hreflang
  alternates - those routes 404 today; Phase D will reinstate. Replaced
  rocket.new Organization schema URL with broadcastbeat.com. Set
  robots index/follow=false until Phase B-D land and Rich Results passes.

Featured-image fallback hardening:
- legacy-source: rowToArticle now treats BB_Gray, no_image and placeholder
  URLs as null and falls back to the new placeholder so dead images do not
  leak through to the page.
- public/assets/images/article-placeholder.svg: new clean dark gradient
  with BroadcastBeat wordmark, NOT no_image.png style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:53:42 +00:00
Claude Code
70e2aebb83 P0-3/4/5: route error boundary, null guards, slug fallback, search
P0-3 (category page crash):
- Add src/app/error.tsx and src/app/global-error.tsx so any future render
  error shows a real ref id instead of the bare client-exception toast.
- NewsPageClient: null-guard every field accessed during search/category/
  sort filtering and article render (title, excerpt, author, tags,
  category, date, image, alt). The user-visible link
  /news?category=live-production now also hydrates filter state from
  ?category= and ?search= search-params, so the dropdown actually filters.

P0-4 (/articles/[slug] 404s):
- /articles/[slug] and /news/[slug]: on slug miss, redirect("/news")
  instead of notFound(). Dead links from the hardcoded NewsTicker /
  FeaturedBento / ArticleFeed arrays now land on the news index instead
  of dead-ending on 404.
- legacy-source: add searchLegacyArticles() (title/excerpt/author ilike).

P0-5 (header search):
- Header: wire Enter on the desktop and mobile search inputs and make
  both search icons clickable buttons. Submits to /search?q=<query>.
- New /search route: server component that runs searchLegacyArticles
  and renders results in the same card style as /news.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:35:24 +00:00
Claude Code
243f4855a1 wip-banner-system: full banner pool, stack all 300x250s, footer 728x90
Banner spec from Ryan:

- 728x90 header (every page): pool is now LiveU only; rotates when more
  728x90 entries are added to ads.ts.
- 728x90 below main menu on article pages: removed (was the article-top
  AdSlot stub).
- 728x90 footer (every page): replaces the old 300x250 rocket.new
  placeholder in Footer.tsx. Rotates from the same ADS_728X90 pool.
- 300x250 sidebar: now renders EVERY entry in ADS_300X250 stacked
  vertically, shuffled per page-view. Pool: AJA, Zixi, Telycam, LiveU,
  Lectrosonics, Opengear, Studio Hero, Magewell. Adding more entries to
  ads.ts auto-grows the stack.
- 300x600 fixed slot: Blackmagic (Adsanity ad-210571), top of sidebar,
  never rotated.

Implementation:

- ads.ts: Ad interface now allows either src (local image) or adsanity_id
  (iframe). New rotateAll() returns ALL ads of a size shuffled; pickAds
  kept for ArticleBody bounded slots.
- AdImage.tsx: dropped the legacy ADSANITY_CAMPAIGN_MAP label fallback —
  ads.ts is now the single source of truth, so Telycam (no adsanity_id)
  renders its local GIF instead of the iframe.
- SidebarAdStack.tsx: rotateAll(300x250) instead of pickAds with a
  count cap. count prop removed; callers updated.
- NewsArticleDetailClient.tsx: removed article-top 728x90 stub.
- Footer.tsx: 728x90 AdImage in place of the 300x250 placeholder.
- LeaderboardAd.tsx: deleted (was unused after the earlier home-page
  cleanup).
- PENDING_ads.md: rewritten — Adsanity IDs to verify (Studio Hero,
  Blackmagic) and optional local-file upgrades.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:43:41 +00:00
Claude Code
146b7259d4 wip-banner-system: real ads, logo fix, remove duplicate banner, clean footer
- Remove duplicate 728x90 LeaderboardAd below news ticker (per Ryan)
- Footer: remove NAB Official Media Partner badge
- Header: replace AdSlot stub with real AdImage rotation (3 verified 728x90s)
- Header: fix logo path from dead WP URL to local /assets/images/logo.png
- ads.ts: only include ads with verified real images (3x728x90, 2x300x250)
- ads.ts: remove 22 unrecovered ads — see PENDING_ads.md for upload list
- SidebarAdStack: handle null FIXED_300X600 gracefully (Blackmagic not recovered)
- public/legacy/ads/: 5 real ad images committed (Tower, Sony, LiveU 728x90; LiveU PAYG, Telycam 300x250)
- public/assets/images/logo.png: recovered from Wayback Machine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 12:23:22 +00:00
Ryan Salazar
89aaa7b36d supabase browser client: match server cookie attributes (SameSite=Lax, no Secure)
The browser-side createBrowserClient was writing cookies with
'Secure; SameSite=None' while the server-side createServerClient
writes the same cookie names with 'SameSite=Lax' (no Secure flag —
Next.js cookies().set() default).

When the wp-login API set the auth cookie via NextResponse, the browser
stored it with SameSite=Lax. Then the browser supabase client (via
AuthContext or any subsequent auth call) tried to re-write the cookie
via document.cookie with Secure;SameSite=None. Browsers can't reconcile
this — depending on Chrome/Safari/Firefox version, you get either:
  - duplicate cookies that confuse session detection
  - the JS write silently failing because Secure;SameSite=None has
    additional constraints
  - the existing Lax cookie getting clobbered with attributes that
    conflict with same-site navigation

Net effect: AuthContext on /contributor doesn't reliably see the
session even though the cookie exists, so /contributor's
'!loading && !user → router.push(/login)' check bounces the user
back to /client-login. Looks like login failed.

Fix: align the JS-side cookie attributes with the server-side defaults
(SameSite=Lax, no Secure). Both writes now produce the same cookie
shape, no conflict.
2026-05-09 09:52:56 +00:00
Ryan Salazar
2b33713c3e wp-login: use service_role admin client for createUser; /login redirects to /client-login
- Add src/lib/supabase/admin.ts: server-only Supabase client constructed
  with SUPABASE_SERVICE_ROLE_KEY. Uses persistSession=false. Throws if
  the env var is missing.
- src/app/api/auth/wp-login/route.ts: switch the auth.admin.createUser
  call (step 5, transparent migration on first login) to use the new
  admin client. The anon-key client cannot call admin.createUser, which
  would have made first-login attempts for legacy WP users fail at
  step 5 with a permission error.
- src/app/login/page.tsx: replaced the old WP-style login form with a
  server-side redirect to /client-login so the new client-login is the
  canonical PR-firm/contributor entry point. Existing bookmarks for
  /login keep working.

Coolify app env: SUPABASE_SERVICE_ROLE_KEY added (set out-of-band; not
NEXT_PUBLIC_-prefixed so it never ships to the client bundle).
2026-05-08 20:21:08 +00:00
Ryan Salazar
9c5b0e4959 wire real ad rotation: Blackmagic 300x600 fixed + rotating 300x250s
Replace the placeholder Unsplash banner array with the live AdSanity
inventory served from /legacy/ads/ (same 27 active creatives currently
on broadcastbeat.com).

New:
- src/lib/ads.ts: ad inventory (1 fixed Blackmagic 300x600, 22 rotating
  300x250s, 4 leaderboard 728x90s) + Fisher-Yates shuffle + pickAds()
  helper that draws N unique ads excluding any caller-supplied src set.
- src/components/AdImage.tsx: renders one ad as <a target=_blank rel=
  sponsored noopener noreferrer><AppImage>.
- src/components/SidebarAdStack.tsx: Blackmagic 300x600 fixed on top,
  then count rotating 300x250s. Memoizes the rotation per render so a
  refresh re-shuffles. Accepts excludeSrcs to coordinate with other
  slots on the same page.
- src/components/ArticleBody.tsx: splits article HTML at </p>
  boundaries, splices a 300x250 ad after every Nth paragraph (default
  4). Same exclude-set pattern.

Wiring:
- ArticleFeed.tsx: remove the inline sidebarAds array (was 5 Unsplash
  placeholders + 1 LiveU 300x250). Sidebar now <SidebarAdStack count=5/>.
- NewsArticleDetailClient.tsx: sidebar 300x600 + 300x250 AdSlot
  placeholders replaced with <SidebarAdStack count=4/>. Article body
  innerHTML replaced with <ArticleBody html=... everyN=4/> so
  300x250 ads slot in between paragraphs.

Same banners as the live broadcastbeat.com (sourced from the existing
WP AdSanity install we cached locally during Phase 2). Each <a> opens
in a new tab and uses rel='sponsored noopener noreferrer'.
2026-05-08 18:16:07 +00:00
Ryan Salazar
f1e1d5ea76 delete sampleArticles seed; serve all sections from bb.wp_imported_posts (DB)
Eliminates 72 hardcoded rocket.new image refs in one shot. Section pages
(/gear, /technology, /show-coverage, /news) now render from
bb.wp_imported_posts via getLegacyArticlesBySection(section), which maps
each non-news section to a curated tag set (e.g. gear -> Audio, Cameras,
lighting, Blackmagic Design, monitoring; technology -> AI, Streaming,
OTT, IP, Cloud, AV; show-coverage -> NAB, IBC, BroadcastAsia).

Also:
- src/lib/articles/types.ts: Article interface lifted out of the deleted
  seed file. Type-only consumers (NewsArticleDetailClient,
  ArticleDetailClient, legacy-source) re-imported from here.
- /news (client component) now wrapped: server fetches articles from DB
  and passes to NewsPageClient. Filter UI unchanged.
- generateStaticParams on /news/[slug] and /articles/[slug] no longer
  references seed slugs; pre-renders 50 most-recent imported slugs and
  relies on dynamicParams + revalidate=3600 for the rest.
- sitemap.ts now sources article URLs from
  getLegacyRecentSitemapEntries() (up to 5000 most-recent imported posts).
  Default BASE_URL fallback updated from the rocket.new placeholder to
  bb-staging.onsethost.com.
- src/app/api/ai/article-suggestions/route.ts now pulls candidates from
  the DB (top 100 recent news) and resolves AI-returned slugs via DB
  lookup if not in the candidate window.

Inline rocket.new image refs in homepage components (ArticleFeed,
FeaturedBento) are unchanged in this commit; those are inline seed
arrays in the components, not imports of sampleArticles.
2026-05-08 14:31:25 +00:00
Ryan Salazar
08df1ad4c1 wire /news/[slug] and /articles/[slug] to read bb.wp_imported_posts via ISR
- new src/lib/articles/legacy-source.ts: server-side Supabase reader
  (bb schema, anon key, RLS public_read policy) that maps imported rows
  to the existing Article shape, applies rewriteLegacyImageUrl for the
  featured image and rewriteLegacyImageUrlsInHtml across the body.
- both [slug]/page.tsx routes now: try seed sampleArticles first; on
  miss fall back to getLegacyArticleBySlug. Related articles match via
  same-category recent posts when imported.
- generateStaticParams pre-renders seed slugs + 50 most recent imported
  posts. dynamicParams=true means anything else server-renders on first
  request and ISR caches it for 1h (revalidate=3600). Avoids a 4577-page
  build.
- Hardcoded builtwithrocket.new URLs in JSON-LD replaced with
  process.env.NEXT_PUBLIC_SITE_URL.
2026-05-08 06:25:29 +00:00
Ryan Salazar
ef1921ca7c add legacy image rewrite helper
src/lib/legacy-image.ts exports rewriteLegacyImageUrl(url) and
rewriteLegacyImageUrlsInHtml(html) which translate WP origin URLs
(https://www.broadcastbeat.com/wp-content/uploads/...) to local paths
under /legacy/... served from the new persistent storage volume mounted
at /app/public/legacy/.

src/lib/legacy-image-map.json holds 3,856 URL→path entries built in
Phase 2: 3,839 featured images (mostly converted to webp, q=85) plus
27 ad creatives plus the site logo.

Use this helper at server-render time when emitting <img>/<Image> for
imported WP articles, so the 707 MB on-host cache replaces external
fetches to the origin.
2026-05-08 00:04:34 +00:00
Ryan Salazar
70aa1ad46e initial commit: rocket.new export of broadcastbeat 2026-05-07 16:39:17 +00:00