From cc268c320e1dbc94107b8c7bffdd70f7e0824651 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 7 Mar 2026 19:10:23 +0100 Subject: [PATCH 1/5] feat: replace ghost buttons with underline tab bar for site navigation Dashboard, Uptime, Funnels, and Settings now use a consistent underline tab bar with orange active indicator, matching the existing panel tab design language. --- CHANGELOG.md | 9 ++++++ app/sites/[id]/funnels/page.tsx | 25 ++++++--------- app/sites/[id]/page.tsx | 38 ++++------------------ app/sites/[id]/settings/page.tsx | 3 ++ app/sites/[id]/uptime/page.tsx | 18 ++++------- components/dashboard/SiteNav.tsx | 54 ++++++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 59 deletions(-) create mode 100644 components/dashboard/SiteNav.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 76af938..dbcb278 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] +### Improved + +- **Cleaner site navigation.** Dashboard, Uptime, Funnels, and Settings now use an underline tab bar instead of floating buttons. The active section is highlighted with an orange underline, making it easy to see where you are and switch between views. + +### Fixed + +- **City and region data is now accurate.** Location data was incorrectly showing the CDN server's location (e.g. Paris, Villeurbanne) instead of the visitor's actual city. Fixed by reading the correct visitor IP header from Bunny CDN. +- **"Reset Data" now clears everything.** Previously, resetting a site's data in Settings only removed pageviews and daily stats. Uptime check history, uptime daily stats, and cached dashboard data were left behind. All collected data is now properly cleared when you reset, while your site configuration, goals, funnels, and uptime monitors are kept. + ## [0.13.0-alpha] - 2026-03-07 ### Added diff --git a/app/sites/[id]/funnels/page.tsx b/app/sites/[id]/funnels/page.tsx index 2b565c4..ff3e0e9 100644 --- a/app/sites/[id]/funnels/page.tsx +++ b/app/sites/[id]/funnels/page.tsx @@ -6,6 +6,7 @@ import { listFunnels, deleteFunnel, type Funnel } from '@/lib/api/funnels' import { toast, PlusIcon, ArrowRightIcon, ChevronLeftIcon, TrashIcon, Button } from '@ciphera-net/ui' import { FunnelsListSkeleton, useMinimumLoading } from '@/components/skeletons' import Link from 'next/link' +import SiteNav from '@/components/dashboard/SiteNav' export default function FunnelsPage() { const params = useParams() @@ -52,14 +53,10 @@ export default function FunnelsPage() { return (
+ +
-
- - - +

