Commit Graph

45 Commits

Author SHA1 Message Date
Ryan Salazar
077f146eda forum: drop Recent Threads widget, surface last post inline on category cards
Per Ryan — category drill-down already shows newest-first ordering so
the standalone Recent Threads list was redundant. Each category card now
shows "Last post: <title> · <relative time>" below the description so
glanceable activity stays on the landing page. Categories API joins one
latest thread per category in a single round-trip (in_ + limit + group
in memory) instead of N+1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:14:50 +00:00
Ryan Salazar
e163746d87 submission-receive: name the disallowed phrase in error + support link
422 response now reads "Your submission contains disallowed phrase X.
Please remove it and try again. Questions? Email
support@relevantmediaproperties.com." so PR firms know exactly what
tripped the filter without having to ask.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:39:37 +00:00
Ryan Salazar
74ab4dae3d submission-receive: defense-in-depth approval check
Even though distribute-rmp gates the submitter, re-validate the
contributor's distribute.users.status here before inserting into
bb.native_articles. A leaked DISTRIBUTE_SHARED_SECRET shouldn't be
enough to bypass the approval system. Falls back to letting the
request through on transient lookup failure (distribute side already
checked).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:13:27 +00:00
Ryan Salazar
61910d6c8f fixes: image-url proxy + raw-publish fallback on rewrite failure
1. Clean image URLs
   Article HTML was leaking the full Supabase Storage URL:
     https://supabase.onsethost.com/storage/v1/object/public/distribute-media/<uuid>/<file>
   Now rewritten on render to:
     /img/distribute-media/<uuid>/<file>
   Served by new /img/[...path] route handler that proxies to Supabase
   with 24h public cache + 7d SWR. Better SEO (first-party images) and
   stops leaking the storage host into article markup.

2. Stuck PR submissions
   When the rewrite pipeline fails (Ollama 503 / bad model output), the
   submission used to sit forever in ai_original_submissions without
   becoming a public wp_imported_posts row. PR firms got "delivered"
   status with no visible article anywhere.

   /api/ai/submit now calls publishRawFallback() if rewriteSubmission
   returns anything other than 'succeeded'. The fallback inserts a
   wp_imported_posts row containing the raw HTML body with a visible
   "Distributed as a press release — staff editorial rewrite pending"
   banner at the top, so the article surfaces immediately. A future
   retry job can swap in the rewritten content when Ollama recovers.
2026-05-27 19:43:35 +00:00
Ryan Salazar
3460baa8b9 ux: dark-theme conversion + forum hero + ticker cleanup
- /manufacturers index + DirectoryClient: convert white/light-ink theme
  to BB dark theme so it stops standing out against the rest of the site
- /manufacturers/[slug]: same conversion, includes the new executives
  card grid and Headquarters block
- /nab-2026: same conversion, including hero gradient and sponsor tile
  borders (amber on dark instead of amber on white)
- Forum: remove "Top Performing Threads" widget from both /forum and
  /forum/[slug] (display only — TopThreadsWidget component preserved
  in case admins want to re-enable later)
- Forum hero: vertical padding py-10 → py-5, tagline reduced from
  three sentences to one. Adds a "Sign in to post" or "Start a thread"
  CTA in the hero's top-right depending on auth state.
- Recent Forum Posts ticker: drop replies entirely, surface only thread
  titles so breadth of conversation shows instead of the loudest
  threads dominating with reply chatter.
2026-05-27 14:52:08 +00:00
Ryan Salazar
52a6792c9e ask-bb-ai: swap from Anthropic to local Ollama on AI_002
Editorial rule is no paid AI services in production — this endpoint was
calling api.anthropic.com which violated that. Rewires to local Ollama
at 10.10.0.67:11434 using llama3:latest (warm, ~4.7GB, reliable).

Adds 3-attempt retry with backoff for 503/busy responses since AI_002
is shared with the article rewrite pipeline that periodically saturates
Ollama's pending queue. Drops max_tokens 1024 → 512 to keep responses
snappy and reduce queue pressure.

OLLAMA_HOST + BB_ASK_AI_MODEL env vars supported for overrides.

