[PULSE-60] Frontend hardening, UX polish, and security #35

Merged
uz1mani merged 41 commits from staging into main 2026-02-22 21:43:06 +00:00
uz1mani commented 2026-02-22 21:32:40 +00:00 (Migrated from github.com)

Work Item

PULSE-60

Summary

  • Replaced all spinners/blank screens with skeleton loading, added error boundaries, page metadata, server-side route protection, security headers, form UX improvements, accessibility, and TypeScript cleanup
  • Removed debug logs from production, optimized bundle size with tree-shaken icon imports, replaced <img> with next/image for favicons, and centralized shared constants
  • Fixed loading flicker, org switch session bug, dark mode chart tooltips, logout redirect loop, duplicate CSP, onboarding form limits, and landing page dashboard preview

Changes

  • components/skeletons.tsx — New shared skeleton component library (DashboardSkeleton, UptimeSkeleton, FunnelsSkeleton, etc.)
  • components/useMinimumLoading.ts — New hook to prevent sub-second loading flicker (300ms minimum display)
  • middleware.ts — New server-side route protection; redirects unauthenticated users to /login, authenticated users away from /login
  • components/ErrorDisplay.tsx — New shared error UI component for route-level error boundaries
  • app/**/error.tsx — Error boundaries added to all route groups (root, sites, uptime, funnels, settings, notifications, org-settings, share)
  • app/**/layout.tsx — Page metadata and Open Graph tags added to all routes; dynamic generateMetadata for /share/[id]
  • next.config.ts — Security headers (X-Frame-Options, HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-XSS-Protection), remotePatterns for favicon images, optimizePackageImports for react-icons
  • app/sites/[id]/page.tsx, realtime/page.tsx, uptime/page.tsx, funnels/page.tsx, settings/page.tsx, notifications/page.tsx — Skeleton loading, useMinimumLoading, dynamic document.title, contextual toast errors
  • app/sites/[id]/uptime/page.tsx — Fixed dark mode chart tooltip (useTheme().theme → resolvedTheme)
  • app/sites/[id]/settings/page.tsx — Autofocus, useUnsavedChanges hook, character counters, maxLength
  • app/sites/new/page.tsx, welcome/page.tsx, funnels/new/page.tsx — Autofocus, maxLength on inputs
  • components/dashboard/Locations.tsx, TechSpecs.tsx, ContentStats.tsx, Campaigns.tsx, TopReferrers.tsx, PerformanceStats.tsx — Skeleton loading, useTabListKeyboard for arrow key nav, ARIA attributes
  • components/notifications/NotificationCenter.tsx — Skeleton rows, ARIA (aria-expanded, aria-haspopup, role="dialog"), Escape key handler, semantic button element
  • components/WorkspaceSwitcher.tsx — ARIA (role="group", aria-current, aria-busy), sessionStorage flag for org switch loading
  • components/settings/OrganizationSettings.tsx — Skeleton loading, catch (error: any) → catch (error: unknown), onChange: any → React.ChangeEvent, removed @ts-ignore, fixed localStorage token bug
  • components/dashboard/ExportModal.tsx — Removed any casts with proper Record types and jspdf-autotable.d.ts
  • components/Footer.tsx — LinkComponent: any → React.ElementType
  • lib/hooks/useUnsavedChanges.ts — New hook for beforeunload warning
  • lib/hooks/useTabListKeyboard.ts — New hook for WAI-ARIA tab list keyboard navigation
  • lib/utils/logger.ts — New dev-only logger that suppresses console.error/warn in production client builds
  • lib/utils/icons.tsx — Centralized FAVICON_SERVICE_URL constant
  • lib/api/client.ts — Removed dead getClient() with localStorage fallback; ApiError.data: any → Record<string, unknown>
  • types/iso-3166-2.d.ts, types/jspdf-autotable.d.ts — New type declarations for untyped libraries
  • app/layout-content.tsx — Org switch loading overlay via sessionStorage flag
  • app/actions/auth.ts, lib/auth/context.tsx, lib/api/organization.ts — Removed console.log statements, migrated console.error to logger
  • app/page.tsx — Real dashboard screenshot preview, removed static mockup
  • app/share/[id]/page.tsx — next/image for favicon, ApiError instanceof checks
  • app/pricing/page.tsx — Static metadata, removed @ts-ignore, catch any → unknown
  • lib/utils/notifications.tsx — aria-hidden on decorative icons
  • docs/DESIGN_SYSTEM.md — Updated error toast example
  • package.json — Version bump to 0.11.0-alpha
  • CHANGELOG.md — Full release notes for 0.11.0-alpha

Test Plan

