Skip to content

feat(landing): intitialize landing page v1 design#17

Merged
Ryanakml merged 2 commits intomainfrom
feature/landing-page
Mar 23, 2026
Merged

feat(landing): intitialize landing page v1 design#17
Ryanakml merged 2 commits intomainfrom
feature/landing-page

Conversation

@Ryanakml
Copy link
Copy Markdown
Owner

@Ryanakml Ryanakml commented Mar 22, 2026

Summary by CodeRabbit

  • New Features

    • Public landing page with marketing, pricing, use-case articles, and Early Access modal + signup
    • Dashboard home page showing user info and role-based KPIs
    • Landing analytics/tracking with session capture
  • Bug Fixes

    • Post-login now redirects to the dashboard
  • UI/Style

    • Updated branding and login styling
    • Unified navigation via a new DashboardShell for consistent layout

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-fuse-dashboard Ready Ready Preview, Comment Mar 23, 2026 2:26pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6a15f317-da97-4056-a1a0-b8bba442e9df

📥 Commits

Reviewing files that changed from the base of the PR and between 8ce7071 and e0543bc.

📒 Files selected for processing (1)
  • apps/dashboard/app/login/layout.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/dashboard/app/login/layout.tsx

📝 Walkthrough

Walkthrough

This PR introduces a public landing site (static HTML, assets, tracking, and early-access modal), adds use-case article pages and styles, creates Supabase tables for landing tracking and whitelist signups, introduces a DashboardShell component and migrates authenticated pages into it, and adjusts auth/login flows and middleware rules.

Changes