Removes the "AI not configured" 503 — the only way that error fires
now is if Ollama itself is unreachable for all 3 retries.
2026-05-27 13:39:24 +00:00
Ryan Salazar
1793c4f0e2 ingestion: reject submissions containing disallowed phrases
/api/ai/submit and /api/submission-receive now scan incoming payload
title + body against the bb.disallowed_phrases allowlist (cached 3
min) and return 422 with the matching phrases if any are found. Stops
competitor mentions from re-entering the pipeline after the bulk
purge.
2026-05-27 13:34:41 +00:00
Ryan Salazar
2e78726291 cross-link: forum auto-link, NAB hub, sponsor strip, recent stories
Feature 3 — forum auto-linking
  Thread + reply bodies in /forum/thread/[id] now wrap company-name
  mentions with /manufacturers/<slug> links via a new client-side
  LinkifyPlainText component. Loads the directory list once per page
  (module-level promise cache), regex-scans each body, preserves
  whitespace, caps at 10 links per body to avoid clutter.
  CompanyMentionsHover mounted on the page so the same preview card
  pops on hover.
  Adds /api/public/companies/list — minimal { name, slug } directory
  feed, cached 10min.

Feature 4 — /nab-2026 live coverage hub
  New page aggregating every directory company with exhibits_nab=true.
  Three sections:
    - Coverage Partners — active advertisers exhibiting at NAB, larger
      tiles with emerald Sponsor border
    - Latest from the show floor — last 60 days of articles with NAB
      in the title, cinematic grid
    - All NAB 2026 exhibitors — alphabetical, capped at 200 with link
      to full /manufacturers directory
  Every company tile carries data-company-slug for hover previews.

Feature 5b — Sponsor logo strip on homepage
  New SponsorLogoStrip server component renders just above the footer
  on the homepage, showing logos of every current banner advertiser
  with a directory entry + logo. Each links to the manufacturer
  profile with the hover-card slug attribute.

Feature 5c — Recent coverage on manufacturer profiles
  /manufacturers/[slug] now includes a grid of the 6 most recent
  articles matching the company name in the title, with thumbnails.
  Renames the existing manufacturer_id-linked block to "Press
  releases" since it's a separate data source.
2026-05-27 12:55:25 +00:00
Ryan Salazar
a59846a524 cross-link: companies-in-this-story sidebar + unified company search
Feature 1 — sidebar
  Adds a "Companies in this story" panel on every news article that
  surfaces every directory company my linker matched in the body. Each
  tile shows logo, name, Sponsor / NAB 26 / IBC badges, and a one-sentence
  bio. Tiles link to /manufacturers/<slug> and carry data-company-slug so
  the global hover card pops on hover too.

  Extends company-mentions.ts with linkifyAndExtractMentions() that
  returns both the linked HTML and the ordered list of matched slugs in
  one pass, so /news/[slug] doesn't re-scan the body for the sidebar.

Feature 2 — unified typeahead
  /api/search/suggest now returns { companies, items } — companies are
  matched against bb.tracked_companies (directory_visible=true), capped
  at 6, ordered by featured → mention_count, then bumped if they're an
  active advertiser on broadcastbeat (adv schema lookup).

  Header search dropdown renders a "Companies" section at the top with
  logo, name, Sponsor badge (active advertiser) or NAB 26 badge, above
  the existing "Stories" results. Each company link carries
  data-company-slug so the hover card works in the dropdown too.
2026-05-27 12:47:54 +00:00
Ryan Salazar
b479b62331 articles: auto-link company mentions to manufacturer directory
When an article body is rendered, scan it for any company name in
bb.tracked_companies (directory_visible=true) and wrap the first
mention of each company in
  <a class="company-mention" data-company-slug="...">Name</a>
linking to /manufacturers/<slug>. Caps at 12 mentions per article so
heavy press-release lists don't get visually noisy.

Match rules:
- Word-boundary, case-insensitive
- Skips text already inside <a>, <script>, <style>, code/pre/headings
- Skips any element with data-no-autolink="true"
- Longest name wins on overlap (Sony Pictures beats Sony)
- One link per company per article (first occurrence)

Hover behavior: a new CompanyMentionsHover client component mounts once
per article page, listens globally for [data-company-slug] hovers, and
pops a floating card showing logo, name, 2-sentence bio, sponsor/NAB/IBC
badges, HQ city, 3 recent stories, and links to the full profile +
external website. Preview data fetched from new endpoint
  /api/public/companies/[slug]/preview
with a client-side cache keyed by slug.

Includes non-advertisers — any visible directory entry gets linked,
which is the point. If a company is *also* a current banner advertiser
on broadcastbeat, the hover card adds a green "Sponsor" badge.