[ ] Sign out → should land on Pulse homepage, not Ciphera Auth
[ ] Visit /sites/123 while logged out → should redirect to /login
[ ] Visit /login while logged in → should redirect to /
[ ] Navigate between dashboard pages → skeleton loading appears without sub-second flicker
[ ] Trigger a runtime error → error boundary shows "Try again" button
[ ] Share a public dashboard link on social media → OG preview shows site name and favicon
[ ] Edit settings, change a field, navigate away → browser warns about unsaved changes
[ ] Type near character limit on site name → counter appears
[ ] Use keyboard (arrows, Escape, Home/End) on dashboard tabs and notification bell
[ ] Switch organizations → branded loading overlay appears during reload
[ ] Check browser console in production → no console.log or console.error output
[ ] Verify dark mode on uptime chart tooltips → should have dark background
[ ] Landing page shows real dashboard screenshot with bottom fade

## Work Item PULSE-60 ## Summary - Replaced all spinners/blank screens with skeleton loading, added error boundaries, page metadata, server-side route protection, security headers, form UX improvements, accessibility, and TypeScript cleanup - Removed debug logs from production, optimized bundle size with tree-shaken icon imports, replaced `<img>` with `next/image` for favicons, and centralized shared constants - Fixed loading flicker, org switch session bug, dark mode chart tooltips, logout redirect loop, duplicate CSP, onboarding form limits, and landing page dashboard preview ## Changes - `components/skeletons.tsx` — New shared skeleton component library (DashboardSkeleton, UptimeSkeleton, FunnelsSkeleton, etc.) - `components/useMinimumLoading.ts` — New hook to prevent sub-second loading flicker (300ms minimum display) - `middleware.ts` — New server-side route protection; redirects unauthenticated users to /login, authenticated users away from /login - `components/ErrorDisplay.tsx` — New shared error UI component for route-level error boundaries - `app/**/error.tsx` — Error boundaries added to all route groups (root, sites, uptime, funnels, settings, notifications, org-settings, share) - `app/**/layout.tsx` — Page metadata and Open Graph tags added to all routes; dynamic generateMetadata for /share/[id] - `next.config.ts` — Security headers (X-Frame-Options, HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, X-XSS-Protection), remotePatterns for favicon images, optimizePackageImports for react-icons - `app/sites/[id]/page.tsx`, `realtime/page.tsx`, `uptime/page.tsx`, `funnels/page.tsx`, `settings/page.tsx`, `notifications/page.tsx` — Skeleton loading, useMinimumLoading, dynamic document.title, contextual toast errors - `app/sites/[id]/uptime/page.tsx` — Fixed dark mode chart tooltip (useTheme().theme → resolvedTheme) - `app/sites/[id]/settings/page.tsx` — Autofocus, useUnsavedChanges hook, character counters, maxLength - `app/sites/new/page.tsx`, `welcome/page.tsx`, `funnels/new/page.tsx` — Autofocus, maxLength on inputs - `components/dashboard/Locations.tsx`, `TechSpecs.tsx`, `ContentStats.tsx`, `Campaigns.tsx`, `TopReferrers.tsx`, `PerformanceStats.tsx` — Skeleton loading, useTabListKeyboard for arrow key nav, ARIA attributes - `components/notifications/NotificationCenter.tsx` — Skeleton rows, ARIA (aria-expanded, aria-haspopup, role="dialog"), Escape key handler, semantic button element - `components/WorkspaceSwitcher.tsx` — ARIA (role="group", aria-current, aria-busy), sessionStorage flag for org switch loading - `components/settings/OrganizationSettings.tsx` — Skeleton loading, catch (error: any) → catch (error: unknown), onChange: any → React.ChangeEvent, removed @ts-ignore, fixed localStorage token bug - `components/dashboard/ExportModal.tsx` — Removed any casts with proper Record types and jspdf-autotable.d.ts - `components/Footer.tsx` — LinkComponent: any → React.ElementType - `lib/hooks/useUnsavedChanges.ts` — New hook for beforeunload warning - `lib/hooks/useTabListKeyboard.ts` — New hook for WAI-ARIA tab list keyboard navigation - `lib/utils/logger.ts` — New dev-only logger that suppresses console.error/warn in production client builds - `lib/utils/icons.tsx` — Centralized FAVICON_SERVICE_URL constant - `lib/api/client.ts` — Removed dead getClient() with localStorage fallback; ApiError.data: any → Record<string, unknown> - `types/iso-3166-2.d.ts`, `types/jspdf-autotable.d.ts` — New type declarations for untyped libraries - `app/layout-content.tsx` — Org switch loading overlay via sessionStorage flag - `app/actions/auth.ts`, `lib/auth/context.tsx`, `lib/api/organization.ts` — Removed console.log statements, migrated console.error to logger - `app/page.tsx` — Real dashboard screenshot preview, removed static mockup - `app/share/[id]/page.tsx` — next/image for favicon, ApiError instanceof checks - `app/pricing/page.tsx` — Static metadata, removed @ts-ignore, catch any → unknown - `lib/utils/notifications.tsx` — aria-hidden on decorative icons - `docs/DESIGN_SYSTEM.md` — Updated error toast example - `package.json` — Version bump to 0.11.0-alpha - `CHANGELOG.md` — Full release notes for 0.11.0-alpha ## Test Plan [ ] Sign out → should land on Pulse homepage, not Ciphera Auth [ ] Visit /sites/123 while logged out → should redirect to /login [ ] Visit /login while logged in → should redirect to / [ ] Navigate between dashboard pages → skeleton loading appears without sub-second flicker [ ] Trigger a runtime error → error boundary shows "Try again" button [ ] Share a public dashboard link on social media → OG preview shows site name and favicon [ ] Edit settings, change a field, navigate away → browser warns about unsaved changes [ ] Type near character limit on site name → counter appears [ ] Use keyboard (arrows, Escape, Home/End) on dashboard tabs and notification bell [ ] Switch organizations → branded loading overlay appears during reload [ ] Check browser console in production → no console.log or console.error output [ ] Verify dark mode on uptime chart tooltips → should have dark background [ ] Landing page shows real dashboard screenshot with bottom fade
greptile-apps[bot] commented 2026-02-22 21:35:59 +00:00 (Migrated from github.com)

Greptile Summary

Large hardening and polish PR that touches 73 files across the entire Pulse frontend. The changes fall into several well-structured categories:

  • Skeleton loading system: Replaces all LoadingOverlay spinners and blank screens with content-aware skeleton placeholders, paired with a useMinimumLoading hook to prevent sub-300ms flicker
  • Error boundaries: Adds error.tsx files to every route group using a shared ErrorDisplay component
  • Security: New middleware for server-side route protection (cookie-based session detection), security headers in next.config.ts (HSTS, X-Frame-Options, CSP directives, etc.)
  • Accessibility: WAI-ARIA tab list keyboard navigation, semantic HTML (<button> replacing <div role="button">), aria-expanded/aria-haspopup/aria-current attributes, screen reader text, Escape key handlers
  • TypeScript cleanup: Systematic elimination of any types across ~15 files (catch (error: any)catch (error: unknown), onChange: anyReact.ChangeEvent, proper Record types)
  • Production hygiene: Debug console.log removed, console.error routed through a dev-only logger utility, dead getClient() with insecure localStorage token fallback removed
  • UX improvements: useUnsavedChanges hook for form navigation warnings, character counters with maxLength on inputs, autoFocus on form fields, real dashboard screenshot on landing page, live site stats on home page cards
  • Bug fixes: Dark mode chart tooltip (themeresolvedTheme), org switch session bug (cookie-based via setSessionAction instead of localStorage), favicon <img>next/image

Key issue found: The middleware redirects unauthenticated users to /login without preserving their intended destination URL, which degrades the login experience.

Confidence Score: 4/5

  • This PR is safe to merge — the changes are well-structured improvements with one functional issue in the middleware that should be addressed.
  • Score of 4 reflects that this is a large, well-executed hardening PR with consistent patterns applied across the codebase. The TypeScript cleanup, accessibility improvements, skeleton loading, and security headers are all solid. The one notable issue is the middleware not preserving the return URL on login redirect, which impacts UX but doesn't break functionality. The remaining style-level findings (deprecated Permissions-Policy directive, potential hydration mismatch, residual any type) are minor.
  • middleware.ts needs attention for the missing return_to parameter on login redirect. app/layout-content.tsx has a minor potential hydration mismatch from sessionStorage in useState initializer.

Important Files Changed

Filename Overview
middleware.ts New server-side route protection middleware. Clean logic using cookie-based session detection, but login redirect discards the original URL — users lose their intended destination after authentication.
next.config.ts Security headers, remote image patterns, and bundle optimization added. Minor issue: interest-cohort is a deprecated/non-standard Permissions-Policy directive.
lib/utils/logger.ts Dev-only logger that suppresses client-side console output in production. Clean and minimal implementation.
components/useMinimumLoading.ts Anti-flicker hook to ensure skeletons display for at least 300ms. Properly handles timer cleanup.
components/skeletons.tsx Comprehensive skeleton component library with primitives and composites for every page. Well-structured and consistent with design system.
components/ErrorDisplay.tsx Shared error UI component for route-level error boundaries. Provides retry and go-home actions with consistent styling.
lib/api/client.ts Removed dead getClient() with insecure localStorage.getItem('token') fallback. TypeScript types tightened from any to Record<string, unknown>.
app/actions/auth.ts Debug console.log statements removed from production server action. Logger migrated for error paths. No logic changes to auth flow.
app/layout-content.tsx Org switch loading overlay via sessionStorage flag. Potential hydration mismatch from reading sessionStorage in useState initializer. Remaining any[] type for orgs state.
components/settings/OrganizationSettings.tsx Comprehensive TypeScript cleanup: catch (error: any)catch (error: unknown), onChange: anyReact.ChangeEvent, removed @ts-ignore, fixed localStorage token bug by using setSessionAction, skeleton loading states replace spinners.
app/sites/[id]/settings/page.tsx Added unsaved changes warning via useUnsavedChanges, character counters, maxLength on inputs, skeleton loading, and TypeScript improvements. Form dirty detection uses JSON.stringify comparison which works correctly.
app/sites/[id]/uptime/page.tsx Fixed dark mode chart tooltip by switching from useTheme().theme to resolvedTheme. Skeleton loading, dynamic document.title, and character counters added.
app/sites/[id]/page.tsx LoadingOverlay replaced with DashboardSkeleton, useMinimumLoading prevents flicker, dynamic document.title added, logger migration for error paths.
components/notifications/NotificationCenter.tsx Significant accessibility improvements: ARIA attributes (aria-expanded, aria-haspopup, role="dialog"), Escape key handler, semantic button element replaces div[role="button"], skeleton loading rows.
components/WorkspaceSwitcher.tsx ARIA attributes added (role="group", aria-current, aria-busy), sessionStorage flag for org switch loading, screen reader text for current workspace, logger migration.
components/dashboard/Locations.tsx TypeScript cleanup: defined LocationItem type replacing any[], added useTabListKeyboard for arrow key navigation, skeleton loading, null-safe property access with ?? ''.
components/sites/SiteList.tsx Major refactor: extracted SiteCard component, added real-time stats display, replaced hardcoded -- with live visitor/pageview data, uses next/image for favicons.
app/page.tsx Landing page now shows real dashboard screenshot with bottom fade. Home page fetches site stats via Promise.allSettled for resilient loading. TypeScript cleanup.
app/share/[id]/page.tsx Improved error handling with ApiError instanceof checks instead of duck-typing. Replaced <img> with next/image. Minor: redundant cast on apiErr.data.
app/share/[id]/layout.tsx Dynamic generateMetadata for Open Graph tags using server-side fetch with revalidation. Provides good fallback metadata when fetch fails.
lib/hooks/useTabListKeyboard.ts WAI-ARIA compliant tab list keyboard handler supporting Arrow, Home, and End keys with wrapping focus.
lib/hooks/useUnsavedChanges.ts Clean beforeunload warning hook for unsaved form changes. Properly uses useCallback and cleanup.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Incoming Request] --> B{middleware.ts}
    B -->|Static asset / API| C[Skip - Not matched]
    B -->|Has session cookie?| D{Cookie check}
    
    D -->|Yes + /login or /signup| E[Redirect → /]
    D -->|Public route| F[Allow through]
    D -->|No session + Protected route| G[Redirect → /login]
    D -->|Has session + Protected route| H[Allow through]

    H --> I{Page Component}
    I -->|Loading| J[useMinimumLoading hook]
    J -->|< 300ms elapsed| K[Show Skeleton]
    J -->|≥ 300ms elapsed| L[Show Content]
    
    I -->|Runtime error| M[error.tsx boundary]
    M --> N[ErrorDisplay component]
    N -->|Retry| I
    N -->|Go home| O[Navigate to /]

    subgraph Org Switch Flow
        P[User clicks org] --> Q[switchContext API]
        Q --> R[setSessionAction - cookie]
        R --> S[sessionStorage flag]
        S --> T[window.location.reload]
        T --> U[LayoutContent reads flag]
        U --> V[LoadingOverlay 600ms]
        V --> W[Clear flag + render]
    end

Last reviewed commit: 31de661

<h3>Greptile Summary</h3> Large hardening and polish PR that touches 73 files across the entire Pulse frontend. The changes fall into several well-structured categories: - **Skeleton loading system**: Replaces all `LoadingOverlay` spinners and blank screens with content-aware skeleton placeholders, paired with a `useMinimumLoading` hook to prevent sub-300ms flicker - **Error boundaries**: Adds `error.tsx` files to every route group using a shared `ErrorDisplay` component - **Security**: New middleware for server-side route protection (cookie-based session detection), security headers in `next.config.ts` (HSTS, X-Frame-Options, CSP directives, etc.) - **Accessibility**: WAI-ARIA tab list keyboard navigation, semantic HTML (`<button>` replacing `<div role="button">`), `aria-expanded`/`aria-haspopup`/`aria-current` attributes, screen reader text, Escape key handlers - **TypeScript cleanup**: Systematic elimination of `any` types across ~15 files (`catch (error: any)` → `catch (error: unknown)`, `onChange: any` → `React.ChangeEvent`, proper `Record` types) - **Production hygiene**: Debug `console.log` removed, `console.error` routed through a dev-only `logger` utility, dead `getClient()` with insecure `localStorage` token fallback removed - **UX improvements**: `useUnsavedChanges` hook for form navigation warnings, character counters with `maxLength` on inputs, `autoFocus` on form fields, real dashboard screenshot on landing page, live site stats on home page cards - **Bug fixes**: Dark mode chart tooltip (`theme` → `resolvedTheme`), org switch session bug (cookie-based via `setSessionAction` instead of `localStorage`), favicon `<img>` → `next/image` Key issue found: The middleware redirects unauthenticated users to `/login` without preserving their intended destination URL, which degrades the login experience. <h3>Confidence Score: 4/5</h3> - This PR is safe to merge — the changes are well-structured improvements with one functional issue in the middleware that should be addressed. - Score of 4 reflects that this is a large, well-executed hardening PR with consistent patterns applied across the codebase. The TypeScript cleanup, accessibility improvements, skeleton loading, and security headers are all solid. The one notable issue is the middleware not preserving the return URL on login redirect, which impacts UX but doesn't break functionality. The remaining style-level findings (deprecated Permissions-Policy directive, potential hydration mismatch, residual `any` type) are minor. - `middleware.ts` needs attention for the missing return_to parameter on login redirect. `app/layout-content.tsx` has a minor potential hydration mismatch from sessionStorage in useState initializer. <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | middleware.ts | New server-side route protection middleware. Clean logic using cookie-based session detection, but login redirect discards the original URL — users lose their intended destination after authentication. | | next.config.ts | Security headers, remote image patterns, and bundle optimization added. Minor issue: `interest-cohort` is a deprecated/non-standard Permissions-Policy directive. | | lib/utils/logger.ts | Dev-only logger that suppresses client-side console output in production. Clean and minimal implementation. | | components/useMinimumLoading.ts | Anti-flicker hook to ensure skeletons display for at least 300ms. Properly handles timer cleanup. | | components/skeletons.tsx | Comprehensive skeleton component library with primitives and composites for every page. Well-structured and consistent with design system. | | components/ErrorDisplay.tsx | Shared error UI component for route-level error boundaries. Provides retry and go-home actions with consistent styling. | | lib/api/client.ts | Removed dead `getClient()` with insecure `localStorage.getItem('token')` fallback. TypeScript types tightened from `any` to `Record<string, unknown>`. | | app/actions/auth.ts | Debug console.log statements removed from production server action. Logger migrated for error paths. No logic changes to auth flow. | | app/layout-content.tsx | Org switch loading overlay via sessionStorage flag. Potential hydration mismatch from reading sessionStorage in useState initializer. Remaining `any[]` type for orgs state. | | components/settings/OrganizationSettings.tsx | Comprehensive TypeScript cleanup: `catch (error: any)` → `catch (error: unknown)`, `onChange: any` → `React.ChangeEvent`, removed `@ts-ignore`, fixed localStorage token bug by using `setSessionAction`, skeleton loading states replace spinners. | | app/sites/[id]/settings/page.tsx | Added unsaved changes warning via `useUnsavedChanges`, character counters, maxLength on inputs, skeleton loading, and TypeScript improvements. Form dirty detection uses JSON.stringify comparison which works correctly. | | app/sites/[id]/uptime/page.tsx | Fixed dark mode chart tooltip by switching from `useTheme().theme` to `resolvedTheme`. Skeleton loading, dynamic document.title, and character counters added. | | app/sites/[id]/page.tsx | LoadingOverlay replaced with DashboardSkeleton, useMinimumLoading prevents flicker, dynamic document.title added, logger migration for error paths. | | components/notifications/NotificationCenter.tsx | Significant accessibility improvements: ARIA attributes (aria-expanded, aria-haspopup, role="dialog"), Escape key handler, semantic button element replaces div[role="button"], skeleton loading rows. | | components/WorkspaceSwitcher.tsx | ARIA attributes added (role="group", aria-current, aria-busy), sessionStorage flag for org switch loading, screen reader text for current workspace, logger migration. | | components/dashboard/Locations.tsx | TypeScript cleanup: defined `LocationItem` type replacing `any[]`, added useTabListKeyboard for arrow key navigation, skeleton loading, null-safe property access with `?? ''`. | | components/sites/SiteList.tsx | Major refactor: extracted SiteCard component, added real-time stats display, replaced hardcoded `--` with live visitor/pageview data, uses next/image for favicons. | | app/page.tsx | Landing page now shows real dashboard screenshot with bottom fade. Home page fetches site stats via Promise.allSettled for resilient loading. TypeScript cleanup. | | app/share/[id]/page.tsx | Improved error handling with ApiError instanceof checks instead of duck-typing. Replaced `<img>` with next/image. Minor: redundant cast on `apiErr.data`. | | app/share/[id]/layout.tsx | Dynamic generateMetadata for Open Graph tags using server-side fetch with revalidation. Provides good fallback metadata when fetch fails. | | lib/hooks/useTabListKeyboard.ts | WAI-ARIA compliant tab list keyboard handler supporting Arrow, Home, and End keys with wrapping focus. | | lib/hooks/useUnsavedChanges.ts | Clean beforeunload warning hook for unsaved form changes. Properly uses useCallback and cleanup. | </details> </details> <h3>Flowchart</h3> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart TD A[Incoming Request] --> B{middleware.ts} B -->|Static asset / API| C[Skip - Not matched] B -->|Has session cookie?| D{Cookie check} D -->|Yes + /login or /signup| E[Redirect → /] D -->|Public route| F[Allow through] D -->|No session + Protected route| G[Redirect → /login] D -->|Has session + Protected route| H[Allow through] H --> I{Page Component} I -->|Loading| J[useMinimumLoading hook] J -->|< 300ms elapsed| K[Show Skeleton] J -->|≥ 300ms elapsed| L[Show Content] I -->|Runtime error| M[error.tsx boundary] M --> N[ErrorDisplay component] N -->|Retry| I N -->|Go home| O[Navigate to /] subgraph Org Switch Flow P[User clicks org] --> Q[switchContext API] Q --> R[setSessionAction - cookie] R --> S[sessionStorage flag] S --> T[window.location.reload] T --> U[LayoutContent reads flag] U --> V[LoadingOverlay 600ms] V --> W[Clear flag + render] end ``` <sub>Last reviewed commit: 31de661</sub>
greptile-apps[bot] (Migrated from github.com) reviewed 2026-02-22 21:47:16 +00:00
greptile-apps[bot] (Migrated from github.com) left a comment

73 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

<sub>73 files reviewed, 5 comments</sub> <sub>[Edit Code Review Agent Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews)</sub>
@@ -16,4 +20,4 @@
const auth = useAuth()
const router = useRouter()
const isOnline = useOnlineStatus()
const [orgs, setOrgs] = useState<any[]>([])
greptile-apps[bot] (Migrated from github.com) commented 2026-02-22 21:47:15 +00:00

Remaining any type for orgs state

Throughout this PR, any types have been systematically replaced with proper types, but orgs is still any[]. This should use the OrganizationMember type that's already imported from @/lib/api/organization:

  const [orgs, setOrgs] = useState<OrganizationMember[]>([])
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/layout-content.tsx
Line: 23

Comment:
**Remaining `any` type for `orgs` state**

Throughout this PR, `any` types have been systematically replaced with proper types, but `orgs` is still `any[]`. This should use the `OrganizationMember` type that's already imported from `@/lib/api/organization`:

```suggestion
  const [orgs, setOrgs] = useState<OrganizationMember[]>([])
```

How can I resolve this? If you propose a fix, please make it concise.
**Remaining `any` type for `orgs` state** Throughout this PR, `any` types have been systematically replaced with proper types, but `orgs` is still `any[]`. This should use the `OrganizationMember` type that's already imported from `@/lib/api/organization`: ```suggestion const [orgs, setOrgs] = useState<OrganizationMember[]>([]) ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/layout-content.tsx Line: 23 Comment: **Remaining `any` type for `orgs` state** Throughout this PR, `any` types have been systematically replaced with proper types, but `orgs` is still `any[]`. This should use the `OrganizationMember` type that's already imported from `@/lib/api/organization`: ```suggestion const [orgs, setOrgs] = useState<OrganizationMember[]>([]) ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -21,0 +25,4 @@
if (typeof window === 'undefined') return false
return sessionStorage.getItem(ORG_SWITCH_KEY) === 'true'
})
greptile-apps[bot] (Migrated from github.com) commented 2026-02-22 21:47:12 +00:00

SSR/hydration mismatch from sessionStorage in initial state

The lazy initializer reads from sessionStorage during the first render, but on the server typeof window === 'undefined' returns false. Since this is a 'use client' component, the server renders with false while the client may initialize with true, causing a React hydration mismatch warning. A safer pattern is to initialize as false and sync in a useEffect:

const [isSwitchingOrg, setIsSwitchingOrg] = useState(false)

useEffect(() => {
  if (sessionStorage.getItem(ORG_SWITCH_KEY) === 'true') {
    setIsSwitchingOrg(true)
  }
}, [])

This trades a brief flash for a guaranteed clean hydration. In practice, the current approach may work due to 'use client' only running on the client in many setups, but it's not guaranteed by the Next.js contract.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/layout-content.tsx
Line: 24-27

Comment:
**SSR/hydration mismatch from `sessionStorage` in initial state**

The lazy initializer reads from `sessionStorage` during the first render, but on the server `typeof window === 'undefined'` returns `false`. Since this is a `'use client'` component, the server renders with `false` while the client may initialize with `true`, causing a React hydration mismatch warning. A safer pattern is to initialize as `false` and sync in a `useEffect`:

```
const [isSwitchingOrg, setIsSwitchingOrg] = useState(false)

useEffect(() => {
  if (sessionStorage.getItem(ORG_SWITCH_KEY) === 'true') {
    setIsSwitchingOrg(true)
  }
}, [])
```

This trades a brief flash for a guaranteed clean hydration. In practice, the current approach may work due to `'use client'` only running on the client in many setups, but it's not guaranteed by the Next.js contract.

How can I resolve this? If you propose a fix, please make it concise.
**SSR/hydration mismatch from `sessionStorage` in initial state** The lazy initializer reads from `sessionStorage` during the first render, but on the server `typeof window === 'undefined'` returns `false`. Since this is a `'use client'` component, the server renders with `false` while the client may initialize with `true`, causing a React hydration mismatch warning. A safer pattern is to initialize as `false` and sync in a `useEffect`: ``` const [isSwitchingOrg, setIsSwitchingOrg] = useState(false) useEffect(() => { if (sessionStorage.getItem(ORG_SWITCH_KEY) === 'true') { setIsSwitchingOrg(true) } }, []) ``` This trades a brief flash for a guaranteed clean hydration. In practice, the current approach may work due to `'use client'` only running on the client in many setups, but it's not guaranteed by the Next.js contract. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/layout-content.tsx Line: 24-27 Comment: **SSR/hydration mismatch from `sessionStorage` in initial state** The lazy initializer reads from `sessionStorage` during the first render, but on the server `typeof window === 'undefined'` returns `false`. Since this is a `'use client'` component, the server renders with `false` while the client may initialize with `true`, causing a React hydration mismatch warning. A safer pattern is to initialize as `false` and sync in a `useEffect`: ``` const [isSwitchingOrg, setIsSwitchingOrg] = useState(false) useEffect(() => { if (sessionStorage.getItem(ORG_SWITCH_KEY) === 'true') { setIsSwitchingOrg(true) } }, []) ``` This trades a brief flash for a guaranteed clean hydration. In practice, the current approach may work due to `'use client'` only running on the client in many setups, but it's not guaranteed by the Next.js contract. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
greptile-apps[bot] (Migrated from github.com) commented 2026-02-22 21:47:14 +00:00

Redundant cast — apiErr.data is already Record<string, unknown>

ApiError.data is typed as Record<string, unknown> (changed in this PR), so the cast (apiErr.data as Record<string, unknown>) is a no-op. This can be simplified:

      if (apiErr?.status === 401 && apiErr.data?.is_protected) {
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/share/[id]/page.tsx
Line: 161

Comment:
**Redundant cast — `apiErr.data` is already `Record<string, unknown>`**

`ApiError.data` is typed as `Record<string, unknown>` (changed in this PR), so the cast `(apiErr.data as Record<string, unknown>)` is a no-op. This can be simplified:

```suggestion
      if (apiErr?.status === 401 && apiErr.data?.is_protected) {
```

How can I resolve this? If you propose a fix, please make it concise.
**Redundant cast — `apiErr.data` is already `Record<string, unknown>`** `ApiError.data` is typed as `Record<string, unknown>` (changed in this PR), so the cast `(apiErr.data as Record<string, unknown>)` is a no-op. This can be simplified: ```suggestion if (apiErr?.status === 401 && apiErr.data?.is_protected) { ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: app/share/[id]/page.tsx Line: 161 Comment: **Redundant cast — `apiErr.data` is already `Record<string, unknown>`** `ApiError.data` is typed as `Record<string, unknown>` (changed in this PR), so the cast `(apiErr.data as Record<string, unknown>)` is a no-op. This can be simplified: ```suggestion if (apiErr?.status === 401 && apiErr.data?.is_protected) { ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -0,0 +48,4 @@
if (!hasSession) {
const loginUrl = new URL('/login', request.url)
return NextResponse.redirect(loginUrl)
}
greptile-apps[bot] (Migrated from github.com) commented 2026-02-22 21:47:11 +00:00

Redirect to login discards the original URL

When an unauthenticated user visits a protected route like /sites/123, they are redirected to /login with no return_to or similar query parameter. After they authenticate, they'll land on / instead of being returned to the page they originally intended to visit. Consider preserving the original URL:

  if (!hasSession) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('return_to', pathname)
    return NextResponse.redirect(loginUrl)
  }

This would require the login flow to read return_to and redirect accordingly after authentication.

Prompt To Fix With AI
This is a comment left during a code review.
Path: middleware.ts
Line: 48-51

Comment:
**Redirect to login discards the original URL**

When an unauthenticated user visits a protected route like `/sites/123`, they are redirected to `/login` with no `return_to` or similar query parameter. After they authenticate, they'll land on `/` instead of being returned to the page they originally intended to visit. Consider preserving the original URL:

```suggestion
  if (!hasSession) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('return_to', pathname)
    return NextResponse.redirect(loginUrl)
  }
```

This would require the login flow to read `return_to` and redirect accordingly after authentication.

How can I resolve this? If you propose a fix, please make it concise.
**Redirect to login discards the original URL** When an unauthenticated user visits a protected route like `/sites/123`, they are redirected to `/login` with no `return_to` or similar query parameter. After they authenticate, they'll land on `/` instead of being returned to the page they originally intended to visit. Consider preserving the original URL: ```suggestion if (!hasSession) { const loginUrl = new URL('/login', request.url) loginUrl.searchParams.set('return_to', pathname) return NextResponse.redirect(loginUrl) } ``` This would require the login flow to read `return_to` and redirect accordingly after authentication. <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: middleware.ts Line: 48-51 Comment: **Redirect to login discards the original URL** When an unauthenticated user visits a protected route like `/sites/123`, they are redirected to `/login` with no `return_to` or similar query parameter. After they authenticate, they'll land on `/` instead of being returned to the page they originally intended to visit. Consider preserving the original URL: ```suggestion if (!hasSession) { const loginUrl = new URL('/login', request.url) loginUrl.searchParams.set('return_to', pathname) return NextResponse.redirect(loginUrl) } ``` This would require the login flow to read `return_to` and redirect accordingly after authentication. How can I resolve this? If you propose a fix, please make it concise. ````` </details>
@@ -15,0 +34,4 @@
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',
greptile-apps[bot] (Migrated from github.com) commented 2026-02-22 21:47:13 +00:00

interest-cohort is not a recognized Permissions-Policy directive

interest-cohort was a Chrome-only proposal (FLoC) that was abandoned and replaced by the Topics API. Modern browsers will ignore or warn about this unrecognized directive. It can be safely removed, or you could replace it with browsing-topics=() if you want to opt out of the Topics API:

            value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()',
Prompt To Fix With AI
This is a comment left during a code review.
Path: next.config.ts
Line: 37

Comment:
**`interest-cohort` is not a recognized Permissions-Policy directive**

`interest-cohort` was a Chrome-only proposal (FLoC) that was abandoned and replaced by the Topics API. Modern browsers will ignore or warn about this unrecognized directive. It can be safely removed, or you could replace it with `browsing-topics=()` if you want to opt out of the Topics API:

```suggestion
            value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()',
```

How can I resolve this? If you propose a fix, please make it concise.
**`interest-cohort` is not a recognized Permissions-Policy directive** `interest-cohort` was a Chrome-only proposal (FLoC) that was abandoned and replaced by the Topics API. Modern browsers will ignore or warn about this unrecognized directive. It can be safely removed, or you could replace it with `browsing-topics=()` if you want to opt out of the Topics API: ```suggestion value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()', ``` <details><summary>Prompt To Fix With AI</summary> `````markdown This is a comment left during a code review. Path: next.config.ts Line: 37 Comment: **`interest-cohort` is not a recognized Permissions-Policy directive** `interest-cohort` was a Chrome-only proposal (FLoC) that was abandoned and replaced by the Topics API. Modern browsers will ignore or warn about this unrecognized directive. It can be safely removed, or you could replace it with `browsing-topics=()` if you want to opt out of the Topics API: ```suggestion value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()', ``` How can I resolve this? If you propose a fix, please make it concise. ````` </details>
Sign in to join this conversation.
No description provided.