Cohort / File(s) Summary
Landing HTML & page
apps/dashboard/app/page.tsx, apps/dashboard/lib/landing/landing-body.html
Replaced authenticated home with public landing page that loads static HTML, substitutes {{CURRENT_YEAR}}, renders via dangerouslySetInnerHTML, and injects landing scripts/config.
Landing assets (CSS/JS & vendors)
apps/dashboard/public/landing/css/style.css, apps/dashboard/public/landing/css/modal-override.css, apps/dashboard/public/landing/js/landing.js, apps/dashboard/public/landing/vendors/...
Added full landing styles, z-index modal overrides, landing behavior script (theme, scroll, SVG inlining, Supabase tracking, modal wiring), and vendor libraries (AOS, Glightbox, PureCounter).
Landing content & data
apps/dashboard/lib/landing/use-case-articles.ts, apps/dashboard/app/use-cases/[slug]/page.tsx, apps/dashboard/app/use-cases/[slug]/use-case.css
Added use-case article data module, dynamic article page with metadata and static params, and dedicated CSS for use-case pages.
Early access modal & signup API
apps/dashboard/components/landing/early-access-modal-portal.tsx, apps/dashboard/app/api/whitelist-signup/route.ts
Added client modal portal rendering form; new POST /api/whitelist-signup route validates body and inserts into whitelist_signups, returning 400/500/200 JSON responses.
Supabase migration
supabase/migrations/202603221215_landing_tracking_tables.sql
Creates landing_sessions and whitelist_signups tables, indexes, and grants/revokes privileges per migration.
Dashboard shell & page moves
apps/dashboard/components/dashboard-shell.tsx, apps/dashboard/app/conversations/page.tsx, apps/dashboard/app/conversations/[id]/page.tsx, apps/dashboard/app/escalations/page.tsx, apps/dashboard/app/dashboard/page.tsx
Added DashboardShell component and wrapped conversation/escalation pages and new authenticated /dashboard page inside it; /dashboard shows session info, role-based sections, and sign-out form.
Auth, login, layouts, actions, middleware
apps/dashboard/app/login/page.tsx, apps/dashboard/app/login/client.tsx, apps/dashboard/app/login/layout.tsx, apps/dashboard/app/layout.tsx, apps/dashboard/app/actions.ts, apps/dashboard/middleware.ts
Login page styling updated (SVG, fonts); login client now redirects to /dashboard; added LoginLayout; root layout metadata updated and header removed; handleSignOut revalidates /dashboard; middleware now treats / and /use-cases as public and broadens asset exclusions.
Misc config
apps/dashboard/eslint.config.mjs
Added public/landing/vendors/** to ESLint global ignores.

Sequence Diagram

sequenceDiagram
    participant User as User Browser
    participant Landing as Landing Page (client)
    participant Modal as Early Access Modal (portal)
    participant API as /api/whitelist-signup
    participant DB as Supabase

    User->>Landing: Load landing page
    Landing->>Landing: Initialize tracking & modal handlers
    User->>Modal: Open early access modal (click)
    User->>Modal: Fill form (name,email,clinic_type) and submit
    Modal->>API: POST /api/whitelist-signup {name,email,clinic_type}
    API->>API: Validate payload
    alt missing fields
        API-->>Modal: 400 {error}
    else valid
        API->>DB: INSERT INTO whitelist_signups(...)
        alt insert success
            DB-->>API: inserted record
            API-->>Modal: 200 {success:true}
            Modal->>Modal: show thank-you state
        else insert error
            DB-->>API: error
            API-->>Modal: 500 {error}
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰 Hopped in quick to stitch the site anew,

Signed up, sent events, and wrapped pages too.
Use-cases bloom, dashboards don a shell,
I nudged the modal—then scurried off to dwell. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title describes the main intent of the PR (landing page initialization with v1 design), and most changes reflect this: new landing page markup, components, styling, and scripts. However, the title contains a typo ('intitialize' should be 'initialize'), and several significant changes fall outside landing page scope (dashboard shell wrapping, middleware updates, new dashboard home page, conversation/escalation page shells, new whitelist signup API route).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/landing-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/dashboard/app/login/page.tsx (1)

8-12: ⚠️ Potential issue | 🟠 Major

Await searchParams before reading query messages.

In Next.js 16.1.6, searchParams is a Promise and must be awaited. Reading it synchronously leaves both error and message banners blank even when /login?error=... or ?message=... is passed.

Suggested fix
-export default function LoginPage({
+export default async function LoginPage({
   searchParams,
 }: {
-  searchParams: { message?: string; error?: string };
+  searchParams: Promise<{ message?: string; error?: string }>;
 }) {
+  const { error, message } = await searchParams;
+
   return (
@@
-        {searchParams?.error && (
+        {error && (
           <div className="mb-4 rounded bg-red-100 p-3 text-red-700 dark:bg-red-950/40 dark:text-red-200">
-            {searchParams.error}
+            {error}
           </div>
         )}
 
-        {searchParams?.message && (
+        {message && (
           <div className="mb-4 rounded bg-green-100 p-3 text-green-700 dark:bg-green-950/40 dark:text-green-200">
-            {searchParams.message}
+            {message}
           </div>
         )}

Also applies to: 58-67

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/login/page.tsx` around lines 8 - 12, The LoginPage
component reads searchParams synchronously but in Next.js 16.1.6 searchParams is
a Promise; make LoginPage async and await searchParams before extracting message
and error so the banners render correctly. Update the LoginPage function
signature to "export default async function LoginPage({ searchParams }: {
searchParams: Promise<{ message?: string; error?: string }>; })" (or cast
appropriately), then do "const params = await searchParams" and use
params.message and params.error where the message/error banners are rendered
(references: LoginPage, searchParams, message, error).
🧹 Nitpick comments (13)
apps/dashboard/app/conversations/[id]/page.tsx (1)

17-18: Consider removing debug logging.

Multiple console.log statements throughout the file (lines 17-18, 28, 33, 43-44, 57) should be cleaned up before production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/conversations/`[id]/page.tsx around lines 17 - 18, Remove
the stray console.log debug statements from the Conversations page (remove the
console.log calls that print API_URL and "Fetching conversation timeline..." and
the other console.log occurrences in this file), or replace them with a proper
logger that is disabled in production (e.g., use a debug/logger utility or guard
with NODE_ENV !== 'production'); update the default export component (the page
component) to no longer emit these console logs so no sensitive/debug output
appears in production.
apps/dashboard/app/escalations/page.tsx (1)

20-21: Consider removing debug logging.

Similar to actions.ts, these console.logs should be removed before production.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/escalations/page.tsx` around lines 20 - 21, Remove the two
development console.log statements in apps/dashboard/app/escalations/page.tsx
(the lines logging 'API URL:' and 'Fetching escalations...'); either delete them
entirely or replace them with a conditional debug logger that only runs when a
DEBUG/VERBOSE env flag is enabled or when NODE_ENV !== 'production', ensuring no
debug output reaches production.
supabase/migrations/202603221215_landing_tracking_tables.sql (1)

15-22: Consider adding NOT NULL and unique constraints on email.

The email column allows NULL values and duplicates. For a whitelist signup form, you likely want to:

  1. Require an email (NOT NULL)
  2. Prevent duplicate signups (unique constraint or unique index)
♻️ Proposed fix
 create table if not exists public.whitelist_signups (
   id uuid primary key default gen_random_uuid(),
   session_key text references public.landing_sessions(session_key) on update cascade on delete set null,
   name text,
-  email text,
+  email text not null,
   clinic_type text,
   submitted_at timestamptz not null default now()
 );

And change the email index to enforce uniqueness:

-create index if not exists idx_whitelist_signups_email on public.whitelist_signups(email);
+create unique index if not exists idx_whitelist_signups_email on public.whitelist_signups(email);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/202603221215_landing_tracking_tables.sql` around lines 15
- 22, The whitelist_signups table's email column currently allows NULLs and
duplicates; update the table definition for public.whitelist_signups to make the
email column NOT NULL and add a uniqueness constraint or unique index on email
(e.g., UNIQUE CONSTRAINT on column email or a UNIQUE INDEX like
whitelist_signups_email_idx) so duplicate signups are prevented; ensure you
reference the table name whitelist_signups and column email when applying the
change so the migration enforces both NOT NULL and uniqueness.
apps/dashboard/app/actions.ts (1)

23-26: Consider removing debug logging before production.

These console logs expose session state and API configuration details. While helpful during development, they should be removed or guarded behind a debug flag before release.

🧹 Proposed cleanup
-  console.log('Session error:', error);
-  console.log('Has session:', !!session);
-  console.log('Has token:', !!session?.access_token);
-  console.log('API URL:', process.env.API_URL);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/actions.ts` around lines 23 - 26, Remove or guard the
debug console.logs that expose session and API details (the statements
console.log('Session error:', error), console.log('Has session:', !!session),
console.log('Has token:', !!session?.access_token), console.log('API URL:',
process.env.API_URL')); either delete them for production or wrap them behind a
debug flag (e.g., if (process.env.NODE_ENV !== 'production') { ... } or
process.env.DEBUG) and/or replace with a proper logger call at debug level
(e.g., processLogger.debug(...)) so sensitive info is not logged in production.
apps/dashboard/components/landing/early-access-modal-portal.tsx (1)

6-8: Consider adding explicit synchronization between modal creation and landing.js initialization.

The current implementation relies on implicit timing where the React portal renders before landing.js executes (due to strategy="afterInteractive"). While this should work correctly, using useEffect to signal when the modal DOM is ready makes the dependency explicit and guards against potential timing issues:

Suggested direction
 export function EarlyAccessModalPortal() {
-  if (typeof document === 'undefined') return null;
+  const [mounted, setMounted] = useState(false);
+  useEffect(() => {
+    setMounted(true);
+    window.dispatchEvent(new Event('early-access-modal-mounted'));
+  }, []);
+  if (!mounted) return null;

Then update landing.js to listen for early-access-modal-mounted and initialize (or retry initialization) at that point.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/components/landing/early-access-modal-portal.tsx` around lines
6 - 8, Wrap the portal mount in a React effect that dispatches a custom DOM
event when the modal root is mounted so landing.js can explicitly wait/retry;
specifically, in early-access-modal-portal.tsx use useEffect inside the
component that creates the portal (where createPortal is returned and the
document guard exists) to dispatch an 'early-access-modal-mounted' event on
window/document after the modal DOM node is appended, and ensure landing.js
listens for that 'early-access-modal-mounted' event and triggers its
initialization or retry logic when received.
apps/dashboard/public/landing/css/style.css (1)

21-31: Merge the duplicated .service-card .icon rules.

The later block overrides the earlier width, height, background, and margin values, so the 64px centering tweak never really wins. Keep one source of truth or move the intended override to the end.

Also applies to: 1040-1049

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/public/landing/css/style.css` around lines 21 - 31, There are
duplicate .service-card .icon rules where the later block overrides
width/height/background/margin set earlier; consolidate them into a single rule
(or keep the intended override only) by merging the properties from both
definitions into one authoritative .service-card .icon selector (or move the
intended overriding declarations to the final occurrence) so the 64px
width/height, centering, background-color and margin-bottom are defined once and
not unintentionally overridden.
apps/dashboard/public/landing/js/landing.js (2)

74-75: window.pageYOffset is deprecated.

Use window.scrollY for consistency (already used elsewhere in this file at line 181).

🔧 Use scrollY consistently
     var onScroll = function () {
-      var scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+      var scrollTop = window.scrollY || document.documentElement.scrollTop;
       if (scrollTop > 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/public/landing/js/landing.js` around lines 74 - 75, In the
onScroll handler, replace the deprecated usage of window.pageYOffset when
computing scrollTop with window.scrollY for consistency with other usages in
this file (see onScroll and the scrollTop variable); update the expression
inside the onScroll function that sets scrollTop to use window.scrollY ||
document.documentElement.scrollTop so behavior remains robust.

377-402: Scroll event listener lacks throttling.

initScrollDepthTracking attaches a scroll listener that fires on every scroll event. While the threshold checks prevent redundant trackEvent calls, the function still executes on every scroll frame which can impact performance on low-end devices.

🔧 Add simple throttling
   function initScrollDepthTracking() {
     var thresholds = [25, 50, 75, 100];
     var firedThresholds = {};
+    var ticking = false;

     function computeScrollPercent() {
       var doc = document.documentElement;
       var scrollableHeight = doc.scrollHeight - window.innerHeight;
       if (scrollableHeight <= 0) {
         return 100;
       }
       return Math.min(100, Math.round((window.scrollY / scrollableHeight) * 100));
     }

-    function onScroll() {
+    function checkThresholds() {
       var percent = computeScrollPercent();
       thresholds.forEach(function (threshold) {
         if (percent >= threshold && !firedThresholds[threshold]) {
           firedThresholds[threshold] = true;
           trackEvent('scroll_depth', { depth_percent: threshold });
         }
       });
+      ticking = false;
+    }
+
+    function onScroll() {
+      if (!ticking) {
+        window.requestAnimationFrame(checkThresholds);
+        ticking = true;
+      }
     }

-    onScroll();
+    checkThresholds();
     window.addEventListener('scroll', onScroll);
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/public/landing/js/landing.js` around lines 377 - 402, The
scroll handler in initScrollDepthTracking runs on every scroll event which can
hurt performance; modify onScroll (and its use of computeScrollPercent,
thresholds, and firedThresholds) to be throttled—e.g., wrap the work in a
requestAnimationFrame or a small time-based throttle so computeScrollPercent and
the thresholds.forEach logic only run at most once per animation frame or every
N ms, and replace window.addEventListener('scroll', onScroll) with the throttled
version while keeping trackEvent calls unchanged.
apps/dashboard/app/use-cases/[slug]/page.tsx (4)

137-145: Using content as React key can cause issues with duplicate text.

Using paragraph content as key works only if all paragraphs are unique. If two paragraphs have identical text, React will emit a duplicate key warning and may exhibit unexpected behavior.

🔧 Use index-based keys for safety
-              {article.intro.map((paragraph) => (
-                <p key={paragraph}>{paragraph}</p>
+              {article.intro.map((paragraph, index) => (
+                <p key={index}>{paragraph}</p>
               ))}
...
-                  {section.paragraphs?.map((paragraph) => (
-                    <p key={paragraph}>{paragraph}</p>
+                  {section.paragraphs?.map((paragraph, index) => (
+                    <p key={index}>{paragraph}</p>
                   ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/use-cases/`[slug]/page.tsx around lines 137 - 145, The JSX
uses paragraph text as React keys in the mappings for article.intro and
section.paragraphs causing duplicate-key issues; update the map callbacks (the
article.intro.map and section.paragraphs?.map in the component rendering the
"use-case-section") to use stable keys instead — e.g., use the array index or a
combination like `${index}-${paragraph}` as the key (or a unique id if
available) to ensure keys are unique and prevent React warnings/bugs.

43-51: Redundant fallback in getNextArticle.

Line 50's ?? USE_CASE_ARTICLES[0]! is unreachable. After the empty-array check (line 44-46), modulo arithmetic always produces a valid index. If currentIndex is -1 (slug not found), nextIndex becomes 0, which is valid.

🔧 Simplify by removing redundant fallback
 function getNextArticle(currentSlug: UseCaseSlug): UseCaseArticle {
   if (USE_CASE_ARTICLES.length === 0) {
     throw new Error('Use case articles are not configured.');
   }

   const currentIndex = USE_CASE_ARTICLES.findIndex((article) => article.slug === currentSlug);
   const nextIndex = (currentIndex + 1) % USE_CASE_ARTICLES.length;
-  return USE_CASE_ARTICLES[nextIndex] ?? USE_CASE_ARTICLES[0]!;
+  return USE_CASE_ARTICLES[nextIndex]!;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/use-cases/`[slug]/page.tsx around lines 43 - 51, The
return expression in getNextArticle contains an unreachable fallback (the "??
USE_CASE_ARTICLES[0]!" part) because you already throw on an empty array and
nextIndex is always a valid index; remove the nullish coalescing and non-null
assertion and simply return USE_CASE_ARTICLES[nextIndex] from getNextArticle to
simplify the function.

183-188: Use next/image for optimized image loading.

The static analysis correctly flags that <img> bypasses Next.js image optimization (lazy loading, responsive sizing, format conversion). For external images, configure the domain in next.config.js and use the Image component.

🔧 Replace with next/image
+import Image from 'next/image';
...
-                    <img
-                      src="https://cdn.simpleicons.org/whatsapp/ffffff"
-                      alt="WhatsApp"
-                      style={{ height: '22px', width: '22px' }}
-                    />
+                    <Image
+                      src="https://cdn.simpleicons.org/whatsapp/ffffff"
+                      alt="WhatsApp"
+                      width={22}
+                      height={22}
+                      unoptimized
+                    />

Add to next.config.js:

images: {
  remotePatterns: [
    { protocol: 'https', hostname: 'cdn.simpleicons.org' },
  ],
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/use-cases/`[slug]/page.tsx around lines 183 - 188, Replace
the plain <img> (the element with src
"https://cdn.simpleicons.org/whatsapp/ffffff" and alt "WhatsApp" inside the Book
a Demo markup in page.tsx) with Next.js' Image component: import Image from
'next/image' and render it with explicit width/height (or responsive layout
props) so Next can optimize and lazy-load the asset; also add the
cdn.simpleicons.org entry to next.config.js under images.remotePatterns
(protocol https, hostname cdn.simpleicons.org) so external loading is allowed.
Ensure you remove the inline style on the <img> and use Image sizing props or a
wrapper for styling.

70-78: Load fonts and stylesheets via Next.js conventions.

In Next.js App Router, fonts should be loaded using next/font for automatic optimization, and global stylesheets should be imported in layout.tsx. Inline <link> tags in page components:

  • Load fonts only for this page (not cached across routes)
  • Bypass Next.js optimization (font subsetting, preloading)
  • Can cause layout shift (FOUT/FOIT)

Consider moving the Inter font to use next/font/google and importing Bootstrap CSS in the root layout or via @import in a global stylesheet.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/use-cases/`[slug]/page.tsx around lines 70 - 78, The page
is directly injecting <link> tags for Inter and CSS which bypasses Next.js
optimizations; remove those tags from the page component (the default exported
Page in page.tsx) and instead load Inter via next/font/google (e.g., import
Inter from 'next/font/google' and use the returned font class at the root) and
import Bootstrap and other global CSS in the root layout (layout.tsx) or a
global stylesheet (e.g., import '/landing/vendors/bootstrap/bootstrap.min.css'
and '/landing/css/style.css' from your root layout or global CSS file) so fonts
are optimized and styles are applied app-wide.
apps/dashboard/lib/landing/use-case-articles.ts (1)

92-97: Placeholder testimonials need real content before production.

Multiple articles contain placeholder testimonials with [Clinic name, City], [Dr. Name, Practice Name], [Practice Name, City], and [X]%. These should be replaced with real testimonials or removed before launch to avoid shipping placeholder text to users.

Would you like me to open an issue to track replacing these placeholders with real testimonials?

Also applies to: 151-156, 210-215, 267-272, 325-330, 383-388

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/lib/landing/use-case-articles.ts` around lines 92 - 97, The
testimonial entries in use-case-articles.ts contain placeholder text (e.g.,
attribution values like "[Clinic name, City]" and percentage tokens like "[X]%")
inside the testimonial object (fields title, quote, attribution); replace each
placeholder with real testimonial content or remove the testimonial object
entirely for any article that lacks verified copy. Locate each article's
testimonial object in the file (instances around the shown snippet and at the
other reported ranges) and update the attribution and numeric placeholders to
actual clinic/dr/ practice names and real metrics, or delete the testimonial
field if you don't have verified content yet.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/dashboard/app/api/whitelist-signup/route.ts`:
- Around line 6-15: Wrap the call to request.json() in a try-catch inside the
route handler to catch JSON parse errors and return NextResponse.json({ error:
'Invalid JSON payload' }, { status: 400 }); when parsing fails; after insertion
with supabase.from('whitelist_signups').insert([{ name, email, clinic_type }]),
do not return error.message to the client — instead log the full error
server-side (use your logger or console.error) and return a generic
NextResponse.json({ error: 'Unable to create signup' }, { status: 500 }); also
keep the existing validation for name/email/clinic_type and ensure the catch
covers both parse and unexpected exceptions to return a 500 generic error
response.

In `@apps/dashboard/app/login/layout.tsx`:
- Around line 1-6: The LoginLayout currently re-declares document-level tags
(<html> and <body>) which must only live in the root layout; remove those tags
in the LoginLayout function and return the children directly (e.g., a React
fragment or a wrapper div) so LoginLayout({ children }: { children:
React.ReactNode }) only renders the route content and does not include <html> or
<body> (also remove suppressHydrationWarning usage here if present and keep any
such document-level attributes in the root layout).

In `@apps/dashboard/app/page.tsx`:
- Around line 16-17: The code currently falls back to empty strings for
publicSupabaseUrl and publicSupabaseAnonKey which hides missing envs and causes
client-side failures; instead remove the silent defaults and add a startup-time
validation in page.tsx that checks process.env.NEXT_PUBLIC_SUPABASE_URL and
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY and throws a clear Error if either is
missing (referencing the publicSupabaseUrl and publicSupabaseAnonKey variables)
so the build/deploy fails fast with an actionable message.
- Around line 46-49: Change the floating CDN import in the Script component to
the exact Supabase browser client version that matches package.json to avoid
version drift: update the src on the Script JSX element (the Script component
that currently points to "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2")
to the pinned release used by the app (for example
"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.48.1") so the browser
client version matches the server-side dependency.

In `@apps/dashboard/lib/landing/landing-body.html`:
- Around line 43-55: There are two visible offcanvas triggers on small screens;
ensure only one is shown by keeping the existing <button class="navbar-toggler">
as the mobile trigger and hiding the custom trigger (the other offcanvas toggle
in the 75-114 block) on <lg viewports (e.g., add d-none d-lg-block to the custom
trigger or add d-lg-none to the navbar-toggler depending on desired behavior) so
only one control toggles the offcanvas with id="fbs__net-navbars"; also add an
accessible label to the close button (the <button class="btn-close">) by adding
an aria-label="Close" attribute.
- Around line 1174-1176: Replace the hardcoded year string "&copy; 2025
Chattiphy. All rights reserved." inside the footer div (the element with class
"row credits pt-3") with the page-loader token "{{CURRENT_YEAR}}", i.e. output
"&copy; {{CURRENT_YEAR}} Chattiphy. All rights reserved.", so the existing
page.tsx replacement logic that substitutes "{{CURRENT_YEAR}}" will keep the
footer year current.
- Around line 1125-1127: The footer subscribe input is missing a name attribute
so form submission/FormData won't include the email; update the input element
inside the form (the element with class "form-control" and type="email") to
include a meaningful name (e.g., name="email") so native submission and new
FormData(form) will serialize the email value correctly.

In `@apps/dashboard/middleware.ts`:
- Around line 43-48: The isPublicPath check in middleware currently uses broad
startsWith checks that can accidentally match protected routes (e.g.,
"/login-admin"); update the boolean expression in the isPublicPath computation
to use exact matches or slash-delimited startsWith checks: replace
pathname.startsWith('/login') with (pathname === '/login' ||
pathname.startsWith('/login/')), replace pathname.startsWith('/use-cases') with
(pathname === '/use-cases' || pathname.startsWith('/use-cases/')), and similarly
ensure '/api' is matched as (pathname === '/api' ||
pathname.startsWith('/api/')), keeping pathname === '/' and pathname ===
'/auth/callback' as exact checks so only intended public routes are allowed.

In `@apps/dashboard/public/landing/js/landing.js`:
- Around line 199-238: The initInlineSvg function currently fetches and injects
external SVGs directly, risking XSS; update initInlineSvg to only fetch SVGs
from trusted origins (check imgElement.src using new URL(imgURL, location.href)
and allow same-origin or a configured whitelist), and after parsing svgText into
svgElement remove dangerous content before replaceWith (strip <script> elements,
remove attributes starting with "on" and any javascript: URLs, and/or run the
parsed SVG through a sanitizer like DOMPurify) so only safe SVG markup is
inserted; keep the existing fetch flow but bail out and log a warning for
disallowed origins or failed sanitization.
- Around line 290-302: The current generatedSessionKey (btoa(navigator.userAgent
+ new Date().toDateString()).slice(0, 32)) causes collisions; replace it with a
cryptographically strong unique ID per visitor (e.g., use crypto.getRandomValues
or a UUID generator) when creating generatedSessionKey, then persist that value
to localStorage under sessionStorageKey (same logic around reading/storing in
localStorage and the try/catch) so sessionKey remains unique per visitor and
stable across page loads; update any references to
generatedSessionKey/sessionKey to use the new unique ID generation.
- Around line 356-375: Enable RLS on the whitelist_signups table and add
explicit policies: create a policy that allows anonymous INSERTs (so the client
function submitWhitelist can continue to insert) and separate restrictive
policies that DENY or explicitly prevent SELECT, UPDATE, DELETE for anon/other
roles (instead of relying on GRANT/REVOKE), mirroring the pattern used for
user_roles/audit_events; additionally implement rate-limiting to block
high-frequency submissions either by adding a DB-side trigger/function that
checks recent inserts per IP/session/email or by enforcing throttling in the
application layer before calling submitWhitelist, and ensure any new policies
and triggers are documented and tested.

In `@supabase/migrations/202603221215_landing_tracking_tables.sql`:
- Around line 27-31: Enable Row Level Security on the landing_sessions table and
add policies to limit anon access to only their own session: run ALTER TABLE
public.landing_sessions ENABLE ROW LEVEL SECURITY; create a policy (e.g.,
"anon_insert_own_session") that allows anon to INSERT with with check true, and
create a policy (e.g., "anon_update_own_session") that allows anon to UPDATE
only when session_key equals the request header value (use
current_setting('request.headers')::json->>'x-session-key' in the USING clause);
keep existing grants for anon/authenticated but remove broad UPDATE/SELECT
without RLS so anon cannot read/modify arbitrary rows.

---

Outside diff comments:
In `@apps/dashboard/app/login/page.tsx`:
- Around line 8-12: The LoginPage component reads searchParams synchronously but
in Next.js 16.1.6 searchParams is a Promise; make LoginPage async and await
searchParams before extracting message and error so the banners render
correctly. Update the LoginPage function signature to "export default async
function LoginPage({ searchParams }: { searchParams: Promise<{ message?: string;
error?: string }>; })" (or cast appropriately), then do "const params = await
searchParams" and use params.message and params.error where the message/error
banners are rendered (references: LoginPage, searchParams, message, error).

---

Nitpick comments:
In `@apps/dashboard/app/actions.ts`:
- Around line 23-26: Remove or guard the debug console.logs that expose session
and API details (the statements console.log('Session error:', error),
console.log('Has session:', !!session), console.log('Has token:',
!!session?.access_token), console.log('API URL:', process.env.API_URL')); either
delete them for production or wrap them behind a debug flag (e.g., if
(process.env.NODE_ENV !== 'production') { ... } or process.env.DEBUG) and/or
replace with a proper logger call at debug level (e.g.,
processLogger.debug(...)) so sensitive info is not logged in production.

In `@apps/dashboard/app/conversations/`[id]/page.tsx:
- Around line 17-18: Remove the stray console.log debug statements from the
Conversations page (remove the console.log calls that print API_URL and
"Fetching conversation timeline..." and the other console.log occurrences in
this file), or replace them with a proper logger that is disabled in production
(e.g., use a debug/logger utility or guard with NODE_ENV !== 'production');
update the default export component (the page component) to no longer emit these
console logs so no sensitive/debug output appears in production.

In `@apps/dashboard/app/escalations/page.tsx`:
- Around line 20-21: Remove the two development console.log statements in
apps/dashboard/app/escalations/page.tsx (the lines logging 'API URL:' and
'Fetching escalations...'); either delete them entirely or replace them with a
conditional debug logger that only runs when a DEBUG/VERBOSE env flag is enabled
or when NODE_ENV !== 'production', ensuring no debug output reaches production.

In `@apps/dashboard/app/use-cases/`[slug]/page.tsx:
- Around line 137-145: The JSX uses paragraph text as React keys in the mappings
for article.intro and section.paragraphs causing duplicate-key issues; update
the map callbacks (the article.intro.map and section.paragraphs?.map in the
component rendering the "use-case-section") to use stable keys instead — e.g.,
use the array index or a combination like `${index}-${paragraph}` as the key (or
a unique id if available) to ensure keys are unique and prevent React
warnings/bugs.
- Around line 43-51: The return expression in getNextArticle contains an
unreachable fallback (the "?? USE_CASE_ARTICLES[0]!" part) because you already
throw on an empty array and nextIndex is always a valid index; remove the
nullish coalescing and non-null assertion and simply return
USE_CASE_ARTICLES[nextIndex] from getNextArticle to simplify the function.
- Around line 183-188: Replace the plain <img> (the element with src
"https://cdn.simpleicons.org/whatsapp/ffffff" and alt "WhatsApp" inside the Book
a Demo markup in page.tsx) with Next.js' Image component: import Image from
'next/image' and render it with explicit width/height (or responsive layout
props) so Next can optimize and lazy-load the asset; also add the
cdn.simpleicons.org entry to next.config.js under images.remotePatterns
(protocol https, hostname cdn.simpleicons.org) so external loading is allowed.
Ensure you remove the inline style on the <img> and use Image sizing props or a
wrapper for styling.
- Around line 70-78: The page is directly injecting <link> tags for Inter and
CSS which bypasses Next.js optimizations; remove those tags from the page
component (the default exported Page in page.tsx) and instead load Inter via
next/font/google (e.g., import Inter from 'next/font/google' and use the
returned font class at the root) and import Bootstrap and other global CSS in
the root layout (layout.tsx) or a global stylesheet (e.g., import
'/landing/vendors/bootstrap/bootstrap.min.css' and '/landing/css/style.css' from
your root layout or global CSS file) so fonts are optimized and styles are
applied app-wide.

In `@apps/dashboard/components/landing/early-access-modal-portal.tsx`:
- Around line 6-8: Wrap the portal mount in a React effect that dispatches a
custom DOM event when the modal root is mounted so landing.js can explicitly
wait/retry; specifically, in early-access-modal-portal.tsx use useEffect inside
the component that creates the portal (where createPortal is returned and the
document guard exists) to dispatch an 'early-access-modal-mounted' event on
window/document after the modal DOM node is appended, and ensure landing.js
listens for that 'early-access-modal-mounted' event and triggers its
initialization or retry logic when received.

In `@apps/dashboard/lib/landing/use-case-articles.ts`:
- Around line 92-97: The testimonial entries in use-case-articles.ts contain
placeholder text (e.g., attribution values like "[Clinic name, City]" and
percentage tokens like "[X]%") inside the testimonial object (fields title,
quote, attribution); replace each placeholder with real testimonial content or
remove the testimonial object entirely for any article that lacks verified copy.
Locate each article's testimonial object in the file (instances around the shown
snippet and at the other reported ranges) and update the attribution and numeric
placeholders to actual clinic/dr/ practice names and real metrics, or delete the
testimonial field if you don't have verified content yet.

In `@apps/dashboard/public/landing/css/style.css`:
- Around line 21-31: There are duplicate .service-card .icon rules where the
later block overrides width/height/background/margin set earlier; consolidate
them into a single rule (or keep the intended override only) by merging the
properties from both definitions into one authoritative .service-card .icon
selector (or move the intended overriding declarations to the final occurrence)
so the 64px width/height, centering, background-color and margin-bottom are
defined once and not unintentionally overridden.

In `@apps/dashboard/public/landing/js/landing.js`:
- Around line 74-75: In the onScroll handler, replace the deprecated usage of
window.pageYOffset when computing scrollTop with window.scrollY for consistency
with other usages in this file (see onScroll and the scrollTop variable); update
the expression inside the onScroll function that sets scrollTop to use
window.scrollY || document.documentElement.scrollTop so behavior remains robust.
- Around line 377-402: The scroll handler in initScrollDepthTracking runs on
every scroll event which can hurt performance; modify onScroll (and its use of
computeScrollPercent, thresholds, and firedThresholds) to be throttled—e.g.,
wrap the work in a requestAnimationFrame or a small time-based throttle so
computeScrollPercent and the thresholds.forEach logic only run at most once per
animation frame or every N ms, and replace window.addEventListener('scroll',
onScroll) with the throttled version while keeping trackEvent calls unchanged.

In `@supabase/migrations/202603221215_landing_tracking_tables.sql`:
- Around line 15-22: The whitelist_signups table's email column currently allows
NULLs and duplicates; update the table definition for public.whitelist_signups
to make the email column NOT NULL and add a uniqueness constraint or unique
index on email (e.g., UNIQUE CONSTRAINT on column email or a UNIQUE INDEX like
whitelist_signups_email_idx) so duplicate signups are prevented; ensure you
reference the table name whitelist_signups and column email when applying the
change so the migration enforces both NOT NULL and uniqueness.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 12d3e4b7-642f-415e-a3bf-daa274b86222

📥 Commits

Reviewing files that changed from the base of the PR and between 0272a12 and 8ce7071.

⛔ Files ignored due to path filters (26)
  • apps/dashboard/public/landing/images/about_2-min-1.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/about_2-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/arch-line-reverse.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/arch-line.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/card-expenses-1.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/card-expenses-1.png is excluded by !**/*.png
  • apps/dashboard/public/landing/images/card-expenses.png is excluded by !**/*.png
  • apps/dashboard/public/landing/images/hero-img-1-min-1.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/hero-img-1-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/logo-dark-1.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/logo-dark.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/logo-light-1.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/logo-light.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/logo/actual-size/logo-air-bnb__black.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/logo/actual-size/logo-google__black.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/logo/actual-size/logo-ibm__black.svg is excluded by !**/*.svg
  • apps/dashboard/public/landing/images/person-sq-1-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/person-sq-2-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/person-sq-3-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/person-sq-5-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/person-sq-7-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/images/person-sq-8-min.jpg is excluded by !**/*.jpg
  • apps/dashboard/public/landing/vendors/bootstrap-icons/font/fonts/bootstrap-icons.woff is excluded by !**/*.woff
  • apps/dashboard/public/landing/vendors/bootstrap-icons/font/fonts/bootstrap-icons.woff2 is excluded by !**/*.woff2
  • apps/dashboard/public/landing/vendors/bootstrap/bootstrap.bundle.min.js is excluded by !**/*.min.js
  • apps/dashboard/public/landing/vendors/glightbox/glightbox.min.js is excluded by !**/*.min.js
📒 Files selected for processing (29)
  • apps/dashboard/app/actions.ts
  • apps/dashboard/app/api/whitelist-signup/route.ts
  • apps/dashboard/app/conversations/[id]/page.tsx
  • apps/dashboard/app/conversations/page.tsx
  • apps/dashboard/app/dashboard/page.tsx
  • apps/dashboard/app/escalations/page.tsx
  • apps/dashboard/app/layout.tsx
  • apps/dashboard/app/login/client.tsx
  • apps/dashboard/app/login/layout.tsx
  • apps/dashboard/app/login/page.tsx
  • apps/dashboard/app/page.tsx
  • apps/dashboard/app/use-cases/[slug]/page.tsx
  • apps/dashboard/app/use-cases/[slug]/use-case.css
  • apps/dashboard/components/dashboard-shell.tsx
  • apps/dashboard/components/landing/early-access-modal-portal.tsx
  • apps/dashboard/eslint.config.mjs
  • apps/dashboard/lib/landing/landing-body.html
  • apps/dashboard/lib/landing/use-case-articles.ts
  • apps/dashboard/middleware.ts
  • apps/dashboard/public/landing/css/modal-override.css
  • apps/dashboard/public/landing/css/style.css
  • apps/dashboard/public/landing/js/landing.js
  • apps/dashboard/public/landing/vendors/aos/aos.css
  • apps/dashboard/public/landing/vendors/aos/aos.js
  • apps/dashboard/public/landing/vendors/bootstrap-icons/font/bootstrap-icons.min.css
  • apps/dashboard/public/landing/vendors/bootstrap/bootstrap.min.css
  • apps/dashboard/public/landing/vendors/glightbox/glightbox.min.css
  • apps/dashboard/public/landing/vendors/purecounter/purecounter.js
  • supabase/migrations/202603221215_landing_tracking_tables.sql

Comment on lines +6 to +15
const body = await request.json();
// expects: { name, email, clinic_type }
const { name, email, clinic_type } = body;
if (!name || !email || !clinic_type) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
}
const { error } = await supabase.from('whitelist_signups').insert([{ name, email, clinic_type }]);
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check if the file exists and read its content
cat -n apps/dashboard/app/api/whitelist-signup/route.ts

Repository: Ryanakml/ChatFuse

Length of output: 863


🏁 Script executed:

# Check if there's any middleware handling errors in the app directory
find apps/dashboard/app -type f -name "middleware.ts" -o -name "error.ts" | head -20

Repository: Ryanakml/ChatFuse

Length of output: 43


🏁 Script executed:

# Look for other API routes to see how they handle request.json() and errors
find apps/dashboard/app/api -type f -name "route.ts" | head -10 | xargs -I {} sh -c 'echo "=== {} ===" && head -30 {}'

Repository: Ryanakml/ChatFuse

Length of output: 801


🏁 Script executed:

# Check other API route implementations to see patterns
find apps/dashboard/app/api -type f -name "route.ts" -exec wc -l {} + | head -20

Repository: Ryanakml/ChatFuse

Length of output: 112


🏁 Script executed:

# Look at a few more route files to check error handling patterns
find apps/dashboard/app/api -type f -name "route.ts" | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Repository: Ryanakml/ChatFuse

Length of output: 801


🏁 Script executed:

# Check if there's a global error handler (common patterns in Next.js)
find apps/dashboard -name "error.ts" -o -name "layout.ts" | xargs grep -l "error" 2>/dev/null | head -10

Repository: Ryanakml/ChatFuse

Length of output: 43


Handle invalid JSON and prevent leaking raw DB errors.

The code lacks error handling for request.json() parsing failures, which will bubble up as 500 errors. Additionally, returning error.message directly exposes internal database details to clients.

Wrap request.json() in a try-catch to return a 400 error for malformed payloads, and sanitize the database error response to a generic message while logging the actual error server-side.

Proposed fix
 export async function POST(request: Request) {
   const supabase = await createClient();
-  const body = await request.json();
+  let body: unknown;
+  try {
+    body = await request.json();
+  } catch {
+    return NextResponse.json({ error: 'Invalid JSON payload' }, { status: 400 });
+  }
   // expects: { name, email, clinic_type }
-  const { name, email, clinic_type } = body;
+  const { name, email, clinic_type } = body as {
+    name?: string;
+    email?: string;
+    clinic_type?: string;
+  };
   if (!name || !email || !clinic_type) {
     return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
   }
   const { error } = await supabase.from('whitelist_signups').insert([{ name, email, clinic_type }]);
   if (error) {
-    return NextResponse.json({ error: error.message }, { status: 500 });
+    console.error('whitelist_signups insert failed', { code: error.code });
+    return NextResponse.json({ error: 'Unable to submit signup' }, { status: 500 });
   }
   return NextResponse.json({ success: true });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/api/whitelist-signup/route.ts` around lines 6 - 15, Wrap
the call to request.json() in a try-catch inside the route handler to catch JSON
parse errors and return NextResponse.json({ error: 'Invalid JSON payload' }, {
status: 400 }); when parsing fails; after insertion with
supabase.from('whitelist_signups').insert([{ name, email, clinic_type }]), do
not return error.message to the client — instead log the full error server-side
(use your logger or console.error) and return a generic NextResponse.json({
error: 'Unable to create signup' }, { status: 500 }); also keep the existing
validation for name/email/clinic_type and ensure the catch covers both parse and
unexpected exceptions to return a 500 generic error response.

Comment on lines +16 to +17
const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't silently bootstrap Supabase with blank credentials.

The landing flow depends on these values for tracking and signup. Falling back to '' turns a missing env into a client-side failure after deploy instead of a startup/build-time error.

Suggested fix
-  const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
-  const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
+  const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
+  const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
+
+  if (!publicSupabaseUrl || !publicSupabaseAnonKey) {
+    throw new Error(
+      'Landing page requires NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY',
+    );
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || '';
const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '';
const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
if (!publicSupabaseUrl || !publicSupabaseAnonKey) {
throw new Error(
'Landing page requires NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY',
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/page.tsx` around lines 16 - 17, The code currently falls
back to empty strings for publicSupabaseUrl and publicSupabaseAnonKey which
hides missing envs and causes client-side failures; instead remove the silent
defaults and add a startup-time validation in page.tsx that checks
process.env.NEXT_PUBLIC_SUPABASE_URL and
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY and throws a clear Error if either is
missing (referencing the publicSupabaseUrl and publicSupabaseAnonKey variables)
so the build/deploy fails fast with an actionable message.

Comment on lines +46 to +49
<Script
src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"
strategy="afterInteractive"
/>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's confirm the file exists and check the relevant lines
cat -n apps/dashboard/app/page.tsx | sed -n '40,55p'

Repository: Ryanakml/ChatFuse

Length of output: 975


🏁 Script executed:

# Check if package.json exists and what version of `@supabase/supabase-js` is declared
find . -name "package.json" -type f | head -5

Repository: Ryanakml/ChatFuse

Length of output: 187


🏁 Script executed:

# Look for the package.json in the dashboard app
cat -n apps/dashboard/package.json | grep -A 5 -B 5 "supabase"

Repository: Ryanakml/ChatFuse

Length of output: 467


🏁 Script executed:

# Also check the root package.json
cat -n package.json | grep -A 5 -B 5 "supabase"

Repository: Ryanakml/ChatFuse

Length of output: 43


Pin the Supabase browser client to match declared dependency version.

The Script tag loads @supabase/supabase-js@2 (floating version) from CDN, but apps/dashboard/package.json declares ^2.48.1. This creates version drift: the browser may load a newer 2.x.x version than what the server-side code uses, causing potential incompatibilities or unexpected behavior.

Fix
-      <Script
-        src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"
-        strategy="afterInteractive"
-      />
+      <Script
+        src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.48.1"
+        strategy="afterInteractive"
+      />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/app/page.tsx` around lines 46 - 49, Change the floating CDN
import in the Script component to the exact Supabase browser client version that
matches package.json to avoid version drift: update the src on the Script JSX
element (the Script component that currently points to
"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2") to the pinned release
used by the app (for example
"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2.48.1") so the browser
client version matches the server-side dependency.

Comment on lines +43 to +55
<button
class="navbar-toggler"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#fbs__net-navbars"
>
<span class="navbar-toggler-icon"></span>
</button>

<div class="offcanvas offcanvas-start w-75" id="fbs__net-navbars" tabindex="-1">
<div class="offcanvas-header">
<h5 class="offcanvas-title">Menu</h5>
<button class="btn-close" type="button" data-bs-dismiss="offcanvas"></button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep a single offcanvas trigger on mobile.

Bootstrap already shows .navbar-toggler below lg, and the custom trigger at Lines 75-114 is enabled on the same breakpoint, so small screens get two buttons for the same menu. The close control here is also unlabeled.

Suggested fix
-      <button
-        class="navbar-toggler"
-        type="button"
-        data-bs-toggle="offcanvas"
-        data-bs-target="#fbs__net-navbars"
-      >
-        <span class="navbar-toggler-icon"></span>
-      </button>
@@
-          <button class="btn-close" type="button" data-bs-dismiss="offcanvas"></button>
+          <button
+            class="btn-close"
+            type="button"
+            data-bs-dismiss="offcanvas"
+            aria-label="Close menu"
+          ></button>

Also applies to: 75-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/lib/landing/landing-body.html` around lines 43 - 55, There are
two visible offcanvas triggers on small screens; ensure only one is shown by
keeping the existing <button class="navbar-toggler"> as the mobile trigger and
hiding the custom trigger (the other offcanvas toggle in the 75-114 block) on
<lg viewports (e.g., add d-none d-lg-block to the custom trigger or add
d-lg-none to the navbar-toggler depending on desired behavior) so only one
control toggles the offcanvas with id="fbs__net-navbars"; also add an accessible
label to the close button (the <button class="btn-close">) by adding an
aria-label="Close" attribute.

Comment on lines +43 to +48
const isPublicPath =
pathname === '/' ||
pathname.startsWith('/login') ||
pathname.startsWith('/api') ||
pathname.startsWith('/use-cases') ||
pathname === '/auth/callback';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/dashboard/middleware.ts

Repository: Ryanakml/ChatFuse

Length of output: 3039


🏁 Script executed:

find apps/dashboard -type f \( -name "page.tsx" -o -name "route.ts" -o -name "route.js" \) | head -30

Repository: Ryanakml/ChatFuse

Length of output: 383


🏁 Script executed:

find apps/dashboard/app -type f \( -name "page.tsx" -o -name "route.ts" -o -name "route.js" \) | sort

Repository: Ryanakml/ChatFuse

Length of output: 383


🏁 Script executed:

tree -L 4 apps/dashboard/app --filesfirst 2>/dev/null || find apps/dashboard/app -type d | head -30

Repository: Ryanakml/ChatFuse

Length of output: 607


🏁 Script executed:

cat -n apps/dashboard/app/login/layout.tsx

Repository: Ryanakml/ChatFuse

Length of output: 298


🏁 Script executed:

find apps/dashboard/app/login -type f

Repository: Ryanakml/ChatFuse

Length of output: 166


🏁 Script executed:

find apps/dashboard/app/use-cases -type f

Repository: Ryanakml/ChatFuse

Length of output: 154


🏁 Script executed:

find apps/dashboard/app/api -type f -name "route.*"

Repository: Ryanakml/ChatFuse

Length of output: 109


Tighten public route matching to prevent accidental auth bypass on future routes.

startsWith('/login') and startsWith('/use-cases') match any path prefixed with these strings. If future routes like /use-cases-private or /login-admin are added as protected routes, they would incorrectly be treated as public. Use exact path and slash-delimited checks instead to maintain security boundaries.

Proposed fix
-    const isPublicPath =
-      pathname === '/' ||
-      pathname.startsWith('/login') ||
-      pathname.startsWith('/api') ||
-      pathname.startsWith('/use-cases') ||
-      pathname === '/auth/callback';
+    const isPublicPath =
+      pathname === '/' ||
+      pathname === '/login' ||
+      pathname.startsWith('/login/') ||
+      pathname === '/use-cases' ||
+      pathname.startsWith('/use-cases/') ||
+      pathname === '/auth/callback' ||
+      pathname === '/api/whitelist-signup';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isPublicPath =
pathname === '/' ||
pathname.startsWith('/login') ||
pathname.startsWith('/api') ||
pathname.startsWith('/use-cases') ||
pathname === '/auth/callback';
const isPublicPath =
pathname === '/' ||
pathname === '/login' ||
pathname.startsWith('/login/') ||
pathname === '/use-cases' ||
pathname.startsWith('/use-cases/') ||
pathname === '/auth/callback' ||
pathname === '/api/whitelist-signup';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/middleware.ts` around lines 43 - 48, The isPublicPath check in
middleware currently uses broad startsWith checks that can accidentally match
protected routes (e.g., "/login-admin"); update the boolean expression in the
isPublicPath computation to use exact matches or slash-delimited startsWith
checks: replace pathname.startsWith('/login') with (pathname === '/login' ||
pathname.startsWith('/login/')), replace pathname.startsWith('/use-cases') with
(pathname === '/use-cases' || pathname.startsWith('/use-cases/')), and similarly
ensure '/api' is matched as (pathname === '/api' ||
pathname.startsWith('/api/')), keeping pathname === '/' and pathname ===
'/auth/callback' as exact checks so only intended public routes are allowed.

Comment on lines +199 to +238
function initInlineSvg() {
var imgElements = document.querySelectorAll('.js-img-to-inline-svg');

imgElements.forEach(function (imgElement) {
var imgURL = imgElement.getAttribute('src');
if (!imgURL) {
return;
}

fetch(imgURL)
.then(function (response) {
return response.text();
})
.then(function (svgText) {
var parser = new DOMParser();
var svgDocument = parser.parseFromString(svgText, 'image/svg+xml');
var svgElement = svgDocument.documentElement;

Array.from(imgElement.attributes).forEach(function (attr) {
if (attr.name !== 'class') {
svgElement.setAttribute(attr.name, attr.value);
return;
}

var classes = attr.value.split(' ').filter(function (className) {
return className !== 'js-img-to-inline-svg';
});

if (classes.length > 0) {
svgElement.setAttribute('class', classes.join(' '));
}
});

imgElement.replaceWith(svgElement);
})
.catch(function (error) {
console.error('Error fetching SVG:', error);
});
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

SVG inlining trusts external content.

initInlineSvg fetches SVGs and injects them directly into the DOM. If any .js-img-to-inline-svg element points to an untrusted source, this could enable XSS via malicious SVG content (embedded scripts, event handlers).

Ensure all SVG sources are from trusted origins (same-origin or known CDNs). Consider sanitizing the SVG before insertion if sources may vary.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/public/landing/js/landing.js` around lines 199 - 238, The
initInlineSvg function currently fetches and injects external SVGs directly,
risking XSS; update initInlineSvg to only fetch SVGs from trusted origins (check
imgElement.src using new URL(imgURL, location.href) and allow same-origin or a
configured whitelist), and after parsing svgText into svgElement remove
dangerous content before replaceWith (strip <script> elements, remove attributes
starting with "on" and any javascript: URLs, and/or run the parsed SVG through a
sanitizer like DOMPurify) so only safe SVG markup is inserted; keep the existing
fetch flow but bail out and log a warning for disallowed origins or failed
sanitization.

Comment on lines +290 to +302
var generatedSessionKey = btoa(navigator.userAgent + new Date().toDateString()).slice(0, 32);
var sessionKey = generatedSessionKey;

try {
var storedSessionKey = localStorage.getItem(sessionStorageKey);
if (storedSessionKey && storedSessionKey === generatedSessionKey) {
sessionKey = storedSessionKey;
} else {
localStorage.setItem(sessionStorageKey, generatedSessionKey);
}
} catch {
sessionKey = generatedSessionKey;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Session key generation produces collisions across users.

The session key is generated as btoa(navigator.userAgent + new Date().toDateString()).slice(0, 32). This means all users with the same browser/OS visiting on the same calendar day will share the same session key, causing their tracking data to be merged and overwritten via the upsert.

🔧 Generate a unique session key per visitor
-    var generatedSessionKey = btoa(navigator.userAgent + new Date().toDateString()).slice(0, 32);
-    var sessionKey = generatedSessionKey;
+    function generateUniqueKey() {
+      var array = new Uint8Array(16);
+      crypto.getRandomValues(array);
+      return Array.from(array, function(b) {
+        return b.toString(16).padStart(2, '0');
+      }).join('');
+    }
+
+    var sessionKey;
 
     try {
       var storedSessionKey = localStorage.getItem(sessionStorageKey);
-      if (storedSessionKey && storedSessionKey === generatedSessionKey) {
-        sessionKey = storedSessionKey;
+      if (storedSessionKey) {
+        sessionKey = storedSessionKey;
       } else {
+        sessionKey = generateUniqueKey();
         localStorage.setItem(sessionStorageKey, generatedSessionKey);
       }
     } catch {
-      sessionKey = generatedSessionKey;
+      sessionKey = generateUniqueKey();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/public/landing/js/landing.js` around lines 290 - 302, The
current generatedSessionKey (btoa(navigator.userAgent + new
Date().toDateString()).slice(0, 32)) causes collisions; replace it with a
cryptographically strong unique ID per visitor (e.g., use crypto.getRandomValues
or a UUID generator) when creating generatedSessionKey, then persist that value
to localStorage under sessionStorageKey (same logic around reading/storing in
localStorage and the try/catch) so sessionKey remains unique per visitor and
stable across page loads; update any references to
generatedSessionKey/sessionKey to use the new unique ID generation.

Comment on lines +356 to +375
function submitWhitelist(payload) {
try {
void supabaseClient
.from('whitelist_signups')
.insert({
session_key: sessionKey,
name: payload.name || null,
email: payload.email || null,
clinic_type: payload.clinicType || null,
})
.then(function () {
// non-blocking
})
.catch(function () {
// non-blocking
});
} catch {
// non-blocking
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for RLS policies on whitelist_signups table
fd -e sql . supabase/migrations --exec grep -l -i 'whitelist_signups' {} \; | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Repository: Ryanakml/ChatFuse

Length of output: 1314


🏁 Script executed:

# Search for RLS policy definitions on whitelist_signups
rg -i 'create.*policy|alter table.*whitelist_signups|enable row level security' supabase/

Repository: Ryanakml/ChatFuse

Length of output: 1028


🏁 Script executed:

# Search for triggers or functions that might implement rate limiting
fd -e sql . supabase/ --exec grep -l -i 'trigger\|function\|rate' {} \;

Repository: Ryanakml/ChatFuse

Length of output: 235


🏁 Script executed:

# List all migration files to see full migration history
git ls-files 'supabase/migrations' | head -20

Repository: Ryanakml/ChatFuse

Length of output: 521


🏁 Script executed:

# Get the exact content of whitelist_signups migration again to confirm RLS status
cat supabase/migrations/202603221215_landing_tracking_tables.sql | grep -i 'whitelist_signups\|row level security'

Repository: Ryanakml/ChatFuse

Length of output: 469


🏁 Script executed:

# Check if whitelist_signups is mentioned anywhere else with RLS or policy
rg -i 'whitelist_signups.*policy|policy.*whitelist_signups' supabase/

Repository: Ryanakml/ChatFuse

Length of output: 43


🏁 Script executed:

# Check the full landing.js file context for any client-side rate limiting
head -n 400 apps/dashboard/public/landing/js/landing.js | tail -n 50

Repository: Ryanakml/ChatFuse

Length of output: 1455


Enable Row Level Security (RLS) on whitelist_signups table.

The table currently relies only on table-level GRANT/REVOKE permissions. While GRANT insert allows anonymous inserts and REVOKE delete, update, select prevents unauthorized access, this approach lacks the granularity and explicit security policies used elsewhere in the codebase (e.g., user_roles, audit_events tables).

Implement RLS policies to:

  1. Explicitly allow anonymous inserts
  2. Prevent reading/updating/deleting records (currently revoked at table level, but should be enforced via policy)
  3. Add rate-limiting via trigger or application logic to prevent abuse from high-volume spam submissions
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/public/landing/js/landing.js` around lines 356 - 375, Enable
RLS on the whitelist_signups table and add explicit policies: create a policy
that allows anonymous INSERTs (so the client function submitWhitelist can
continue to insert) and separate restrictive policies that DENY or explicitly
prevent SELECT, UPDATE, DELETE for anon/other roles (instead of relying on
GRANT/REVOKE), mirroring the pattern used for user_roles/audit_events;
additionally implement rate-limiting to block high-frequency submissions either
by adding a DB-side trigger/function that checks recent inserts per
IP/session/email or by enforcing throttling in the application layer before
calling submitWhitelist, and ensure any new policies and triggers are documented
and tested.

Comment on lines +27 to +31
grant select, insert, update on table public.landing_sessions to anon, authenticated;
grant insert on table public.whitelist_signups to anon, authenticated;

revoke delete on table public.landing_sessions from anon, authenticated;
revoke delete, update, select on table public.whitelist_signups from anon, authenticated;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "(migration|202603221215)" | head -20

Repository: Ryanakml/ChatFuse

Length of output: 590


🏁 Script executed:

cat -n supabase/migrations/202603221215_landing_tracking_tables.sql

Repository: Ryanakml/ChatFuse

Length of output: 1476


🏁 Script executed:

rg "landing_sessions|whitelist_signups" supabase/migrations/ -A 5 -B 5

Repository: Ryanakml/ChatFuse

Length of output: 3258


Consider enabling Row Level Security (RLS) on landing_sessions.

Granting SELECT and UPDATE to anon without RLS means any anonymous user can read or modify any session record. Since session_key is the only identifier and is not enforced at the database level during updates, anyone who knows or guesses a session key could tamper with other users' tracking data, even if the key is meant to be unpredictable.

Add RLS policies to restrict access to only the current session (e.g., by matching a session key passed via RPC or headers).

🛡️ Example RLS policy
alter table public.landing_sessions enable row level security;

-- Allow insert for any anon user (they create their own session)
create policy "anon_insert_own_session" on public.landing_sessions
  for insert to anon
  with check (true);

-- Allow update only if session_key matches (client must prove ownership)
create policy "anon_update_own_session" on public.landing_sessions
  for update to anon
  using (session_key = current_setting('request.headers')::json->>'x-session-key');

If RLS is intentionally omitted for simplicity in this MVP, confirm this is acceptable for your threat model and document the assumption that session_key remains secret.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/migrations/202603221215_landing_tracking_tables.sql` around lines 27
- 31, Enable Row Level Security on the landing_sessions table and add policies
to limit anon access to only their own session: run ALTER TABLE
public.landing_sessions ENABLE ROW LEVEL SECURITY; create a policy (e.g.,
"anon_insert_own_session") that allows anon to INSERT with with check true, and
create a policy (e.g., "anon_update_own_session") that allows anon to UPDATE
only when session_key equals the request header value (use
current_setting('request.headers')::json->>'x-session-key' in the USING clause);
keep existing grants for anon/authenticated but remove broad UPDATE/SELECT
without RLS so anon cannot read/modify arbitrary rows.

@Ryanakml Ryanakml merged commit 166ee15 into main Mar 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant