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.
- /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.
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.
/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.
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.
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.
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.
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.
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.
- 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>
- 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>
- 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.
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.
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.
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.
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.
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)
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).
- 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.
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>
- 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>
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>
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>
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>
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>
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>
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>
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).
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.
- 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).
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.