Commit Graph

35 Commits

Author SHA1 Message Date
d5259aaf47 feat: add Dockerfile to bypass nixpacks deno misdetection 2026-05-14 13:47:21 +00:00
3d225c2064 fix: force nixpacks to use npm run start, not deno 2026-05-14 13:44:27 +00:00
Claude Code
146b7259d4 wip-banner-system: real ads, logo fix, remove duplicate banner, clean footer
- Remove duplicate 728x90 LeaderboardAd below news ticker (per Ryan)
- Footer: remove NAB Official Media Partner badge
- Header: replace AdSlot stub with real AdImage rotation (3 verified 728x90s)
- Header: fix logo path from dead WP URL to local /assets/images/logo.png
- ads.ts: only include ads with verified real images (3x728x90, 2x300x250)
- ads.ts: remove 22 unrecovered ads — see PENDING_ads.md for upload list
- SidebarAdStack: handle null FIXED_300X600 gracefully (Blackmagic not recovered)
- public/legacy/ads/: 5 real ad images committed (Tower, Sony, LiveU 728x90; LiveU PAYG, Telycam 300x250)
- public/assets/images/logo.png: recovered from Wayback Machine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 12:23:22 +00:00
Ryan
25545dac48 fix(ads): render iframe OR fallback image, not both 2026-05-11 03:22:19 +00:00
Ryan
031ff6f220 feat(ads): dual tracking - AdSanity iframe + bb.banner_analytics with fixed imports and priority prop 2026-05-11 02:54:13 +00:00
Ryan
fc003fd285 ads: add self-hosted click + impression tracking to AdImage
Migration creates bb.banner_analytics (mirrors article_analytics: public
insert, admin read), applied separately to Supabase.

AdImage becomes a client component that:
- Fires one 'impression' event per render via IntersectionObserver at 50%
  visibility (avoids counting ads scrolled past at the fold).
- Fires a 'click' event on anchor click before the new tab opens.

Renders the same <a><AppImage/></a> as before — visually unchanged. Session
ID stored in sessionStorage as bb_session_id (random uuid, lazily created).
Both parents (SidebarAdStack, ArticleBody) were already 'use client', so no
boundary shifts. Tracking is best-effort and never blocks the user.
2026-05-10 22:00:48 +00:00
Ryan Salazar
aec6d0c43d Header: also rename mobile drawer 'My Account' to 'My Profile' (parity with desktop) 2026-05-10 14:48:48 +00:00
Ryan Salazar
1ca5ca108a Header: consolidate My Account/Profile into one link; group 9 admin tools into TOOLS dropdown
Top-bar nav changes:
- Authenticated user previously saw two separate links 'My Account' (-> /account)
  and 'My Profile' (-> /profile/{userId}). Replaced with a single 'My Profile'
  link -> /account.
- Admin block previously rendered 14 inline links (WP Import, Articles, Editorial,
  Users, Analytics, Audit Log, Notifications, Newsletter, Forum Seed, Company
  Tracker, Show Calendar, AI Console, Banned Terms, Dashboard) which sprawled
  horizontally past the available width. 9 of those are now grouped under a
  single TOOLS hover dropdown matching the existing NEWS/GEAR/SHOW
  COVERAGE/TECHNOLOGY pattern (Tailwind group-hover + group-focus-within for
  keyboard accessibility, role=menu / menuitem, aria-haspopup=true).
- Inline admin links retained per spec: WP Import, Articles, Editorial,
  Newsletter.
- TOOLS submenu: Users, Analytics, Audit Log, Notifications, Forum Seed,
  Company Tracker, Show Calendar, AI Console, Banned Terms.

The whole admin tier (inline + dropdown) remains gated by the existing isAdmin
check, so anonymous and non-admin users see nothing.
2026-05-10 14:41:46 +00:00
Ryan Salazar
fc4181cd46 HOTFIX: redirect post-login to /home-page (where auth works) instead of /contributor (race condition) 2026-05-10 13:10:51 +00:00
Ryan Salazar
73ad12a952 auth-debug: also render client-side supabase readout for parity comparison 2026-05-10 03:10:08 +00:00
Ryan Salazar
0b655a4669 debug: /auth-debug page to surface server-side auth state for a request 2026-05-09 23:34:07 +00:00
Ryan Salazar
e5de543268 debug: add console logging to login form for error visibility
Logs success payload (sans secrets — just success/migrated/legacy/uid/
email/hasToken booleans) and the post-login document.cookie state so
DevTools can show whether the auth cookie actually landed in the
browser before the /contributor redirect runs.
2026-05-09 10:01:39 +00:00
Ryan Salazar
89aaa7b36d supabase browser client: match server cookie attributes (SameSite=Lax, no Secure)
The browser-side createBrowserClient was writing cookies with
'Secure; SameSite=None' while the server-side createServerClient
writes the same cookie names with 'SameSite=Lax' (no Secure flag —
Next.js cookies().set() default).

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

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

