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.