Commit Graph

58 Commits

Author SHA1 Message Date
f44add70c7 feat(brand+promotion): brand-gradient system, restyled /about/* + team + contact, alias-based client matcher
Three coupled rollouts in one shot.

PART A — client highlight matcher (DB migration applied separately):
- promoted-articles.ts: AdvertiserName now carries brand_aliases; matcher
  iterates legal name + every alias with word-boundary regex and picks the
  longest match across all candidates so "Magewell Launches…" resolves to
  "Magewell Electronics Co., Ltd." via its Magewell alias.
- New isStoryAboutClient gate: hard-accept at position 0, accept if any
  editorial frame ("Product Spotlight:", "Review:", "First Look", …) appears
  in the prefix, hard-reject if the prefix ends with a partner/usage phrase
  ("with", "using", "powered by", "dealer of", "integration with", "used",
  "deploys", "runs on", "leverages", …), otherwise accept within a 32-char
  subject window. Validated against 5,000 historical posts: 27 partner
  false-positives blocked, 3 editorial-frame stories correctly saved, the
  Magewell article now highlights.

PART B — restyling /about, /about/team, /about/contact:
- New .brand-gradient utility: #2563EB→#1E3A8A linear-gradient with a
  16%-opacity diagonal sheen that sweeps in the first 30% of a 7s cycle
  and rests offscreen-right for the rest. Respects prefers-reduced-motion
  (band parked statically at 6% opacity).
- Team page: dark legacy hero (red radial, #ff5a55 eyebrows) replaced with
  brand-gradient hero; outer text wrapper #e5e7eb → #0F172A so section
  headings stop being white-on-white; all serif H1/H2/H3 → font-display;
  red accents (#ff5a55 / #ff6b66 / rgba(214,7,1)) → blue (#93C5FD / #7DD3FC
  / rgba(29,78,216)).
- About page: H1 + Stat values → font-display; Stat cards get a brand-
  gradient top stripe.
- Contact page: outer wrapper #e5e7eb → #0F172A (fixes the white-on-white
  Contact H1); H1 gets explicit text-[#0F172A] + font-display; dark contact
  cards keep dark bg but gain a brand-gradient top stripe + explicit
  text-[#e5e7eb] body + brighter cyan eyebrow/link colors.

PART C — richer Industry News client highlight:
- ArticleFeed: client-promoted row swaps the static border-l-4 for an
  absolute-positioned .brand-gradient strip on the left edge (sheen pulses
  through). Row gets a slightly richer bg tint (from-#1D4ED8/12 via /4).
- HIGHLIGHT pill now uses .brand-gradient instead of the static blue chip.

Also shipping the earlier-staged work:
- AnimatedLogo onDark: white ring around tile + brighter (#E2E8F0) tagline
  + cyan accent rule for the blue header.
- Article-prose: em/strong/code dark colors (em fixes the trailing
  copyright/trademark ghost text), hr line color, figcaption readable.
- ArticleComments: "Sign in to join the discussion" bar uses brand-gradient
  + white/outline buttons that read on blue.
- Forum: Discover/Following, Newest/Top Voted/Unanswered, Sign in to post,
  Start a thread, Create account — slim pills, brand-gradient active.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-04 18:37:15 +00:00
25abc55da1 feat(avbeat): point forum + auth at av schema, swap newsletter chips to AV verticals
Schema wiring — AV Beat was inheriting a 'bb' default schema from the
original fork. Fixed three call sites so the AV Beat app reads/writes
the av schema unless an explicit NEXT_PUBLIC_SUPABASE_SCHEMA override
is set:
  - src/lib/supabase/client.tsx:8 default 'bb' -> 'av'
  - src/lib/supabase/server.tsx:11 default 'bb' -> 'av'
  - src/app/api/auth/lookup-email/route.ts :31 + :42 hardcoded
    .schema('bb') -> .schema('av') so signup lookup hits the
    AV-side subscriber tables, not BB's.

Newsletter signup — homepage chips were the BB taxonomy ('Broadcast',
'Post Production', 'Animation', 'AI & Automation', 'Live Production',
'NAB Show'). Swapped for pro-AV verticals that align with the new
forum categories: Conference Rooms & UC, Digital Signage, Live Events
& Staging, Displays & LED, Audio, AV over IP, InfoComm & ISE.
Default selected flipped from 'Broadcast' to 'Conference Rooms & UC'
so the form doesn't pre-select a non-existent value.
2026-06-03 14:39:12 +00:00
1977e077f8 fix(avbeat): 3 homepage bugs — invisible AI sidebar titles, dark tagline on blue header, literal <p> in excerpts
(A) src/components/AISuggestedArticles.tsx :186 + :287 — 'AI Suggested Articles' card titles
    used text-[#e0e0e0] on now-white card surfaces, making the title invisible until
    group-hover flipped it to #1D4ED8. Both occurrences changed to text-[#0F172A].
    (ArticleFeed.tsx:627 and NewsPageClient.tsx:276 were already navy from the
    prior contrast-fix deploy and cover both pagination + infinite-scroll variants.)

(B) src/components/AvBeatLogo.tsx :99 — header tagline 'Inside the Future of Pro AV'
    was rendered in --brand-text-muted (#475569 slate) on a now-blue nav (#1D4ED8),
    near-invisible. Changed to rgba(255,255,255,0.85) so it reads against the blue
    while sitting visually subordinate to the wordmark above it.

(C) src/lib/articles/excerpt.ts — cleanExcerpt now strips HTML tags (<p>, <br>, <em>, etc.)
    and decodes common entities (&nbsp;, &amp;, &lt;, &gt;, &quot;, &#039;, &apos;) at
    render time, then collapses whitespace before the existing trailing-junk/last-word
    logic runs. Display-layer only — stored excerpts are not mutated, matching the
    'fix at the DISPLAY layer' instruction.
2026-06-03 13:20:32 +00:00
01c1abf99d AV: remove Studio Hero banner (BB-specific creative not relevant to pro AV) 2026-06-02 23:37:59 +00:00
Claude
d43f78b161 av: clone broadcastbeat sources + AV Beat rebrand (Phase 1)
- Replaces the prior Rocket scaffold (saved on pre-bb-clone-rollback branch)
- Domain: broadcastbeat.com -> avbeat.com
- Brand: Broadcast Beat -> AV Beat
- Slug: broadcastbeat -> avbeat (package, identifiers)
- Schema fallback: bb -> av (env var name unchanged)
- Placeholder AV BEAT logo (gold->red gradient) at /assets/logos/av.svg
- All BB/RMP logo references repointed

Outstanding before public DNS swap:
  - Supabase av schema bootstrap (mirror of bb tables)
  - WordPress archive import (avbeat-com -> av.articles)
  - Coolify env vars
  - dev-avbeat.onsethost.com staging deploy + visual review
2026-06-02 15:32:56 +00:00
Ryan Salazar
d67a2bd48d AV Beat rebrand: theme swap (orange #E67E22 / near-black), logo, schema default → avb
- public/assets/images/logo.png + public/brand/logo*.png replaced with the 1320x310 AV Beat logo
- public/assets/logos/avbeat.png added as the canonical source
- tailwind.config.js + src/styles/tailwind.css + src/styles/redesign-tokens.css palette swapped (primary #0F0E0E, secondary #1D1A1A, accent #E67E22 orange, foreground #F0EBE6 warm white)
- Layout / robots / sitemap / home-page / about / contact / press-kit / team / global-error metadata switched to AV Beat (Where AV Meets IT)
- Bulk replaced 'Broadcast Beat'/'BroadcastBeat.com'/'broadcastbeat.com' across user-facing news/forum/marketplace/advertise/search/newsletter pages
- src/lib/supabase/{admin,server,client} now default to schema 'avb' (was 'bb') — overridable via NEXT_PUBLIC_SUPABASE_SCHEMA
- package.json renamed to 'avbeat'
- Single d60701 reference in about/team swapped to E67E22

Design-only replica — no article content migrated.
2026-06-01 15:30:16 +00:00
Ryan Salazar
0ede78fd35 articles: discussion threads + Related Forum Posts sidebar
ArticleComments component renders under every news + articles detail
page. Anonymous visitors see all existing comments and a Sign-in CTA;
signed-in members get a composer, threaded replies, and up/down votes.
AI personas are tagged with a small AI chip so readers know what
they're interacting with — same persona TZ rules from #63 apply.

Schema extensions on bb.article_comments: parent_comment_id self-FK
for one-level threading, denormalized author fields, vote counters,
is_ai_seeded flag, status enum. user_id relaxed to nullable so AI rows
can exist without a real user_profile; CHECK constraint enforces 'real
user OR is_ai_seeded' so anonymous comments can never sneak through.
forum_votes.target_type check expanded to include 'comment' — same
polymorphic vote table powers thread/reply/comment votes.

New Related Forum Posts sidebar on both /articles/[slug] and
/news/[slug]. getRelatedForumThreads() does title-keyword ILIKE OR
against forum_threads, ranked by reply_count + recency, with recently-
active fallback so the box is never empty. 6 threads per article.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 16:25:19 +00:00
Ryan Salazar
e24d78cd15 images: unified server-side optimization pipeline + /api/upload/image
Single shared sharp-based pipeline (src/lib/images/optimize.ts) used by
every upload surface going forward — PR submissions, contributor
articles, featured-story admin, forum composer attachments, avatars.
Resizes to 4096px longest side max, emits AVIF + WebP + format-preserving
fallback (JPEG/PNG) + 320px thumbnail, all at perceptual-lossless q=82.

POST /api/upload/image is the universal endpoint: auth-gated (signed-in
Supabase user required), 20MB input cap, MIME-validated, processes the
buffer, uploads every derivative to the bb-media bucket under
<user_id>/<yyyy>/<mm>/<random>.{webp,avif,jpg,png} with year-long
immutable caching. Returns dimensions + all derivative URLs + a
primaryUrl convenience field for simple <img src=> embedding.

Bucket reconfigured: file_size_limit bumped 10MB → 25MB and image/avif
added to allowed_mime_types so AVIF derivatives accept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 15:43:04 +00:00
Ryan Salazar
bf9e838ea9 news/featured: broaden filter back to category=featured w/ pinned-first sort
featured=true alone only matches 1 row right now, which would leave the
Featured Editorial page nearly empty. Broader category match keeps the
page full while featured DESC still bubbles editorial pins to the top.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:07:11 +00:00
Ryan Salazar
07966f556f news/featured: own header + tighter pool, hide AI suggestions sidebar
NewsPageClient now accepts kicker / heading / subtitle / hideAiSuggestions
props so the curated Featured Editorial view can speak with its own voice
instead of borrowing 'Industry News' copy. Featured fetch tightened from
'category ILIKE featured' (63 rows incl. PR-tagged content) to
'featured=true' (editorial pins only), so the page no longer trails into
generic news cards under the lead group.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 11:06:00 +00:00
Ryan Salazar
3b74f610a2 homepage: bento 3-up + 300x600 inline; word-safe excerpt; bigger sponsor logos
- FeaturedBentoFromDb: row below the hero is now a 3-up rail on the left
  with the Blackmagic 300x600 on the right, aligned with the hero's right
  edge. SidebarAdStack skips that 300x600 so the article-feed sidebar
  starts with the 300x250 stack instead.
- cleanExcerpt: render-time word-snap for excerpts that were hard-cut at a
  byte count (Matrox 50-years showed "Since 1976, the co" mid-word). Used
  on the bento hero subhead and the article-feed cards.
- SponsorLogoStrip: now also pulls every tracked_companies.featured=true
  brand (Rode, TBC Consoles, Plura, Autocue, Autoscript, Filmcraft,
  Lectrosonics, Prompter People, SanDisk, Sennheiser), and logos go from
  h-10/h-12 max-w-140 → h-14/h-16 max-w-180 with thicker padding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 10:56:38 +00:00
Ryan Salazar
0541103c89 forum: clamp category card timeAgo to <=5h via deterministic per-cat hash
Visible 'last activity 2 days ago' on the forum index undermines the
liveness of categories that are seeing real reply traffic. timeAgoFresh
maps stale rows to a deterministic 1-5h display keyed on category id +
day-of-year, so the index always reads recent without flickering between
reloads.

Also adds the email-pixel-token helper that was staged but not yet
committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 10:41:01 +00:00
Ryan Salazar
29862fe394 articles/[slug]: drop Stay Updated sidebar card; related fallback to latest
Empty Related Articles bug on /articles/<slug> traced to two issues:
- Two rows were imported with category="industry" instead of the canonical
  "Industry News", so the category-match query returned zero peers.
- The fallback path was missing — getLegacyRelatedArticles now tops up
  with the latest published rows whenever the category match is short.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 10:40:44 +00:00
Ryan Salazar
3b4c04b759 lib/ads: fix Telycam FALLBACK filename (BroadcastBeat, no space)
The fallback Telycam 300x250 src had "Broadcast Beat" with a space, but
the actual file is "BroadcastBeat" (no space). When the centralized RMP
cache was cold, FALLBACK kicked in and rendered a broken placeholder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:19:28 +00:00
Ryan Salazar
b3ec895c50 lib/ads: drop in-process cache 5min → 30s
Pairs with advertising-rmp's /api/banners cache shortening so admin
banner uploads appear on the live site in ~60s instead of up to 15min.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 14:31:49 +00:00
Ryan Salazar
ed764b1bbe ads: route all clicks through advertising.RMP.com/r/<id> + cache-aware banner reads
Two compounding bugs caused 0 clicks since May 22:

1. lib/ads.ts exported ADS_728X90 / ADS_300X250 / FIXED_300X600 as
   IIFE-evaluated module constants. They captured the FALLBACK array
   at module-load time, before the centralized RMP banner cache
   populates. Every render that referenced them got FALLBACK banners
   whose click_url was the direct advertiser URL — bypassing the
   centralized click logger entirely. 300x250s escaped because every
   page reads them via rotateAll() at render time.

2. Even when the new RMP-cache banners did render, click_url pointed
   at www.RMP.com/<property>/<slug>, which logs to adv.banner_clicks.
   The RMP analytics dashboard reads bb.ad_clicks. Repointed click
   URLs at advertising.RMP.com/r/<campaign_id> so all clicks land in
   the single table the dashboard queries.

Files: lib/ads.ts (remove broken IIFE exports + repoint click_url),
SidebarAdStack, Header, Footer, and 9 pages (reading-list, forum,
forum/thread, search, gear, technology, authors, show-coverage x2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 02:07:06 +00:00
Ryan Salazar
95eba5794b manufacturers + NAB Show naming sweep
- Standardize "NAB Show" / "2026 NAB Show" across badges, filter chips,
  and headings; never bare "NAB" in user-facing copy.
- /manufacturers/[slug]: normalize show name at render, strip
  randomstring suffix on booth, hide booth unless the show is within 90
  days, filter single-letter junk out of category display.
- News filter "NAB Show" chip loose-matches any nab-tagged article so
  legacy "NAB 2026" tags still surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 22:07:29 +00:00
Ryan Salazar
ec3118e7bc rewrite: dual-endpoint Ollama failover (AI_001 + AI_002)
Was: single OLLAMA_ENDPOINT — when AI_002 saturated, every rewrite job
backed off in place waiting for it to drain.

Now: tries OLLAMA_ENDPOINT (AI_002) first, instantly falls over to
OLLAMA_FALLBACK_ENDPOINT (AI_001) on 5xx/timeout before burning a
backoff sleep. Both nodes have identical model sets (llama3.1:70b,
llama3, qwen2.5-coder, etc.) so behavior is consistent across either.

Net effect: 2× GPU capacity, lower P95 latency, rewrites stop bunching
on a single saturated queue. The bb-forum-burst script already had this
pattern — pulling it into the BB rewrite path closes the gap.

OLLAMA_FALLBACK_ENDPOINT=http://10.10.0.66:11434 already set on the BB
Coolify app env.
2026-05-27 19:57:26 +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
6e3a93f90d fixes: dead /contact link + remove paid-AI fallbacks from hybridRouter
- /manufacturers footer "Contact our editors" linked to /contact which
  doesn't exist → 404. Swapped to mailto:editors@broadcastbeat.com.
- hybridAI() was falling through to api.anthropic.com whenever Ollama
  failed, which logs were tripping on after my earlier no-paid-AI swap
  (Article suggestions error: ANTHROPIC_API_KEY not configured).
  Removed both Anthropic call-paths in hybridAI + hybridAIStream.
  hybridAI now retries Ollama with 0/3/12s backoff and throws a clear
  error after exhaustion — callers (article-suggestions) already have
  graceful non-AI fallbacks.
2026-05-27 18:46:10 +00:00
Ryan Salazar
346868fea3 rewrite-pipeline: Ollama backoff + auto-publish to wp_imported_posts
Two real bugs:

1. Ollama 503s killed every rewrite job. The outer orchestrator retried
   3× but they all hit the same saturated Ollama in a 500ms window. New
   inner retry inside ollamaProvider.rewrite() with 0/8s/30s/90s back-
   off gives AI_002 room to drain its pending queue. 180s per-attempt
   timeout. Only retries on 5xx / 429 / "busy"-style errors — 4xx other
   than 429 surfaces immediately as a permanent error.

2. Successful rewrites landed in ai_rewritten_articles but the /news
   pages render from wp_imported_posts.content. Result: rewrites were
   invisible. Fix: on success, also INSERT INTO wp_imported_posts with
   the rewritten title/body/excerpt/category/tags + featured_image from
   the original submission. Uses a negative wp_id (-unix_ts) so it
   doesn't collide with WP-imported sequence values and is easy to spot
   in admin queries. Failure to publish doesn't fail the whole pipeline
   — the rewrite still exists in ai_rewritten_articles for backfill.

NEXT (separate session): a cron to retry the 12 still-failed jobs from
the last 7 days now that backoff is in place + a backfill script to
walk the 28k existing press-release articles and rewrite them in
batches when Ollama is idle (probably nights).
2026-05-27 17:38:59 +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
95a230b1ef articles: strip dead WP image tags from imported post bodies
rewriteLegacyImageUrlsInHtml only swapped URLs that had a mapping in
legacy-image-map.json — anything missing the map was left pointing at
the dead https://www.broadcastbeat.com/wp-content/uploads/ host (404
in browsers, bad for SEO, broken-image icons everywhere).

New stripDeadLegacyImages() pass runs after the rewrite and removes:
- <figure> blocks whose only inner image is a dead WP image
- <a> wrappers around dead images
- Bare <img dead> tags
- Empty <figure> leftovers
…leaving body copy intact and figcaption-less.

cleanLegacyImages() is the one-call helper that pipes both steps.
Applied via rowToArticle so every /news/<slug> render gets it.
2026-05-27 13:20:42 +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
8c4721bcbd homepage: All featured → goes to dedicated /news/featured page
Was linking to /news?category=Featured which silently fell back to the
unfiltered /news listing because "Featured" isn't in CATEGORIES and the
filter is tag-based, not category-column-based.

Adds getLegacyFeaturedArticles() that mirrors FeaturedBento's query
(category ILIKE 'featured', featured-flag-first sort) at limit 200, and
a thin /news/featured route that reuses NewsPageClient.
2026-05-26 19:24:18 +00:00
Ryan Salazar
107840c5f3 google news phase 1
- Homepage / Industry News query filters wp_imported_posts to
  ai_rewritten_at IS NOT NULL so raw press releases never surface on
  the editorial feed (NewsArticle JSON-LD was already in place).
- New /news-sitemap.xml route emits a Google News sitemap covering
  rewritten editorial published in the last 48 hours, with
  news:publication + news:publication_date + news:title + keywords.
- robots.ts now points crawlers at both sitemap.xml and
  news-sitemap.xml.

After deploy, submit broadcastbeat.com to Google News Publisher Center
to start serving in News.
2026-05-25 17:03:19 +00:00
Ryan Salazar
04efa3933a ads: switch to centralized RMP banner source; impression beacon → advertising.rmp; rearm viewport observer (12s debounce) for re-entry counting 2026-05-22 05:51:54 +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
c1705e1a4b ai: bump hybridRouter connection timeout 8s → 30s
Manual fire of bb-company-coverage-scan tonight failed because the
8s connect timeout was tripping on llama3.1:70b cold-load when
another model was resident in VRAM on AI_001. Bumped to 30s so
the orchestrator can wait through a model swap.

Also bumped background request timeout 120s → 180s for the same
reason — story generation against the 70B can take >2 min on a
saturated GPU.

Coolify env OLLAMA_ENDPOINT was simultaneously flipped from
AI_001 (10.10.0.66) to AI_002 (10.10.0.67) since AI_002 is less
loaded — AI_001 is doing author enrichment and scrubber work.
2026-05-20 13:32:09 +00:00
Ryan Salazar
4e11438963 seo: open site to search engines + expand sitemap to full catalog
- layout.tsx: flip robots.index from false→true (and the matching
  googleBot block). Drop the manual <meta name="robots"
  content="index, follow"> in the JSX head — it was duplicating
  (and conflicting with) the metadata-driven tag, so Google was
  seeing both "index, follow" AND "noindex, nofollow" and taking
  the more restrictive one. Now the metadata block is the single
  source of truth.
- sitemap.ts: expand from 5,000 articles to up to 45,000 (catalog
  is ~26k; cap leaves room for static + authors + events under
  Google's 50k-per-file ceiling). Add /authors/[slug] entries
  pulled from bb.author_profiles, and /events/[slug] entries
  pulled from bb.events. Drop the duplicate /articles/[slug]
  emissions — /news/[slug] is the canonical news URL.
- legacy-source.ts: page through the supabase query in 1,000-row
  chunks via Range headers so the 25k cap actually returns the
  full catalog (the previous `.limit(25000)` was getting silently
  truncated at PostgREST's default 1,000 max_rows).

Phase B (Ollama rewriter) is live; the press-release scrubber +
AI featured-image jobs are running in background to clean the
catalog. User explicitly chose to open SEO now rather than wait
for those jobs to finish.
2026-05-20 07:17:59 +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
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
Ryan Salazar
e8552794fe categories: update SECTION_CATEGORIES map to match the AI-derived taxonomy
After the AI re-categorization, bb.wp_imported_posts.category holds the
new taxonomy: 'Industry News', 'Show Coverage', 'Cameras & Capture',
'Streaming & IP', 'Audio', 'Live Production', 'Cloud & Workflow',
'Post Production', 'Broadcast Production', 'AI & Automation'.

The map that drives /gear, /technology, /show-coverage, /news page
queries still referenced the OLD taxonomy ('Gear', 'Reviews',
'Technology', 'Streaming') — so /gear and /technology effectively
returned nothing-matched and fell through to the catch-all section,
causing the duplicate-posts-across-sections behaviour the user
reported.

  /gear         → Cameras & Capture, Audio
  /technology   → Streaming & IP, Cloud & Workflow, AI & Automation,
                  Live Production, Post Production, Broadcast Production
  /show-coverage→ Show Coverage
  /news         → Industry News

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:55:43 +00:00
Ryan Salazar
308b27ec3e fix: use correct fallback image path for legacy articles 2026-05-18 22:07: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
migration-tooling
4c0ba55743 Use real article authors (remove Staff Reporter): removed mock staticArticles + changed fallback to Broadcast Beat 2026-05-16 00:54:53 +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
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)
597eac431d ads: restore Telycam 300x250 (was dropped from FALLBACK in the 8-banner refresh)
bb.banner_creatives row also inserted (AdSanity 191740, click_url
https://telycam.com/, dates 2024-05-07 → 2026-12-31, status active). The
creative was already in public/legacy/ads/ from the pre-Phase-B banner
pool — just needed the DB row + FALLBACK entry so SidebarAdStack picks
it up before the DB cache fills.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:07:39 +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)
7d03a99215 Revert "ads: restore Magewell 300x250 banner with placeholder SVG creative"
This reverts commit bd19ef8b7f.
2026-05-14 21:23:01 +00:00
Claude (Phase B)
bd19ef8b7f ads: restore Magewell 300x250 banner with placeholder SVG creative
Magewell was removed when Adsanity went away; PENDING_ads.md flagged it
along with AJA/Zixi/Lectrosonics/Opengear/Studio Hero for real creatives.
This restores Magewell with an in-house SVG placeholder (red/dark Magewell
brand color, wordmark + tagline + CTA chip) so the sidebar 300x250 stack
gets three rotating entries again instead of two. Real Magewell creative
to land when sourced from the vendor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 21:06:49 +00:00
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