Fix: align the JS-side cookie attributes with the server-side defaults
(SameSite=Lax, no Secure). Both writes now produce the same cookie
shape, no conflict.
2026-05-09 09:52:56 +00:00
Ryan Salazar
9e3b48c10d client-login: hard reload to /contributor instead of setSession+router.push
The previous attempt at hydrating the Supabase browser client via
supabase.auth.setSession() was racing against the server-set cookie.
The server cookie was written with Path=/; SameSite=Lax (no Secure
flag — Next.js cookies().set defaults), but the browser supabase
client's setAll handler writes cookies with Secure; SameSite=None.
The two writes for the same cookie name produced inconsistent state.

Drop setSession + router.push entirely. Instead, do a hard
window.location.href = '/contributor' redirect after a successful
POST. The server renders /contributor with the freshly-set auth
cookie; the browser supabase client boots up reading it from
document.cookie on page load. No client-side hydration race.
2026-05-09 06:39:28 +00:00
Ryan Salazar
275f0c9627 client-login: hydrate Supabase session from API response before redirect
The wp-login API sets the auth cookie via NextResponse, but a
client-side router.push() doesn't always pick up the new cookie before
the destination page's AuthContext snapshots auth state — leading to a
brief 'logged out' flash on /contributor right after a successful
login.

Fix: after a successful POST to /api/auth/wp-login, call
supabase.auth.setSession({ access_token, refresh_token }) using the
session payload returned by the API. That hydrates the Supabase
browser client (and therefore AuthContext) immediately, so the next
render is authenticated. router.refresh() is also called to invalidate
the Next.js Router Cache for any stale RSC payloads.

Also adds console.error on both the !res.ok branch and the catch block
so login failures are inspectable in DevTools.
2026-05-09 03:12:50 +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
0674d0188b Header redesign: dropdowns + site-wide leaderboard, drop secondary nav
Reorganize the navigation:
- navLinks now carries an optional dropdown[] per section. NEWS / GEAR &
  REVIEWS / SHOW COVERAGE / TECHNOLOGY get sub-items pulled from the old
  flat categoryLinks list.
- Desktop main nav renders each top-level item as a hover-triggered
  dropdown (CSS-only via Tailwind group-hover + group-focus-within for
  keyboard accessibility). aria-haspopup advertises the popup; the panel
  is role=menu with menuitem children.
- Mobile drawer's category list now groups by parent section instead of
  showing a flat list.
- Removed the secondary category sub-nav row that previously sat below
  the main nav (its content is in the dropdowns now).
- Added a site-wide 728x90 ad slot between the top utility bar and the
  main nav (md+ only). Uses <AdSlot zone='homepage-top'> so the existing
  ad-rotation pool serves it.

Net: 566 → ~600 lines (markup heavier than what was removed because the
dropdown panels are inline).
2026-05-08 22:25:47 +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
c642b4deb5 Header: split Sign In / My Account by auth state, route Sign In to /client-login
Desktop nav (and mobile drawer) now show different account-related
links depending on auth state:

- Anonymous: Sign In (-> /client-login) + Register
- Authenticated: My Account + My Profile
- Admin-tier (administrator/admin/super_admin/editor/support): plus the
  full admin block (already gated by isAdmin from earlier commit)

Previously a single 'Sign In / My Account' link pointed at /account
regardless of auth state, which forced anonymous users through a
server-side bounce to the login page. Now Sign In links straight to the
canonical /client-login.
2026-05-08 21:05:32 +00:00
Ryan Salazar
61aa8e6ead next.config: fix /login redirect self-loop, point at /client-login 2026-05-08 20:37:41 +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
66ff0f08ae add client login page for PR Firms 2026-05-08 18:28:41 +00:00
Ryan Salazar
9c5b0e4959 wire real ad rotation: Blackmagic 300x600 fixed + rotating 300x250s
Replace the placeholder Unsplash banner array with the live AdSanity
inventory served from /legacy/ads/ (same 27 active creatives currently
on broadcastbeat.com).

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

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

Same banners as the live broadcastbeat.com (sourced from the existing
WP AdSanity install we cached locally during Phase 2). Each <a> opens
in a new tab and uses rel='sponsored noopener noreferrer'.
2026-05-08 18:16:07 +00:00
Ryan Salazar
7c9d2c8c79 remove rocket.new leaderboard banner; re-add admin nav guard
- src/app/home-page/components/LeaderboardAd.tsx: drop the hardcoded
  img.rocket.new image (rocket_gen_img_1bebfd3c7) at the top of the
  homepage. Replaced with <AdSlot zone='homepage-top' size='728x90' />
  to keep the slot for the eventual ad-server wiring while removing the
  rocket.new dependency entirely.