Funnels @@ -68,14 +65,12 @@ export default function FunnelsPage() { Track user journeys and identify drop-off points

-
- - - -
+ + +
{funnels.length === 0 ? ( diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 4c878d0..61809c0 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useAuth } from '@/lib/auth/context' + import { logger } from '@/lib/utils/logger' import { useCallback, useEffect, useState, useMemo } from 'react' import { useParams, useRouter, useSearchParams } from 'next/navigation' @@ -34,6 +34,7 @@ import PerformanceStats from '@/components/dashboard/PerformanceStats' import GoalStats from '@/components/dashboard/GoalStats' import ScrollDepth from '@/components/dashboard/ScrollDepth' import Campaigns from '@/components/dashboard/Campaigns' +import SiteNav from '@/components/dashboard/SiteNav' import FilterBar from '@/components/dashboard/FilterBar' import AddFilterDropdown, { type FilterSuggestion, type FilterSuggestions } from '@/components/dashboard/AddFilterDropdown' import EventProperties from '@/components/dashboard/EventProperties' @@ -79,8 +80,8 @@ function getInitialDateRange(): { start: string; end: string } { } export default function SiteDashboardPage() { - const { user } = useAuth() - const canEdit = user?.role === 'owner' || user?.role === 'admin' + + const params = useParams() const router = useRouter() @@ -494,39 +495,12 @@ export default function SiteDashboardPage() { ]} />
-
-
- - - {canEdit && ( - - )} -
+ + {/* Dimension Filters */}
diff --git a/app/sites/[id]/settings/page.tsx b/app/sites/[id]/settings/page.tsx index e5d34ea..fefa324 100644 --- a/app/sites/[id]/settings/page.tsx +++ b/app/sites/[id]/settings/page.tsx @@ -15,6 +15,7 @@ import { APP_URL } from '@/lib/api/client' import { generatePrivacySnippet } from '@/lib/utils/privacySnippet' import { useUnsavedChanges } from '@/lib/hooks/useUnsavedChanges' import { getSubscription, type SubscriptionDetails } from '@/lib/api/billing' +import SiteNav from '@/components/dashboard/SiteNav' import { getRetentionOptionsForPlan, formatRetentionMonths } from '@/lib/plans' import { motion, AnimatePresence } from 'framer-motion' import { useAuth } from '@/lib/auth/context' @@ -403,6 +404,8 @@ export default function SiteSettingsPage() { return (
+ +

Site Settings

diff --git a/app/sites/[id]/uptime/page.tsx b/app/sites/[id]/uptime/page.tsx index 85a666b..09ac33c 100644 --- a/app/sites/[id]/uptime/page.tsx +++ b/app/sites/[id]/uptime/page.tsx @@ -21,6 +21,7 @@ import { toast } from '@ciphera-net/ui' import { useTheme } from '@ciphera-net/ui' import { getAuthErrorMessage } from '@ciphera-net/ui' import { Button, Modal } from '@ciphera-net/ui' +import SiteNav from '@/components/dashboard/SiteNav' import { UptimeSkeleton, ChecksSkeleton, useMinimumLoading } from '@/components/skeletons' import { AreaChart, @@ -723,21 +724,14 @@ export default function UptimePage() { transition={{ duration: 0.2 }} className="w-full max-w-6xl mx-auto px-4 sm:px-6 py-8" > + + {/* Header */}
-
- - / -

- Uptime -

-
+

+ Uptime +

Monitor your endpoints and track availability over time

diff --git a/components/dashboard/SiteNav.tsx b/components/dashboard/SiteNav.tsx new file mode 100644 index 0000000..0c3bae6 --- /dev/null +++ b/components/dashboard/SiteNav.tsx @@ -0,0 +1,54 @@ +'use client' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import { useTabListKeyboard } from '@/lib/hooks/useTabListKeyboard' +import { useAuth } from '@/lib/auth/context' + +interface SiteNavProps { + siteId: string +} + +export default function SiteNav({ siteId }: SiteNavProps) { + const pathname = usePathname() + const handleTabKeyDown = useTabListKeyboard() + const { user } = useAuth() + const canEdit = user?.role === 'owner' || user?.role === 'admin' + + const tabs = [ + { label: 'Dashboard', href: `/sites/${siteId}` }, + { label: 'Uptime', href: `/sites/${siteId}/uptime` }, + { label: 'Funnels', href: `/sites/${siteId}/funnels` }, + ...(canEdit ? [{ label: 'Settings', href: `/sites/${siteId}/settings` }] : []), + ] + + const isActive = (href: string) => { + if (href === `/sites/${siteId}`) { + return pathname === href || pathname === `${href}/realtime` + } + return pathname.startsWith(href) + } + + return ( +
+ +
+ ) +} From d2dfe6299309484461f5dcae9ac61222174260f5 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 7 Mar 2026 19:37:41 +0100 Subject: [PATCH 2/5] fix: recover gracefully from stale Server Action hashes after deployment Wrap all Server Action calls (getSessionAction, exchangeAuthCode, logoutAction) in try-catch so a cached browser bundle with old action IDs triggers a hard reload instead of an infinite loading spinner. --- CHANGELOG.md | 1 + app/auth/callback/page.tsx | 9 ++++++++- lib/auth/context.tsx | 19 ++++++++++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbcb278..2fee81c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed +- **Login no longer gets stuck after updates.** If you happened to have Pulse open when a new version was deployed, logging back in could get stuck on a loading screen. The app now automatically refreshes itself to pick up the latest version. - **City and region data is now accurate.** Location data was incorrectly showing the CDN server's location (e.g. Paris, Villeurbanne) instead of the visitor's actual city. Fixed by reading the correct visitor IP header from Bunny CDN. - **"Reset Data" now clears everything.** Previously, resetting a site's data in Settings only removed pageviews and daily stats. Uptime check history, uptime daily stats, and cached dashboard data were left behind. All collected data is now properly cleared when you reset, while your site configuration, goals, funnels, and uptime monitors are kept. diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index 2359b75..933fdce 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -22,7 +22,14 @@ function AuthCallbackContent() { const codeVerifier = localStorage.getItem('oauth_code_verifier') const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/auth/callback' : '' if (!code) return - const result = await exchangeAuthCode(code, codeVerifier, redirectUri) + let result: Awaited> + try { + result = await exchangeAuthCode(code, codeVerifier, redirectUri) + } catch { + // * Stale build — cached JS has old Server Action hashes. Hard reload to fix. + window.location.reload() + return + } if (result.success && result.user) { // * Fetch full profile (including display_name) before navigating so header shows correct name on first paint try { diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index 95f8b39..d9adb92 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -90,7 +90,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const logout = useCallback(async () => { setIsLoggingOut(true) - await logoutAction() + try { await logoutAction() } catch { /* stale build — continue with client-side cleanup */ } localStorage.removeItem('user') localStorage.removeItem('ciphera_token_refreshed_at') localStorage.removeItem('ciphera_last_activity') @@ -132,7 +132,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const init = async () => { // * 1. Check server-side session (cookies) - let session = await getSessionAction() + let session: Awaited> = null + try { + session = await getSessionAction() + } catch { + // * Stale build — browser has cached JS with old Server Action hashes. + // * Force a hard reload to fetch fresh bundles from the server. + window.location.reload() + return + } // * 2. If no access_token but refresh_token may exist, try refresh (fixes 15-min inactivity logout) if (!session && typeof window !== 'undefined') { @@ -142,7 +150,12 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { credentials: 'include', }) if (refreshRes.ok) { - session = await getSessionAction() + try { + session = await getSessionAction() + } catch { + window.location.reload() + return + } } } From 6338d1dfe726a2c8f6848a5c2cb009c0b634d611 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 7 Mar 2026 19:55:16 +0100 Subject: [PATCH 3/5] fix: prevent infinite reload loop on stale build recovery Use sessionStorage guard so the hard reload only fires once. If the reload doesn't fix it (CDN still serving stale JS), fall through gracefully instead of looping forever. --- app/auth/callback/page.tsx | 11 +++++++++-- lib/auth/context.tsx | 22 +++++++++++++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index 933fdce..3b630ae 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -26,8 +26,15 @@ function AuthCallbackContent() { try { result = await exchangeAuthCode(code, codeVerifier, redirectUri) } catch { - // * Stale build — cached JS has old Server Action hashes. Hard reload to fix. - window.location.reload() + // * Stale build — cached JS has old Server Action hashes. Hard reload once to fix. + const key = 'pulse_reload_for_stale_build' + if (!sessionStorage.getItem(key)) { + sessionStorage.setItem(key, '1') + window.location.reload() + return + } + sessionStorage.removeItem(key) + setError('Something went wrong. Please try logging in again.') return } if (result.success && result.user) { diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index d9adb92..b6a9944 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -135,10 +135,19 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { let session: Awaited> = null try { session = await getSessionAction() + sessionStorage.removeItem('pulse_reload_for_stale_build') } catch { // * Stale build — browser has cached JS with old Server Action hashes. - // * Force a hard reload to fetch fresh bundles from the server. - window.location.reload() + // * Force a hard reload once to fetch fresh bundles. Guard prevents infinite loop. + const key = 'pulse_reload_for_stale_build' + if (!sessionStorage.getItem(key)) { + sessionStorage.setItem(key, '1') + window.location.reload() + return + } + sessionStorage.removeItem(key) + // * Reload didn't fix it — treat as no session + setLoading(false) return } @@ -153,7 +162,14 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { try { session = await getSessionAction() } catch { - window.location.reload() + const key = 'pulse_reload_for_stale_build' + if (!sessionStorage.getItem(key)) { + sessionStorage.setItem(key, '1') + window.location.reload() + return + } + sessionStorage.removeItem(key) + setLoading(false) return } } From ad806e0427714329e26b019c604068b019cae9c3 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 7 Mar 2026 20:02:58 +0100 Subject: [PATCH 4/5] fix: remove reload-based stale build recovery to stop login loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit window.location.reload() causes infinite loops when the CDN keeps serving cached assets. Instead, silently treat Server Action failures as no-session — the OAuth flow uses full navigations (window.location.href) which naturally fetch fresh content from the server on return. --- app/auth/callback/page.tsx | 9 +-------- lib/auth/context.tsx | 26 ++++---------------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/app/auth/callback/page.tsx b/app/auth/callback/page.tsx index 3b630ae..0452bf5 100644 --- a/app/auth/callback/page.tsx +++ b/app/auth/callback/page.tsx @@ -26,14 +26,7 @@ function AuthCallbackContent() { try { result = await exchangeAuthCode(code, codeVerifier, redirectUri) } catch { - // * Stale build — cached JS has old Server Action hashes. Hard reload once to fix. - const key = 'pulse_reload_for_stale_build' - if (!sessionStorage.getItem(key)) { - sessionStorage.setItem(key, '1') - window.location.reload() - return - } - sessionStorage.removeItem(key) + // * Stale build or network error — show error so user can retry via full navigation setError('Something went wrong. Please try logging in again.') return } diff --git a/lib/auth/context.tsx b/lib/auth/context.tsx index b6a9944..db49ca7 100644 --- a/lib/auth/context.tsx +++ b/lib/auth/context.tsx @@ -135,20 +135,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { let session: Awaited> = null try { session = await getSessionAction() - sessionStorage.removeItem('pulse_reload_for_stale_build') } catch { - // * Stale build — browser has cached JS with old Server Action hashes. - // * Force a hard reload once to fetch fresh bundles. Guard prevents infinite loop. - const key = 'pulse_reload_for_stale_build' - if (!sessionStorage.getItem(key)) { - sessionStorage.setItem(key, '1') - window.location.reload() - return - } - sessionStorage.removeItem(key) - // * Reload didn't fix it — treat as no session - setLoading(false) - return + // * Stale build — treat as no session. The login page will redirect + // * to the auth service via window.location.href (full navigation), + // * which fetches fresh HTML/JS from the server on return. } // * 2. If no access_token but refresh_token may exist, try refresh (fixes 15-min inactivity logout) @@ -162,15 +152,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { try { session = await getSessionAction() } catch { - const key = 'pulse_reload_for_stale_build' - if (!sessionStorage.getItem(key)) { - sessionStorage.setItem(key, '1') - window.location.reload() - return - } - sessionStorage.removeItem(key) - setLoading(false) - return + // * Stale build — fall through as no session } } } From 2f5bcf479aff04f9c34fdbeaa23ab25fabbaef7f Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Sat, 7 Mar 2026 20:12:11 +0100 Subject: [PATCH 5/5] fix: add Cache-Control no-cache for HTML pages to prevent stale CDN content Bunny CDN was caching HTML pages, so after deploys the browser kept loading old JS bundles with expired Server Action hashes. This header tells the CDN to always revalidate with the origin. Static assets (/_next/static/*) are excluded since they are content-hashed. --- next.config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/next.config.ts b/next.config.ts index a8ec28d..0052619 100644 --- a/next.config.ts +++ b/next.config.ts @@ -47,6 +47,14 @@ const nextConfig: NextConfig = { }, async headers() { return [ + { + // * Prevent CDN/browser from serving stale HTML after deploys. + // * Static assets (/_next/static/*) are content-hashed and cached separately by Next.js. + source: '/((?!_next/static|_next/image).*)', + headers: [ + { key: 'Cache-Control', value: 'no-cache, must-revalidate' }, + ], + }, { source: '/(.*)', headers: [