[PULSE-60] Frontend hardening, UX polish, and security #35
Reference in New Issue
Block a user
No description provided.
Delete Branch "staging"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Work Item
PULSE-60
Summary
<img>withnext/imagefor favicons, and centralized shared constantsChanges
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 /logincomponents/ErrorDisplay.tsx— New shared error UI component for route-level error boundariesapp/**/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-iconsapp/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 errorsapp/sites/[id]/uptime/page.tsx— Fixed dark mode chart tooltip (useTheme().theme → resolvedTheme)app/sites/[id]/settings/page.tsx— Autofocus, useUnsavedChanges hook, character counters, maxLengthapp/sites/new/page.tsx,welcome/page.tsx,funnels/new/page.tsx— Autofocus, maxLength on inputscomponents/dashboard/Locations.tsx,TechSpecs.tsx,ContentStats.tsx,Campaigns.tsx,TopReferrers.tsx,PerformanceStats.tsx— Skeleton loading, useTabListKeyboard for arrow key nav, ARIA attributescomponents/notifications/NotificationCenter.tsx— Skeleton rows, ARIA (aria-expanded, aria-haspopup, role="dialog"), Escape key handler, semantic button elementcomponents/WorkspaceSwitcher.tsx— ARIA (role="group", aria-current, aria-busy), sessionStorage flag for org switch loadingcomponents/settings/OrganizationSettings.tsx— Skeleton loading, catch (error: any) → catch (error: unknown), onChange: any → React.ChangeEvent, removed @ts-ignore, fixed localStorage token bugcomponents/dashboard/ExportModal.tsx— Removed any casts with proper Record types and jspdf-autotable.d.tscomponents/Footer.tsx— LinkComponent: any → React.ElementTypelib/hooks/useUnsavedChanges.ts— New hook for beforeunload warninglib/hooks/useTabListKeyboard.ts— New hook for WAI-ARIA tab list keyboard navigationlib/utils/logger.ts— New dev-only logger that suppresses console.error/warn in production client buildslib/utils/icons.tsx— Centralized FAVICON_SERVICE_URL constantlib/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 librariesapp/layout-content.tsx— Org switch loading overlay via sessionStorage flagapp/actions/auth.ts,lib/auth/context.tsx,lib/api/organization.ts— Removed console.log statements, migrated console.error to loggerapp/page.tsx— Real dashboard screenshot preview, removed static mockupapp/share/[id]/page.tsx— next/image for favicon, ApiError instanceof checksapp/pricing/page.tsx— Static metadata, removed @ts-ignore, catch any → unknownlib/utils/notifications.tsx— aria-hidden on decorative iconsdocs/DESIGN_SYSTEM.md— Updated error toast examplepackage.json— Version bump to 0.11.0-alphaCHANGELOG.md— Full release notes for 0.11.0-alphaTest 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 Summary
Large hardening and polish PR that touches 73 files across the entire Pulse frontend. The changes fall into several well-structured categories:
LoadingOverlayspinners and blank screens with content-aware skeleton placeholders, paired with auseMinimumLoadinghook to prevent sub-300ms flickererror.tsxfiles to every route group using a sharedErrorDisplaycomponentnext.config.ts(HSTS, X-Frame-Options, CSP directives, etc.)<button>replacing<div role="button">),aria-expanded/aria-haspopup/aria-currentattributes, screen reader text, Escape key handlersanytypes across ~15 files (catch (error: any)→catch (error: unknown),onChange: any→React.ChangeEvent, properRecordtypes)console.logremoved,console.errorrouted through a dev-onlyloggerutility, deadgetClient()with insecurelocalStoragetoken fallback removeduseUnsavedChangeshook for form navigation warnings, character counters withmaxLengthon inputs,autoFocuson form fields, real dashboard screenshot on landing page, live site stats on home page cardstheme→resolvedTheme), org switch session bug (cookie-based viasetSessionActioninstead oflocalStorage), favicon<img>→next/imageKey issue found: The middleware redirects unauthenticated users to
/loginwithout preserving their intended destination URL, which degrades the login experience.Confidence Score: 4/5
anytype) are minor.middleware.tsneeds attention for the missing return_to parameter on login redirect.app/layout-content.tsxhas a minor potential hydration mismatch from sessionStorage in useState initializer.Important Files Changed
interest-cohortis a deprecated/non-standard Permissions-Policy directive.getClient()with insecurelocalStorage.getItem('token')fallback. TypeScript types tightened fromanytoRecord<string, unknown>.any[]type for orgs state.catch (error: any)→catch (error: unknown),onChange: any→React.ChangeEvent, removed@ts-ignore, fixed localStorage token bug by usingsetSessionAction, skeleton loading states replace spinners.useUnsavedChanges, character counters, maxLength on inputs, skeleton loading, and TypeScript improvements. Form dirty detection uses JSON.stringify comparison which works correctly.useTheme().themetoresolvedTheme. Skeleton loading, dynamic document.title, and character counters added.LocationItemtype replacingany[], added useTabListKeyboard for arrow key navigation, skeleton loading, null-safe property access with?? ''.--with live visitor/pageview data, uses next/image for favicons.<img>with next/image. Minor: redundant cast onapiErr.data.Flowchart
Last reviewed commit:
31de66173 files reviewed, 5 comments
Edit Code Review Agent Settings | Greptile
@@ -16,4 +20,4 @@const auth = useAuth()const router = useRouter()const isOnline = useOnlineStatus()const [orgs, setOrgs] = useState<any[]>([])Remaining
anytype fororgsstateThroughout this PR,
anytypes have been systematically replaced with proper types, butorgsis stillany[]. This should use theOrganizationMembertype that's already imported from@/lib/api/organization:Prompt To Fix With AI
@@ -21,0 +25,4 @@if (typeof window === 'undefined') return falsereturn sessionStorage.getItem(ORG_SWITCH_KEY) === 'true'})SSR/hydration mismatch from
sessionStoragein initial stateThe lazy initializer reads from
sessionStorageduring the first render, but on the servertypeof window === 'undefined'returnsfalse. Since this is a'use client'component, the server renders withfalsewhile the client may initialize withtrue, causing a React hydration mismatch warning. A safer pattern is to initialize asfalseand sync in auseEffect: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
Redundant cast —
apiErr.datais alreadyRecord<string, unknown>ApiError.datais typed asRecord<string, unknown>(changed in this PR), so the cast(apiErr.data as Record<string, unknown>)is a no-op. This can be simplified:Prompt To Fix With AI
@@ -0,0 +48,4 @@if (!hasSession) {const loginUrl = new URL('/login', request.url)return NextResponse.redirect(loginUrl)}Redirect to login discards the original URL
When an unauthenticated user visits a protected route like
/sites/123, they are redirected to/loginwith noreturn_toor 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:This would require the login flow to read
return_toand redirect accordingly after authentication.Prompt To Fix With AI
@@ -15,0 +34,4 @@{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },{key: 'Permissions-Policy',value: 'camera=(), microphone=(), geolocation=(), interest-cohort=()',interest-cohortis not a recognized Permissions-Policy directiveinterest-cohortwas 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 withbrowsing-topics=()if you want to opt out of the Topics API:Prompt To Fix With AI