- src/components/Header.tsx: re-add the isAdmin gate that was reverted
  in 0d51dad. Reads bb.user_profiles.role and only renders the desktop
  admin nav block (WP Import, Articles, Editorial, Users, Analytics,
  Audit Log, Notifications, Newsletter, Forum Seed, Company Tracker,
  Show Calendar, AI Console, Banned Terms, Dashboard) and the mobile WP
  Import link when role is in (administrator, admin, super_admin,
  editor, support). 'support' is admin-tier per the new RBAC matrix.
  Page-level auth checks on each /admin/* route remain authoritative;
  this only suppresses menu visibility for non-admin/anonymous.
2026-05-08 15:03:16 +00:00
Ryan Salazar
f1e1d5ea76 delete sampleArticles seed; serve all sections from bb.wp_imported_posts (DB)
Eliminates 72 hardcoded rocket.new image refs in one shot. Section pages
(/gear, /technology, /show-coverage, /news) now render from
bb.wp_imported_posts via getLegacyArticlesBySection(section), which maps
each non-news section to a curated tag set (e.g. gear -> Audio, Cameras,
lighting, Blackmagic Design, monitoring; technology -> AI, Streaming,
OTT, IP, Cloud, AV; show-coverage -> NAB, IBC, BroadcastAsia).

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

Inline rocket.new image refs in homepage components (ArticleFeed,
FeaturedBento) are unchanged in this commit; those are inline seed
arrays in the components, not imports of sampleArticles.
2026-05-08 14:31:25 +00:00
Ryan Salazar
08df1ad4c1 wire /news/[slug] and /articles/[slug] to read bb.wp_imported_posts via ISR
- new src/lib/articles/legacy-source.ts: server-side Supabase reader
  (bb schema, anon key, RLS public_read policy) that maps imported rows
  to the existing Article shape, applies rewriteLegacyImageUrl for the
  featured image and rewriteLegacyImageUrlsInHtml across the body.
- both [slug]/page.tsx routes now: try seed sampleArticles first; on
  miss fall back to getLegacyArticleBySlug. Related articles match via
  same-category recent posts when imported.
- generateStaticParams pre-renders seed slugs + 50 most recent imported
  posts. dynamicParams=true means anything else server-renders on first
  request and ISR caches it for 1h (revalidate=3600). Avoids a 4577-page
  build.
- Hardcoded builtwithrocket.new URLs in JSON-LD replaced with
  process.env.NEXT_PUBLIC_SITE_URL.
2026-05-08 06:25:29 +00:00
Ryan Salazar
54154e2d3c add AdSlot component and place banner zones on article pages
Adds <AdSlot zone size /> server component that renders a labeled
placeholder for now (size + zone exposed as data attrs for the future
bb.ad_placements lookup).

Inserted on news/[slug] article template:
- 728×90 leaderboard above the <article>
- 300×600 half-page at top of sidebar
- 300×250 mid-rectangle in sidebar (replaces the old hand-rolled
  placeholder div)
- 728×90 leaderboard below the </article>, before related-articles

Same scheme should be applied to articles/[slug]/page.tsx in a follow-up
once that template's body is finalized.
2026-05-08 04:25:18 +00:00
Ryan Salazar
3334d11597 remove visible publication dates from articles (keep JSON-LD for SEO)
Drops the rendered date span from:
- ArticleFeed.tsx (homepage feed)
- news/page.tsx (news list — also drops the orphan separator)
- news/[slug]/NewsArticleDetailClient.tsx (article header + related-articles row, drops paired separators)
- AISuggestedArticles.tsx (sidebar — keeps readTime alone)

JSON-LD datePublished/publishedTime in news/[slug]/page.tsx and
articles/[slug]/page.tsx is unchanged so search engines still see the
publication date.
2026-05-08 00:18:12 +00:00
Ryan Salazar
eabe31f911 temporarily hide SpotlightCarousel (old stories) 2026-05-08 00:11:19 +00:00
Ryan Salazar
ef1921ca7c add legacy image rewrite helper
src/lib/legacy-image.ts exports rewriteLegacyImageUrl(url) and
rewriteLegacyImageUrlsInHtml(html) which translate WP origin URLs
(https://www.broadcastbeat.com/wp-content/uploads/...) to local paths
under /legacy/... served from the new persistent storage volume mounted
at /app/public/legacy/.

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

Use this helper at server-render time when emitting <img>/<Image> for
imported WP articles, so the 707 MB on-host cache replaces external
fetches to the origin.
2026-05-08 00:04:34 +00:00
Ryan Salazar
0d51dade6e Revert "hide admin nav links unless user has admin role"
This reverts commit 92a256bbe0.
2026-05-07 22:50:35 +00:00
Ryan Salazar
92a256bbe0 hide admin nav links unless user has admin role
Header was rendering all admin links unconditionally to every visitor
(including unauthenticated). Add an isAdmin state derived from
user_profiles.role (administrator|admin|super_admin) and wrap both the
desktop admin block and the mobile WP Import link in the guard.

Note: page-level auth checks on each /admin/* route remain authoritative;
this only suppresses menu visibility.
2026-05-07 22:25:33 +00:00
Ryan Salazar
7f63cf64fb force node provider in nixpacks (skip deno detection from supabase/functions/*.ts) 2026-05-07 17:00:42 +00:00
Ryan Salazar
70aa1ad46e initial commit: rocket.new export of broadcastbeat 2026-05-07 16:39:17 +00:00