Styling: dotted underline that becomes solid on hover, slightly bolder
weight, accent color on hover. Subtle so multi-mention paragraphs don't
look like a link-soup.
2026-05-27 12:32:14 +00:00
Ryan Salazar
c1ab36297c homepage: auto-promote advertiser stories + premium slots in Industry News
Three-tier sort on the Industry News feed:
1. Premium — articles pinned via adv.featured_story_slots (paid slots).
   Sorted by position ascending. Highlighted with an amber left-border
   gradient + ★ Featured ribbon.
2. Advertiser — articles whose title contains the company name of a
   current active banner advertiser on this property, AND were published
   in the last 48 hours. Highlighted with a blue left-border gradient
   + ◆ Advertiser ribbon. Multiple matches ordered by publish date.
3. General — everything else, newest first.

After 48h, advertiser stories naturally drop back into the general
tier. Premium slots fall off when their ends_at passes.

Detection is title-only with case-insensitive word-boundary match; the
longest matching company name wins so "Sony Pictures" beats "Sony"
when both advertise. Backed by the new adv.active_advertisers_by_
property view.

createAdminClient() now accepts an optional schema arg so we can hit
adv from BB without juggling clients.

API revalidate dropped 300 → 60s so the feed reflects campaign flips
within a minute.
2026-05-27 12:25:20 +00:00
Ryan Salazar
93dc131a02 fix: forum user lookup handles duplicate display names
When multiple profiles share a display_name (Michael Carter, Alex
Rivera, etc.), .maybeSingle() blew up with PGRST116, returning 404.
Switched to ordered limit(1) so the earliest-created profile wins
and the route resolves deterministically.
2026-05-23 04:16:53 +00:00
Ryan Salazar
06390db497 sitewide: rename THE LATEST → INDUSTRY NEWS; add social shares to /articles/<slug> template too 2026-05-22 05:02:59 +00:00
Ryan Salazar
729547764e ticker: query wp_imported_posts (not native_articles) + fallback when 7d window empty 2026-05-22 00:46:44 +00:00
Ryan Salazar
aee67ef9f6 homepage: neon dual-speed ticker + emerald/cyan palette pass
- New DualSpeedTicker component (CSS keyframes, GPU-accelerated,
  pause on hover, 28px rows, 17s BEAT / 40s FORUM, refresh 5min)
- /api/ticker/beat-news: last 7 days from bb.native_articles
- /api/ticker/forum-posts: latest threads + replies interleaved
- Mounted at the bottom of Header so every page gets it
- Scoped .bb-neon palette on homepage root: section heads +
  text-shadow glow, neon-tinted card borders, emerald→cyan button
  gradient, cyan hover state on links, subscribe boxes neon-bordered
- CSS-only — no layout changes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 23:58:12 +00:00
Ryan Salazar
a8b75593c2 forum: require auth for posting + add login/register/edit pages
- API: require auth on thread+reply POST (401 if !user); lazy-create
  forum_user_profile via service-role; author_name = display_name
- UI: surface API errors (kill silent catch); auto-redirect to login
  on 401; gate new-thread + reply forms with "Sign in to post" CTA
- /forum/login + /forum/register: forum-themed Supabase Auth pages
  with ?next= redirect support
- /forum/profile/edit + /api/forum/profile/me: edit own forum profile
  (username, display_name, bio, role, company, location, avatar)
- Header: logout button + sign-in/join links when not authed
- forum/user/[username]: show Edit Profile button when viewing own

Fixes posting bug: RLS rejected anon inserts with cryptic message
that the UI silently swallowed; now blocked with friendly 401 + redirect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 22:41:28 +00:00
Ryan Salazar
78c2ee656e homepage: typography upgrade + ArticleFeed polish
- layout.tsx: load Lora (serif) + Inter (sans) via next/font/google,
  self-hosted at build time, exposed as --font-serif / --font-sans
  CSS variables on <html>. No CDN fetch, no FOIT, no GDPR concern.
- tailwind.css: --font-heading and --font-body now reference the
  next/font variables with Georgia / -apple-system fallbacks for the
  brief window before the self-hosted face is ready. --font-mono
  upgraded to IBM Plex Mono → ui-monospace → Courier New fallback
  (DoubleTicker chips already specify IBM Plex Mono directly).
- ArticleFeed: drop the FEATURED filter chip (duplicates the
  Staff Editorial section above), add publish date alongside the
  byline on every card, lift the homepage feed cap from 200 → 5000.
- api/public/posts: lift MAX_LIMIT to 10000 so client-side
  pagination + infinite-scroll see the full archive.
2026-05-21 01:51:29 +00:00
Ryan Salazar
74677a80b8 homepage: remove top bar + fix subscribe + Staff Editorial header
1. Remove SystemStatusBar from root layout. The "29,220 articles
   indexed / 13 events tracked / [v2.0.0]" strip ate vertical space
   above the fold without serving readers.

