feat(landing): intitialize landing page v1 design#17
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟠 MajorAwait
searchParamsbefore reading query messages.In Next.js 16.1.6,
searchParamsis 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 addingNOT NULLand unique constraints onThe
- Require an email (
NOT NULL)- 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.jsexecutes (due tostrategy="afterInteractive"). While this should work correctly, usinguseEffectto 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.jsto listen forearly-access-modal-mountedand 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 .iconrules.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.pageYOffsetis deprecated.Use
window.scrollYfor 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.
initScrollDepthTrackingattaches a scroll listener that fires on every scroll event. While the threshold checks prevent redundanttrackEventcalls, 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
paragraphcontent askeyworks 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 ingetNextArticle.Line 50's
?? USE_CASE_ARTICLES[0]!is unreachable. After the empty-array check (line 44-46), modulo arithmetic always produces a valid index. IfcurrentIndexis -1 (slug not found),nextIndexbecomes 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: Usenext/imagefor 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 innext.config.jsand use theImagecomponent.🔧 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/fontfor automatic optimization, and global stylesheets should be imported inlayout.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/googleand importing Bootstrap CSS in the root layout or via@importin 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 "© 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
"© {{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
⛔ Files ignored due to path filters (26)
apps/dashboard/public/landing/images/about_2-min-1.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/about_2-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/arch-line-reverse.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/arch-line.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/card-expenses-1.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/card-expenses-1.pngis excluded by!**/*.pngapps/dashboard/public/landing/images/card-expenses.pngis excluded by!**/*.pngapps/dashboard/public/landing/images/hero-img-1-min-1.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/hero-img-1-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/logo-dark-1.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/logo-dark.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/logo-light-1.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/logo-light.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/logo/actual-size/logo-air-bnb__black.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/logo/actual-size/logo-google__black.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/logo/actual-size/logo-ibm__black.svgis excluded by!**/*.svgapps/dashboard/public/landing/images/person-sq-1-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/person-sq-2-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/person-sq-3-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/person-sq-5-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/person-sq-7-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/images/person-sq-8-min.jpgis excluded by!**/*.jpgapps/dashboard/public/landing/vendors/bootstrap-icons/font/fonts/bootstrap-icons.woffis excluded by!**/*.woffapps/dashboard/public/landing/vendors/bootstrap-icons/font/fonts/bootstrap-icons.woff2is excluded by!**/*.woff2apps/dashboard/public/landing/vendors/bootstrap/bootstrap.bundle.min.jsis excluded by!**/*.min.jsapps/dashboard/public/landing/vendors/glightbox/glightbox.min.jsis excluded by!**/*.min.js
📒 Files selected for processing (29)
apps/dashboard/app/actions.tsapps/dashboard/app/api/whitelist-signup/route.tsapps/dashboard/app/conversations/[id]/page.tsxapps/dashboard/app/conversations/page.tsxapps/dashboard/app/dashboard/page.tsxapps/dashboard/app/escalations/page.tsxapps/dashboard/app/layout.tsxapps/dashboard/app/login/client.tsxapps/dashboard/app/login/layout.tsxapps/dashboard/app/login/page.tsxapps/dashboard/app/page.tsxapps/dashboard/app/use-cases/[slug]/page.tsxapps/dashboard/app/use-cases/[slug]/use-case.cssapps/dashboard/components/dashboard-shell.tsxapps/dashboard/components/landing/early-access-modal-portal.tsxapps/dashboard/eslint.config.mjsapps/dashboard/lib/landing/landing-body.htmlapps/dashboard/lib/landing/use-case-articles.tsapps/dashboard/middleware.tsapps/dashboard/public/landing/css/modal-override.cssapps/dashboard/public/landing/css/style.cssapps/dashboard/public/landing/js/landing.jsapps/dashboard/public/landing/vendors/aos/aos.cssapps/dashboard/public/landing/vendors/aos/aos.jsapps/dashboard/public/landing/vendors/bootstrap-icons/font/bootstrap-icons.min.cssapps/dashboard/public/landing/vendors/bootstrap/bootstrap.min.cssapps/dashboard/public/landing/vendors/glightbox/glightbox.min.cssapps/dashboard/public/landing/vendors/purecounter/purecounter.jssupabase/migrations/202603221215_landing_tracking_tables.sql
| 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 }); | ||
| } |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 -20Repository: 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 -20Repository: 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 -10Repository: 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.
| const publicSupabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || ''; | ||
| const publicSupabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || ''; |
There was a problem hiding this comment.
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.
| 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.
| <Script | ||
| src="https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2" | ||
| strategy="afterInteractive" | ||
| /> |
There was a problem hiding this comment.
🧩 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 -5Repository: 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.
| <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> |
There was a problem hiding this comment.
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.
| const isPublicPath = | ||
| pathname === '/' || | ||
| pathname.startsWith('/login') || | ||
| pathname.startsWith('/api') || | ||
| pathname.startsWith('/use-cases') || | ||
| pathname === '/auth/callback'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/dashboard/middleware.tsRepository: 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 -30Repository: 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" \) | sortRepository: 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 -30Repository: Ryanakml/ChatFuse
Length of output: 607
🏁 Script executed:
cat -n apps/dashboard/app/login/layout.tsxRepository: Ryanakml/ChatFuse
Length of output: 298
🏁 Script executed:
find apps/dashboard/app/login -type fRepository: Ryanakml/ChatFuse
Length of output: 166
🏁 Script executed:
find apps/dashboard/app/use-cases -type fRepository: 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.
| 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.
| 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); | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 50Repository: 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:
- Explicitly allow anonymous inserts
- Prevent reading/updating/deleting records (currently revoked at table level, but should be enforced via policy)
- 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.
| 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; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | grep -E "(migration|202603221215)" | head -20Repository: Ryanakml/ChatFuse
Length of output: 590
🏁 Script executed:
cat -n supabase/migrations/202603221215_landing_tracking_tables.sqlRepository: Ryanakml/ChatFuse
Length of output: 1476
🏁 Script executed:
rg "landing_sessions|whitelist_signups" supabase/migrations/ -A 5 -B 5Repository: 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.
Summary by CodeRabbit
New Features
Bug Fixes
UI/Style