2. /api/newsletter/subscribe was returning 500 "Failed to save
   subscription" because:
   - The route used the anon-session client and tried to UPSERT on
     bb.newsletter_subscribers, but the table's RLS only has an
     INSERT policy for the public role. Upsert needs INSERT+UPDATE,
     so the second branch failed RLS.
   - Switched to createAdminClient (service-role) — bypasses RLS,
     which is fine because this is a server-side endpoint with
     server-side email validation.
   - Welcome email now sends via nodemailer in a non-blocking
     background task so a slow SMTP doesn't hold up the response.
     Newer HTML template, brand-aligned, with a real CTA back to
     /news.

   Side work to wire SMTP (done out-of-band):
   - Created newsletter@broadcastbeat.com mailbox in Mailcow via
     the API (2GB quota, active, no force-pw-update).
   - Set SMTP_HOST=mail.onsethost.com, SMTP_PORT=587,
     SMTP_USER/PASS/FROM_EMAIL/FROM_NAME in Coolify env so the
     welcome email actually fires after the next deploy.

3. FeaturedBentoFromDb section header: "Featured · staff editorial"
   (small mono caps) → "Staff Editorial" (font-serif 2xl/3xl, bold,
   with a thin metadata line "Selected by our newsroom" beside it
   and a 2px bottom rule). Matches the editorial typography on the
   article pages.
2026-05-20 18:51:25 +00:00
Ryan Salazar
418027b469 search: real autocomplete previews + clearer placeholder
PLACEHOLDER: "Search broadcast news…" → "Search articles, gear,
vendors". Drops the weird ellipsis and the "we're a broadcast site
so 'broadcast news' is redundant" feel.

API (/api/search/suggest):
- Match expanded from {title, author_name} to
  {title, excerpt, content, author_name} — surfaces hits when the
  phrase lives deep in the article body.
- Returns a snippet object {text, matchStart, matchEnd} sliced
  from the first hit (HTML-stripped), with ~80 chars of context on
  either side and "…" markers when truncated.
- Returns matched_in: 'title' | 'excerpt' | 'content' | 'author' so
  the UI can show "match in article body" / "match in byline".
- Returns date + author_name on each result.

UI dropdown (Header.tsx):
- Each suggestion now renders three rows:
  1) Title (bold) + category chip
  2) Snippet with the matched substring wrapped in <mark>
  3) Author byline + date + (if match wasn't in title) a tag
     showing where the match occurred
- Vertical stacking with a colored left-border on hover so the
  active suggestion is visible.

CSS:
- Stack item layout, snippet with -webkit-line-clamp:2 so long
  matches don't overflow.
- <mark> background uses the accent blue at 28% so the highlight
  reads against the dark card.
2026-05-20 16:22:04 +00:00
Ryan Salazar
f6b52f7c2d forum: user profile pages + clickable bylines + neon pulse favicon
USER PROFILES (/forum/user/[username]):
- New API at /api/forum/user-by-username/[username] resolves by
  username OR display_name (URL-encoded), since forum threads/replies
  store author_name = display_name. Returns the persona's profile,
  thread history (up to 50), and recent replies (up to 20).
- New page renders the persona card with archetype badge
  (Audio Engineer / Colorist / etc.) + expertise level badge
  (beginner/intermediate/senior) + role title, company, location,
  years_experience, join date, and an expertise-tags chip row.
- Two columns underneath: "Threads started" and "Recent replies"
  with proper deep-links into each thread.
- Author names in /forum, /forum/[slug], and /forum/thread/[id] are
  now <Link> elements pointing to /forum/user/<displayname>, with
  onClick=stopPropagation so the card-level click handler doesn't
  also fire.

FAVICON:
- Hand-built neon pulse waveform — emerald→cyan→purple gradient on
  dark navy background, designed to read at 16×16 to 256×256.
- Multi-resolution favicon.ico (16/32/48/64) + SVG + PNGs at
  16/32/48/64/96/192/256 under /static/favicons/.
- layout.tsx icons block now references all sizes including the
  apple-touch sizes (192, 256).

No env / DB changes here — pure deploy.
2026-05-20 12:56:56 +00:00
Ryan Salazar
dc166dfb10 forum: fix API + UI column-name mismatch against bb.forum_threads
The schema has upvotes / downvotes / vote_score; API + UI were
querying/reading upvote_count / downvote_count (non-existent
columns). Threads existed in the DB but didn't render.

- /api/forum/threads route.ts:
  - 'top-voted' sort: order by vote_score (was upvote_count)
  - 'newest' default sort: forum_threads has no 'status' column,
    so order pinned-first via is_pinned, then last_reply_at desc
- UI ForumThread interface + components: rename
  upvote_count → upvotes, add downvotes + vote_score so the
  vote display in the thread cards and the admin moderation
  list pulls live data from the existing columns.

After this lands and the deploy completes, the 50 seeded
threads + the 9 ai-seeded new threads + 253 backfilled replies
already in the DB will all render at /forum.
2026-05-20 08:34:05 +00:00
Ryan Salazar
80b307ba92 ai: implement ollama rewrite provider + ryan-salazar byline default
The press-release rewriter (ai_rewrite_jobs) was stuck because:
- BB_REWRITE_PROVIDER defaulted to "anthropic" in code, AND
- production env still had BB_REWRITE_PROVIDER=anthropic, AND
- ollamaProvider.rewrite() was a stub that threw "not implemented".
Every job since 2026-01-26 hit Anthropic 400 "credit balance too
low" — 12 failed_api in a row.

Changes:
- Implement ollamaProvider against /v1/chat/completions on the local
  Ollama (works against AI_001:11434 / llama3.1:70b). Same JSON
  contract as the Anthropic provider; gets prompt_tokens /
  completion_tokens out of Ollama's usage block.
- Flip the default provider from "anthropic" → "ollama" in
  getProvider(), so the rewriter works out of the box now that the
  Ollama implementation exists.
- Make the BB auto-story byline configurable via env. Default flips
  from a 12-name pen rotation to a single "Ryan Salazar" byline for
  Broadcast Beat (per editorial directive). AV Beat keeps its own
  pen pool. Set BB_AUTOSTORY_BYLINE=rotate to restore the old
  behaviour.

Production env still needs (set in Coolify dashboard):
  OLLAMA_ENDPOINT=http://10.10.0.66:11434
  OLLAMA_MODEL_DEFAULT=llama3.1:70b
  BB_REWRITE_PROVIDER=ollama
  BB_REWRITE_MODEL=llama3.1:70b
  (remove ANTHROPIC_API_KEY)
2026-05-20 06:42:22 +00:00
Ryan Salazar
f7c462891f authors: db-driven /authors/[slug] for all 160 contributors
Drops the 540-line hand-curated map and replaces it with a real
bb.author_profiles table + view (author_profiles_with_stats) that
computes first_post_date / total_articles live from
wp_imported_posts. The page now resolves any slug correctly:

- Slug looked up in author_profiles; if no row, the slug is
  titleized ("desert-moon-communications" → "Desert Moon
  Communications") and used as the wp_imported_posts.author_name
  filter, so articles + join date still resolve.
- "Joined ..." line is now the actual MIN(wp_published_at) for that
  author, formatted as Month YYYY. Fixes Desert Moon showing 2020
  when its first post was Nov 2013.
- Role line defaults to "Contributor"; staff/founder roles ride on
  the new role column. Ryan seeded with "Founder of Broadcast Beat"
  in the migration.
- About tab renders a contact card (address / phone / email /
  website) whenever any of those fields exist on the profile row,
  plus a profile-facts panel with location + member-since +
  articles published + role.
- Bio is from the profile row; when empty, page shows
  "No biography has been added yet for {name}" instead of falling
  back to another author's bio.

Bios + contact details for the remaining ~160 authors are populated
by a separate Ollama+web-fetch enrichment job (next commit).
2026-05-20 06:25:08 +00:00
Ryan Salazar
fa94e92af5 chore: purge all rocket.new references from source
- Replace img.rocket.new mock URLs in authors/[slug] + FeaturedBento
  with /assets/images/article-placeholder.svg (live DB data drives
  these pages now; the static maps are fallback only).
- Replace broadcastb5322.builtwithrocket.new with broadcastbeat.com
  in admin API routes, news detail, forum layout, and the 4 Supabase
  edge functions that build outbound email links.
- Swap rocket-hosted JSON-LD logo URL in root layout for the local
  /assets/images/logo.png we already ship.
- Drop img.rocket.new from the Next.js image-hosts allowlist;
  add supabase.onsethost.com so storage proxy URLs can render.

The DB has had zero rocket.new image refs for weeks; this finishes
the job in the codebase so no page on broadcastbeat.com can render
a rocket-hosted asset anymore.
2026-05-20 06:14:02 +00:00
Ryan Salazar
9eadda4b9e authors: real article list from DB by author_name + version bump
Authors page was rendering a hardcoded mock article list per slug,
with Ryan Salazar's slug falling through to the staff-reporter mock
list (847 fake articles). New behaviour:

  - GET /api/author/[slug] returns ALL real articles where
    bb.wp_imported_posts.author_name matches the canonical name(s)
    for that slug. Slug → names map:
      ryan-salazar     → ["Ryan Salazar"]
      james-whitfield  → ["James Whitfield"]
      sarah-chen       → ["Sarah Chen"]
      … etc.
    Unknown slugs fall back to a Title-Cased guess.
    Response: { total, articles[] }, cached 5min / SWR 10min.

  - /authors/[slug] now fetches that endpoint on mount and replaces
    the hardcoded list with the live one. Article count shown on the
    profile header + sidebar is now the live total (not the
    hardcoded 1200/847/etc).

  - Version badge in the status bar bumped v4.2.1 → v2.0.0 to match
    the actual public release.

Ryan Salazar's profile now correctly shows his 135 real bylines from
WordPress instead of the staff-reporter mock 847.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:04:30 +00:00
Ryan Salazar
cb00f7f343 search: return all matches (not just 100) + add typeahead autocomplete
- searchLegacyArticles() default limit: 50 → 5000. /search page
    explicitly passes 5000 too. ilike on title/excerpt/author is fast
    enough on the 29k-row archive that returning everything is fine.

  - New API route /api/search/suggest?q=...&limit=8 returns the top
    matching article titles + category + slug, with a 30-second CDN
    cache + 60-second stale-while-revalidate.

  - The search input in the Browse bar now debounces (150ms) and
    fires that API per keystroke once 2+ chars are typed. Matches
    appear in a styled dropdown below the input — click to navigate
    directly to /news/<slug>, or press Enter to fall through to the
    full /search page. "See all results for …" link in the dropdown
    footer.

  - Keyboard: Escape closes the dropdown; aria-autocomplete + role
    listbox/option for accessibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 06:00:10 +00:00
claude-code
82ae43ccf5 feat: allow cron-secret to invoke company-coverage scan with service role 2026-05-16 15:01:30 +00:00
claude-code
39311a4401 feat: wire up The Latest feed — fetch imported WP posts from new /api/public/posts 2026-05-16 12:56:16 +00:00
broadcastbeat-rename
0db3ea34e3 Replace 'BroadcastBeat' with 'Broadcast Beat' in all user-visible text (133 occurrences across 40+ files) 2026-05-16 00:32:13 +00:00
959e3980ae Revert "slice 4: accent palette refinement — blue → teal"
This reverts commit 52fe7bceb6.
2026-05-15 23:49:36 +00:00
52fe7bceb6 slice 4: accent palette refinement — blue → teal
Replace the four accent-family hex codes site-wide with the teal palette
(keeps the dark theme; only swaps the accent family, not the dark base):

  #3b82f6 (accent primary CTA)   → #5B7C8D  (teal)        [839×]
  #2563eb (accent-dark / hover)  → #4A6473  (darker teal) [44×]
  #60a5fa (info link, lighter)   → #8FB0C3  (mid teal)    [68×]
  #1e3a5f (accent-muted bg)      → #2F4F5F  (navy)        [44×]

tailwind.config.js tokens updated to match (accent/accent-dark/accent-muted).
Sweep also covers tailwind.css. 108 files touched. The dark base (#0d0d0d,
#111, #161616, #1a1a1a etc.) is intentionally untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 23:33:04 +00:00
Claude (Phase B)
4546d5be3b Phase 6C featured carousel + Phase 2 event tag chips
FeaturedCarouselServer (RSC) pulls 5 most recent featured=true rows from
bb.ai_rewritten_articles UNION bb.wp_imported_posts, renders into the
client FeaturedCarousel component: 4s auto-advance (pause on hover),
chevron + dot nav + NN/NN counter, [ID xxxx] / [CATEGORY] badges,
serif headline, lede, optional neural-summary box with corner brackets +
confidence + entity chips, byline with initials avatar. Replaces the
FeaturedBento on the homepage.

Seeded bb.wp_imported_posts.featured = TRUE on 5 recent posts so the
carousel renders out-of-the-box (Amagi, Riedel, LiveU BroadcastAsia,
Cobalt Best-of-Show, Leader MPTS).

Phase 2 UI:
  EventTagPicker (client) — chip multi-select reading bb.events,
                            writing to bb.article_events via
                            /api/admin/review-queue/[id]/events.
                            article_table=ai_rewritten for now;
                            future submission-form integration will
                            cover other tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 08:20:49 +00:00
Claude (Phase B)
a785ef428e BB AI redesign: 4-item nav, Show coverage, About, status bar, wire ticker,
trends sidebar, Ask BB AI, neural-summary schema, 13-event seed.

Phases 1-10 (11 phases minus Phase 8 which is the pgvector marker).

DB:
  bb.events                — 13 industry events seeded (MPTS, ANGA COM,
                             BroadcastAsia, SET Expo, IBC, NAB NY,
                             NewTECHForum, SATIS, DPP EBS, SVVS,
                             Hamburg Open, CABSAT, NAB Show '27).
  bb.article_events        — composite PK (article_id, article_table, event_id)
                             links any article-table row to one or more events.
  bb.banner_creatives      — (no change tonight)
  bb.{native_articles,
      ai_rewritten_articles,
      wp_imported_posts,
      press_releases}.neural_summary jsonb — populated by Phase 7
                             analyzer (deferred).
  bb.{native_articles,
      ai_rewritten_articles}.featured boolean — featured carousel source.

Routes:
  /api/events/upcoming     — date-gated, enriched with is_live/days_until/
                             day_of_event/total_days.
  /api/ask-bb-ai           — Claude Opus 4.7 chat with prompt caching on
                             the system block, 30-msg/hr/IP rate limit.
                             Tool-use deferred — first cut is straight
                             LLM with archive-citation prompting.

Pages:
  /show-coverage           — index of all events, upcoming + past split.
  /show-coverage/[slug]    — hero + status (live/T-Nd/past) + schema.org
                             Event JSON-LD + tagged articles list.
  /about, /about/{team,contact,advertise,press-kit}.

Components:
  Header                   — 4-item nav: Show coverage (dropdown) /
                             Newsletter / Forum / About (dropdown).
                             Old NEWS/GEAR/TECHNOLOGY/ADVERTISE items
                             removed from nav (routes still exist).
  EventsDropdown           — 340px CSI panel with L-corner brackets,
                             pulsing dot, "BB AI" badge, live/T-Nd rows,
                             auto-updated stamp.
  AboutDropdown            — 5-item lighter treatment.
  SystemStatusBar          — Index online pulse, articles/sources/events
                             counts, build version. Above Header.
  LiveWireTicker           — replaces NewsTicker. [HH:MM] [SRC] prefix,
                             CSS marquee, doubled for seamless loop.
  TrendsSidebar            — BB AI detected trends, top 5 entities by
                             7d-vs-30d lift, vector-pass stamp.
                             Phase 6b: replace with pgvector.
  AskBBAI                  — floating bottom-right button + 420px right
                             drawer chat. No mention of Anthropic/Claude.

Styling:
  redesign-tokens.css      — --color-text-info, --color-background-*,
                             --font-serif/mono/body, bb-pulse/bb-ring
                             animations + bb-marquee.

Constraints honored:
  - LiveU 728x90 still between </nav> and ticker.
  - Blackmagic 300x600 still pinned top of sidebar.
  - /r/[slug] click tracking unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 02:22:23 +00:00
Claude (Phase B)
c0584092eb ad analytics: impressions + clicks + admin stats + /r/[slug] redirect
Tables (Phase B+):
  bb.ad_impressions   — every render = a row. is_bot + viewport flags
                        for filtering. No unique constraints — gross
                        counts per Ry an spec.
  bb.ad_clicks        — every /r/[slug] hit = a row.
  bb.ad_campaign_clients — schema only; future client reporting.

FK target is bb.banner_creatives (the renderer table from 5/15 banner
refresh). bb.ad_campaigns is the AdOps billing schema and unrelated.

Routes:
  GET  /r/[slug]                   — log click, 302 to bb.banner_creatives.click_url
  POST /api/track/impression       — record impression (slug | campaign_id),
                                     used by client beacon

AdImage rewrite:
  - link href is /r/{slug} target=_blank rel=sponsored noopener noreferrer
  - sendBeacon on mount fires impression (viewport=false)
  - additional sendBeacon on IntersectionObserver ≥0.5 (viewport=true)
  - removed direct supabase.from('banner_analytics').insert path

Ad type carries slug now; FALLBACK list + DB mapper both populate it.

/admin/banners:
  per-row stats — 24h, 7d, lifetime impressions/clicks/CTR
  link to /admin/banners/[id]/analytics

/admin/banners/[id]/analytics (new):
  recharts line charts for impressions + clicks (30d), top page paths,
  top referrer domains, bot/human toggle.

Tower Products orphan image file removed; banner was not in any active
table or in code (already off rotation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:45:33 +00:00
Claude (Phase B)
280d9818ba banners: refresh 8 creatives + bb.banner_creatives + /admin/banners
Backend:
  bb.banner_creatives (new) — image_path, click_url, size, start/end_date,
  status, adsanity_source_id. Seeded with 8 banners from broadcas_temp
  WP-export DB (post_type=ads): LiveU 728x90, Blackmagic DaVinci Resolve
  20 (300x600), Studio Hero, Magewell Pro-Convert, LiveU PAYG, AJA
  ColorBox, Zixi, Lectrosonics (all 300x250). Dates and click URLs come
  from the AdSanity rows; current creatives downloaded from Wayback
  snapshot 20260316123737.

src/lib/ads.ts:
  - Reads bb.banner_creatives via anon supabase-js client, gated by
    status='active' AND start<=NOW()<=end.
  - In-process 5-min TTL cache with stale-while-revalidate. Module load
    primes the cache; clients see fallback on first hit, live DB on
    subsequent.
  - Hardcoded FALLBACK list mirrors the seeded rows so the site keeps
    rendering banners if Supabase is unreachable during deploy.
  - Public helpers (rotateAll, pickAds, ADS_728X90, ADS_300X250,
    FIXED_300X600) remain synchronous so existing client component
    callsites work unchanged.

/admin/banners:
  Admin-only list + inline editor for every banner_creatives row:
  name, click URL, start/end date, status (active|scheduled|paused|
  expired). PATCH via /api/admin/banners/[id] — service_role bypass for
  the write, user_profiles.role admin/administrator for the gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:49:09 +00:00
Claude (Phase B)
8fd7165a6a Phase B polish: revalidatePath on approve, reject empty submissions
src/app/api/admin/review-queue/[id]/route.ts
  - Call revalidatePath on /news, /news/<slug>, /articles/<slug>, /home-page,
    section pages, and /sitemap.xml on approve/reject. The /news listing is
    ISR-cached (revalidate=1800) so approval would otherwise be invisible
    for up to 30 minutes; this flushes the cache immediately.

src/app/api/ai/submit/route.ts
  - Reject submissions where rawTitle/title is whitespace or rawContent/body
    is empty after stripping HTML. Previously empty bodies fell through into
    the LLM and Claude dutifully wrote a 400-word piece about the empty
    submission. Hard-cap at the gate to avoid the spend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 19:38:20 +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
f20f4c3250 Add /api/submission-receive endpoint and wp-login/wp-admin -> distribution.rmp redirects 2026-05-14 19:22:15 +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
Ryan Salazar
ca1247a968 wp-login: skip phpass when legacy row is already linked to auth user
Previously the route ran phpass validation against wp_legacy_users.
wp_hashed_password before checking auth_user_id, even for users who had
already been migrated. That meant any user whose Supabase password
diverged from the original WP hash (e.g., curated migration users like
ryan.salazar@relevantmediaproperties.com — wp_id=1 'broadcastbeat',
where the hash on the legacy row is from the migration template, not
the user's current password) would get 401 even with the correct
Supabase password.

Fix: if auth_user_id is set OR password_upgraded is true, skip phpass
and go directly to supabase.auth.signInWithPassword. The legacy hash
is only authoritative for the first-ever login (when neither flag is
set). Returns legacy:true so the client can distinguish from the
fully-native signInWithPassword fallback (legacy:false).
2026-05-09 02:57:29 +00:00
Ryan Salazar
e7f434305d wp-login: fall back to native signInWithPassword when no wp_legacy_users row
The /api/auth/wp-login route previously returned 401 immediately if the
email had no bb.wp_legacy_users entry. That made it impossible for
modern staff accounts (created directly via the Supabase Auth Admin
API, no WP migration history) to sign in via /client-login.

When the legacy lookup misses, fall through to a regular
supabase.auth.signInWithPassword call. If that succeeds, return a
success response with legacy=false so the client can distinguish
modern auth from a phpass-migrated session.

Existing legacy WP users keep working — the fallback only fires when
the legacy lookup returns no rows.
2026-05-08 23:05:53 +00:00
Ryan Salazar
e544ddccd1 update homepage tagline to 'News & Intelligence for Broadcast, Post-Production & Streaming Technology' 2026-05-08 21:06:49 +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
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
70aa1ad46e initial commit: rocket.new export of broadcastbeat 2026-05-07 16:39:17 +00:00