From bcc02c93a05449a28e0a0182267ac291711974bd Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 08:04:46 +0100 Subject: [PATCH 01/16] chore: update CHANGELOG.md to highlight faster dashboard loading feature with intelligent caching for improved performance --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e1f285..ab2ee63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Faster dashboard loading.** Your site analytics now load almost instantly, even during busy periods. Behind the scenes, we've added intelligent caching that remembers your dashboard data for 30 seconds and refreshes it automatically in the background. Real-time visitor counts are updated every 5 seconds so you always see current activity without waiting. - **Performance insights.** Track how fast your site loads with Core Web Vitals (page load speed, layout shifts, responsiveness). Turn it on in Site Settings → Data & Privacy to see a performance widget on your dashboard. - **Goals & Events.** Define custom goals (e.g. signup, purchase) and track them with `pulse.track()` in your snippet. Counts appear on your dashboard once you add goals in Site Settings → Goals & Events. - **2FA recovery codes backup.** When you enable 2FA, you receive recovery codes. You can now regenerate new codes (with password confirmation) from Settings and download them as a `.txt` file. Regenerating invalidates all existing codes. From 209ec1608ab750da787bef68ec3c250361a2b763 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 08:41:02 +0100 Subject: [PATCH 02/16] chore: update CHANGELOG.md to include better data management for long-term performance, enhancing analytics data storage and retrieval --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2ee63..e85a60d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added - **Faster dashboard loading.** Your site analytics now load almost instantly, even during busy periods. Behind the scenes, we've added intelligent caching that remembers your dashboard data for 30 seconds and refreshes it automatically in the background. Real-time visitor counts are updated every 5 seconds so you always see current activity without waiting. +- **Better data management for long-term performance.** We've restructured how your analytics data is stored so the app stays fast even as you collect months of data. Old data is now automatically organized by month and cleaned up efficiently based on your retention settings, keeping everything running smoothly no matter how much traffic you get. - **Performance insights.** Track how fast your site loads with Core Web Vitals (page load speed, layout shifts, responsiveness). Turn it on in Site Settings → Data & Privacy to see a performance widget on your dashboard. - **Goals & Events.** Define custom goals (e.g. signup, purchase) and track them with `pulse.track()` in your snippet. Counts appear on your dashboard once you add goals in Site Settings → Goals & Events. - **2FA recovery codes backup.** When you enable 2FA, you receive recovery codes. You can now regenerate new codes (with password confirmation) from Settings and download them as a `.txt` file. Regenerating invalidates all existing codes. From faa0bfe64a5a2558e8d26120297ebc9132e0d30f Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 08:47:22 +0100 Subject: [PATCH 03/16] chore: update CHANGELOG.md to include smarter database indexing for improved query performance and reduced storage overhead --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e85a60d..56f9cd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Faster dashboard loading.** Your site analytics now load almost instantly, even during busy periods. Behind the scenes, we've added intelligent caching that remembers your dashboard data for 30 seconds and refreshes it automatically in the background. Real-time visitor counts are updated every 5 seconds so you always see current activity without waiting. - **Better data management for long-term performance.** We've restructured how your analytics data is stored so the app stays fast even as you collect months of data. Old data is now automatically organized by month and cleaned up efficiently based on your retention settings, keeping everything running smoothly no matter how much traffic you get. -- **Performance insights.** Track how fast your site loads with Core Web Vitals (page load speed, layout shifts, responsiveness). Turn it on in Site Settings → Data & Privacy to see a performance widget on your dashboard. +- **Smarter database indexing.** We've optimized how your analytics data is indexed, making common queries—like loading your dashboard or filtering by date—significantly faster. This also reduces storage overhead, keeping the app lean as your data grows. +- **Performance insights. Track how fast your site loads with Core Web Vitals (page load speed, layout shifts, responsiveness). Turn it on in Site Settings → Data & Privacy to see a performance widget on your dashboard. - **Goals & Events.** Define custom goals (e.g. signup, purchase) and track them with `pulse.track()` in your snippet. Counts appear on your dashboard once you add goals in Site Settings → Goals & Events. - **2FA recovery codes backup.** When you enable 2FA, you receive recovery codes. You can now regenerate new codes (with password confirmation) from Settings and download them as a `.txt` file. Regenerating invalidates all existing codes. From 3aa0d7ae7c7c2c68edd2567d25f7f5c264636689 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 08:49:23 +0100 Subject: [PATCH 04/16] chore: update CHANGELOG.md to include faster dashboard statistics feature using pre-computed daily summaries for improved loading times --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56f9cd0..2403a39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Faster dashboard loading.** Your site analytics now load almost instantly, even during busy periods. Behind the scenes, we've added intelligent caching that remembers your dashboard data for 30 seconds and refreshes it automatically in the background. Real-time visitor counts are updated every 5 seconds so you always see current activity without waiting. - **Better data management for long-term performance.** We've restructured how your analytics data is stored so the app stays fast even as you collect months of data. Old data is now automatically organized by month and cleaned up efficiently based on your retention settings, keeping everything running smoothly no matter how much traffic you get. - **Smarter database indexing.** We've optimized how your analytics data is indexed, making common queries—like loading your dashboard or filtering by date—significantly faster. This also reduces storage overhead, keeping the app lean as your data grows. +- **Faster dashboard statistics.** Loading stats for any date range is now much quicker. Instead of recalculating from scratch every time, we use pre-computed daily summaries so your analytics appear instantly, even for months of data. - **Performance insights. Track how fast your site loads with Core Web Vitals (page load speed, layout shifts, responsiveness). Turn it on in Site Settings → Data & Privacy to see a performance widget on your dashboard. - **Goals & Events.** Define custom goals (e.g. signup, purchase) and track them with `pulse.track()` in your snippet. Counts appear on your dashboard once you add goals in Site Settings → Goals & Events. - **2FA recovery codes backup.** When you enable 2FA, you receive recovery codes. You can now regenerate new codes (with password confirmation) from Settings and download them as a `.txt` file. Regenerating invalidates all existing codes. From 3efd23b38646a98d8b54e164d60dc54146ebcaf5 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 09:10:08 +0100 Subject: [PATCH 05/16] chore: update CHANGELOG.md to include enhancements for dashboard performance, including smarter updates, real-time visitor tracking, and faster event processing --- CHANGELOG.md | 4 + app/sites/[id]/page.tsx | 75 +++++++++++++++-- lib/hooks/useVisibilityPolling.ts | 128 ++++++++++++++++++++++++++++++ lib/swr/dashboard.ts | 114 ++++++++++++++++++++++++++ package.json | 1 + 5 files changed, 314 insertions(+), 8 deletions(-) create mode 100644 lib/hooks/useVisibilityPolling.ts create mode 100644 lib/swr/dashboard.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2403a39..0e9700b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Smarter dashboard updates.** Your dashboard now knows when you're actively viewing it versus when it's in the background. When you switch to another tab, we intelligently slow down data refreshes to save resources, then instantly catch up when you return. This keeps your analytics current without putting unnecessary load on the system. +- **Instant real-time visitor counts.** Your dashboard's "current visitors" counter now updates lightning-fast using an optimized tracking system. Instead of scanning your entire database, we maintain a live session index that shows active visitors in milliseconds—even when thousands of people are browsing your sites simultaneously. +- **More accurate visitor tracking.** We've upgraded how we identify unique visitors to ensure your analytics are always precise, even during the busiest traffic spikes. Every visitor now gets a truly unique identifier that never overlaps with others, eliminating rare edge cases where visitor counts could be slightly off. +- **Faster event tracking.** Your analytics data is now captured instantly without slowing down your website. We've switched to asynchronous processing that collects events in batches of 100, so your visitors' page views and interactions are recorded with zero impact on their browsing experience, even during traffic spikes. - **Faster dashboard loading.** Your site analytics now load almost instantly, even during busy periods. Behind the scenes, we've added intelligent caching that remembers your dashboard data for 30 seconds and refreshes it automatically in the background. Real-time visitor counts are updated every 5 seconds so you always see current activity without waiting. - **Better data management for long-term performance.** We've restructured how your analytics data is stored so the app stays fast even as you collect months of data. Old data is now automatically organized by month and cleaned up efficiently based on your retention settings, keeping everything running smoothly no matter how much traffic you get. - **Smarter database indexing.** We've optimized how your analytics data is indexed, making common queries—like loading your dashboard or filtering by date—significantly faster. This also reduces storage overhead, keeping the app lean as your data grows. diff --git a/app/sites/[id]/page.tsx b/app/sites/[id]/page.tsx index 8622551..56f73fd 100644 --- a/app/sites/[id]/page.tsx +++ b/app/sites/[id]/page.tsx @@ -2,7 +2,7 @@ import { useAuth } from '@/lib/auth/context' import { logger } from '@/lib/utils/logger' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useState, useRef } from 'react' import { useParams, useRouter } from 'next/navigation' import { motion } from 'framer-motion' import { getSite, type Site } from '@/lib/api/sites' @@ -148,6 +148,23 @@ export default function SiteDashboardPage() { return { start: prevStart.toISOString().split('T')[0], end: prevEnd.toISOString().split('T')[0] } }, []) + // * Visibility-aware polling intervals + // * Historical data: 60s when visible, paused when hidden + // * Real-time data: 5s when visible, 30s when hidden + const [isVisible, setIsVisible] = useState(true) + const dashboardIntervalRef = useRef(null) + const realtimeIntervalRef = useRef(null) + + // * Track visibility state + useEffect(() => { + const handleVisibilityChange = () => { + const visible = document.visibilityState === 'visible' + setIsVisible(visible) + } + document.addEventListener('visibilitychange', handleVisibilityChange) + return () => document.removeEventListener('visibilitychange', handleVisibilityChange) + }, []) + const loadData = useCallback(async (silent = false) => { try { if (!silent) setLoading(true) @@ -204,18 +221,60 @@ export default function SiteDashboardPage() { const data = await getRealtime(siteId) setRealtime(data.visitors) } catch (error) { - // Silently fail for realtime updates + // * Silently fail for realtime updates } }, [siteId]) + // * Visibility-aware polling for dashboard data (historical) + // * Refreshes every 60 seconds when tab is visible, pauses when hidden useEffect(() => { - if (isSettingsLoaded) loadData() - const interval = setInterval(() => { - loadData(true) + if (!isSettingsLoaded) return + + // * Initial load + loadData() + + // * Clear existing interval + if (dashboardIntervalRef.current) { + clearInterval(dashboardIntervalRef.current) + } + + // * Only poll when visible (saves server resources when tab is backgrounded) + if (isVisible) { + dashboardIntervalRef.current = setInterval(() => { + loadData(true) + }, 60000) // * 60 seconds for historical data + } + + return () => { + if (dashboardIntervalRef.current) { + clearInterval(dashboardIntervalRef.current) + } + } + }, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, isVisible]) + + // * Visibility-aware polling for realtime data + // * Refreshes every 5 seconds when visible, every 30 seconds when hidden + useEffect(() => { + if (!isSettingsLoaded) return + + // * Clear existing interval + if (realtimeIntervalRef.current) { + clearInterval(realtimeIntervalRef.current) + } + + // * Different intervals based on visibility + const interval = isVisible ? 5000 : 30000 // * 5s visible, 30s hidden + + realtimeIntervalRef.current = setInterval(() => { loadRealtime() - }, 30000) - return () => clearInterval(interval) - }, [siteId, dateRange, todayInterval, multiDayInterval, isSettingsLoaded, loadData, loadRealtime]) + }, interval) + + return () => { + if (realtimeIntervalRef.current) { + clearInterval(realtimeIntervalRef.current) + } + } + }, [siteId, isSettingsLoaded, loadRealtime, isVisible]) useEffect(() => { if (site?.domain) document.title = `${site.domain} | Pulse` diff --git a/lib/hooks/useVisibilityPolling.ts b/lib/hooks/useVisibilityPolling.ts new file mode 100644 index 0000000..a20bd61 --- /dev/null +++ b/lib/hooks/useVisibilityPolling.ts @@ -0,0 +1,128 @@ +// * Custom hook for visibility-aware polling +// * Pauses polling when tab is not visible, resumes when visible +// * Reduces server load when users aren't actively viewing the dashboard + +import { useEffect, useRef, useState, useCallback } from 'react' + +interface UseVisibilityPollingOptions { + // * Polling interval when tab is visible (in milliseconds) + visibleInterval: number + // * Polling interval when tab is hidden (in milliseconds, or null to pause) + hiddenInterval: number | null +} + +interface UseVisibilityPollingReturn { + // * Whether polling is currently active + isPolling: boolean + // * Time since last poll + lastPollTime: number | null + // * Force a poll immediately + triggerPoll: () => void +} + +export function useVisibilityPolling( + callback: () => void | Promise, + options: UseVisibilityPollingOptions +): UseVisibilityPollingReturn { + const { visibleInterval, hiddenInterval } = options + const [isPolling, setIsPolling] = useState(false) + const [lastPollTime, setLastPollTime] = useState(null) + const intervalRef = useRef(null) + const callbackRef = useRef(callback) + + // * Keep callback reference up to date + useEffect(() => { + callbackRef.current = callback + }, [callback]) + + // * Get current polling interval based on visibility + const getInterval = useCallback((): number | null => { + if (typeof document === 'undefined') return null + + const isVisible = document.visibilityState === 'visible' + if (isVisible) { + return visibleInterval + } + return hiddenInterval + }, [visibleInterval, hiddenInterval]) + + // * Start polling with current interval + const startPolling = useCallback(() => { + const interval = getInterval() + if (interval === null) { + setIsPolling(false) + return + } + + setIsPolling(true) + + // * Clear any existing interval + if (intervalRef.current) { + clearInterval(intervalRef.current) + } + + // * Set up new interval + intervalRef.current = setInterval(() => { + callbackRef.current() + setLastPollTime(Date.now()) + }, interval) + }, [getInterval]) + + // * Stop polling + const stopPolling = useCallback(() => { + if (intervalRef.current) { + clearInterval(intervalRef.current) + intervalRef.current = null + } + setIsPolling(false) + }, []) + + // * Trigger immediate poll + const triggerPoll = useCallback(() => { + callbackRef.current() + setLastPollTime(Date.now()) + + // * Restart polling timer + startPolling() + }, [startPolling]) + + // * Handle visibility changes + useEffect(() => { + if (typeof document === 'undefined') return + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + // * Tab became visible - resume polling with visible interval + startPolling() + // * Trigger immediate poll to get fresh data + triggerPoll() + } else { + // * Tab hidden - switch to hidden interval or pause + const interval = getInterval() + if (interval === null) { + stopPolling() + } else { + // * Restart with hidden interval + startPolling() + } + } + } + + // * Listen for visibility changes + document.addEventListener('visibilitychange', handleVisibilityChange) + + // * Start polling initially + startPolling() + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange) + stopPolling() + } + }, [startPolling, stopPolling, triggerPoll, getInterval]) + + return { + isPolling, + lastPollTime, + triggerPoll, + } +} diff --git a/lib/swr/dashboard.ts b/lib/swr/dashboard.ts new file mode 100644 index 0000000..18f6b4c --- /dev/null +++ b/lib/swr/dashboard.ts @@ -0,0 +1,114 @@ +// * SWR configuration for dashboard data fetching +// * Implements stale-while-revalidate pattern for efficient data updates + +import useSWR from 'swr' +import { getDashboard, getRealtime, getStats, getDailyStats } from '@/lib/api/stats' +import { getSite } from '@/lib/api/sites' +import type { Site } from '@/lib/api/sites' +import type { Stats, DailyStat } from '@/lib/api/stats' + +// * SWR fetcher functions +const fetchers = { + site: (siteId: string) => getSite(siteId), + dashboard: (siteId: string, start: string, end: string) => getDashboard(siteId, start, end), + stats: (siteId: string, start: string, end: string) => getStats(siteId, start, end), + dailyStats: (siteId: string, start: string, end: string, interval: 'hour' | 'day' | 'minute') => + getDailyStats(siteId, start, end, interval), + realtime: (siteId: string) => getRealtime(siteId), +} + +// * Standard SWR config for dashboard data +const dashboardSWRConfig = { + // * Keep stale data visible while revalidating (better UX) + revalidateOnFocus: false, + // * Revalidate when reconnecting (fresh data after offline) + revalidateOnReconnect: true, + // * Retry failed requests + shouldRetryOnError: true, + errorRetryCount: 3, + // * Error retry interval with exponential backoff + errorRetryInterval: 5000, +} + +// * Hook for site data (loads once, refreshes rarely) +export function useSite(siteId: string) { + return useSWR( + siteId ? ['site', siteId] : null, + () => fetchers.site(siteId), + { + ...dashboardSWRConfig, + // * Site data changes rarely, refresh every 5 minutes + refreshInterval: 5 * 60 * 1000, + // * Deduping interval to prevent duplicate requests + dedupingInterval: 30 * 1000, + } + ) +} + +// * Hook for dashboard summary data (refreshed less frequently) +export function useDashboard(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboard', siteId, start, end] : null, + () => fetchers.dashboard(siteId, start, end), + { + ...dashboardSWRConfig, + // * Refresh every 60 seconds for dashboard summary + refreshInterval: 60 * 1000, + // * Deduping interval to prevent duplicate requests + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for stats (refreshed less frequently) +export function useStats(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['stats', siteId, start, end] : null, + () => fetchers.stats(siteId, start, end), + { + ...dashboardSWRConfig, + // * Refresh every 60 seconds for stats + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for daily stats (refreshed less frequently) +export function useDailyStats( + siteId: string, + start: string, + end: string, + interval: 'hour' | 'day' | 'minute' +) { + return useSWR( + siteId && start && end ? ['dailyStats', siteId, start, end, interval] : null, + () => fetchers.dailyStats(siteId, start, end, interval), + { + ...dashboardSWRConfig, + // * Refresh every 60 seconds for chart data + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for realtime visitor count (refreshed frequently) +export function useRealtime(siteId: string, refreshInterval: number = 5000) { + return useSWR<{ visitors: number }>( + siteId ? ['realtime', siteId] : null, + () => fetchers.realtime(siteId), + { + ...dashboardSWRConfig, + // * Refresh frequently for real-time data (default 5 seconds) + refreshInterval, + // * Short deduping for real-time + dedupingInterval: 2000, + // * Keep previous data while loading new data + keepPreviousData: true, + } + ) +} + +// * Re-export for convenience +export { fetchers } diff --git a/package.json b/package.json index f8d66ec..0463c28 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "react-simple-maps": "^3.0.0", "recharts": "^2.15.0", "sonner": "^2.0.7", + "swr": "^2.3.3", "xlsx": "^0.18.5" }, "overrides": { From 36774cc995af419540f804ad01db699a9a34a572 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 09:13:29 +0100 Subject: [PATCH 06/16] chore: update CHANGELOG.md to include smarter data fetching with request deduplication and caching for improved performance --- CHANGELOG.md | 1 + lib/api/client.ts | 97 +++++++++++++++++++++++++++++++++++++---------- package-lock.json | 23 +++++++++++ 3 files changed, 102 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e9700b..e0b7fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Smarter data fetching.** Your dashboard now automatically prevents duplicate requests when multiple components ask for the same data at the same time. It also briefly caches recent responses, so switching between pages feels instant while still keeping everything up to date. This reduces server load and makes the app feel snappier. - **Smarter dashboard updates.** Your dashboard now knows when you're actively viewing it versus when it's in the background. When you switch to another tab, we intelligently slow down data refreshes to save resources, then instantly catch up when you return. This keeps your analytics current without putting unnecessary load on the system. - **Instant real-time visitor counts.** Your dashboard's "current visitors" counter now updates lightning-fast using an optimized tracking system. Instead of scanning your entire database, we maintain a live session index that shows active visitors in milliseconds—even when thousands of people are browsing your sites simultaneously. - **More accurate visitor tracking.** We've upgraded how we identify unique visitors to ensure your analytics are always precise, even during the busiest traffic spikes. Every visitor now gets a truly unique identifier that never overlaps with others, eliminating rare edge cases where visitor counts could be slightly off. diff --git a/lib/api/client.ts b/lib/api/client.ts index f7cccce..10643e1 100644 --- a/lib/api/client.ts +++ b/lib/api/client.ts @@ -59,21 +59,46 @@ function onRefreshFailed(err: unknown) { } /** - * Base API client with error handling + * Base API client with error handling, request deduplication, and short-term caching */ async function apiRequest( endpoint: string, options: RequestInit = {} ): Promise { + // * Skip deduplication for non-GET requests (mutations should always execute) + const method = options.method || 'GET' + const shouldDedupe = method === 'GET' + + if (shouldDedupe) { + // * Clean up expired entries periodically + if (pendingRequests.size > 100 || responseCache.size > 100) { + cleanupExpiredEntries() + } + + const requestKey = getRequestKey(endpoint, options) + + // * Check if we have a recent cached response (within 2 seconds) + const cached = responseCache.get(requestKey) as CachedResponse | undefined + if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { + return cached.data + } + + // * Check if there's an identical request in flight + const pending = pendingRequests.get(requestKey) as PendingRequest | undefined + if (pending && Date.now() - pending.timestamp < 30000) { + return pending.promise + } + } + // * Determine base URL const isAuthRequest = endpoint.startsWith('/auth') const baseUrl = isAuthRequest ? AUTH_API_URL : API_URL - + // * Handle legacy endpoints that already include /api/ prefix - const url = endpoint.startsWith('/api/') + const url = endpoint.startsWith('/api/') ? `${baseUrl}${endpoint}` : `${baseUrl}/api/v1${endpoint}` - + const headers: HeadersInit = { 'Content-Type': 'application/json', ...options.headers, @@ -86,22 +111,24 @@ async function apiRequest( const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) const signal = options.signal ?? controller.signal - let response: Response - try { - response = await fetch(url, { - ...options, - headers, - credentials: 'include', // * IMPORTANT: Send cookies - signal, - }) - clearTimeout(timeoutId) - } catch (e) { - clearTimeout(timeoutId) - if (e instanceof Error && (e.name === 'AbortError' || e.name === 'TypeError')) { - throw new ApiError(AUTH_ERROR_MESSAGES.NETWORK, 0) + // * Create the request promise + const requestPromise = (async (): Promise => { + let response: Response + try { + response = await fetch(url, { + ...options, + headers, + credentials: 'include', // * IMPORTANT: Send cookies + signal, + }) + clearTimeout(timeoutId) + } catch (e) { + clearTimeout(timeoutId) + if (e instanceof Error && (e.name === 'AbortError' || e.name === 'TypeError')) { + throw new ApiError(AUTH_ERROR_MESSAGES.NETWORK, 0) + } + throw e } - throw e - } if (!response.ok) { if (response.status === 401) { @@ -182,6 +209,38 @@ async function apiRequest( } return response.json() + })() + + // * For GET requests, track the promise for deduplication and cache the result + if (shouldDedupe) { + const requestKey = getRequestKey(endpoint, options) + + // * Store in pending requests + pendingRequests.set(requestKey, { + promise: requestPromise as Promise, + timestamp: Date.now(), + }) + + // * Clean up pending request and cache the result when done + requestPromise + .then((data) => { + // * Cache successful response + responseCache.set(requestKey, { + data, + timestamp: Date.now(), + }) + // * Remove from pending + pendingRequests.delete(requestKey) + return data + }) + .catch((error) => { + // * Remove from pending on error too + pendingRequests.delete(requestKey) + throw error + }) + } + + return requestPromise } export const authFetch = apiRequest diff --git a/package-lock.json b/package-lock.json index 3ca446f..1d89a05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "react-simple-maps": "^3.0.0", "recharts": "^2.15.0", "sonner": "^2.0.7", + "swr": "^2.3.3", "xlsx": "^0.18.5" }, "devDependencies": { @@ -10356,6 +10357,19 @@ "node": ">=12.0.0" } }, + "node_modules/swr": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.0.tgz", + "integrity": "sha512-sUlC20T8EOt1pHmDiqueUWMmRRX03W7w5YxovWX7VR2KHEPCTMly85x05vpkP5i6Bu4h44ePSMD9Tc+G2MItFw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tailwind-merge": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", @@ -11113,6 +11127,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", From 4cff0c621d6b0764db50d5bc8f4d83c2767ec537 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 09:17:51 +0100 Subject: [PATCH 07/16] feat: implement request deduplication and caching in API client for improved performance --- lib/api/client.ts | 59 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/lib/api/client.ts b/lib/api/client.ts index 10643e1..506a5d5 100644 --- a/lib/api/client.ts +++ b/lib/api/client.ts @@ -58,6 +58,57 @@ function onRefreshFailed(err: unknown) { refreshSubscribers = [] } +// * ============================================================================ +// * Request Deduplication & Caching +// * ============================================================================ + +/** Cache TTL in milliseconds (2 seconds) */ +const CACHE_TTL_MS = 2_000 + +/** Stores in-flight requests for deduplication */ +interface PendingRequest { + promise: Promise + timestamp: number +} +const pendingRequests = new Map() + +/** Stores cached responses */ +interface CachedResponse { + data: unknown + timestamp: number +} +const responseCache = new Map() + +/** + * Generate a unique key for a request based on endpoint and options + */ +function getRequestKey(endpoint: string, options: RequestInit): string { + const method = options.method || 'GET' + const body = options.body || '' + return `${method}:${endpoint}:${body}` +} + +/** + * Clean up expired entries from pending requests and response cache + */ +function cleanupExpiredEntries(): void { + const now = Date.now() + + // * Clean up stale pending requests (older than 30 seconds) + for (const [key, pending] of pendingRequests.entries()) { + if (now - pending.timestamp > 30_000) { + pendingRequests.delete(key) + } + } + + // * Clean up stale cached responses (older than CACHE_TTL_MS) + for (const [key, cached] of responseCache.entries()) { + if (now - cached.timestamp > CACHE_TTL_MS) { + responseCache.delete(key) + } + } +} + /** * Base API client with error handling, request deduplication, and short-term caching */ @@ -78,15 +129,15 @@ async function apiRequest( const requestKey = getRequestKey(endpoint, options) // * Check if we have a recent cached response (within 2 seconds) - const cached = responseCache.get(requestKey) as CachedResponse | undefined + const cached = responseCache.get(requestKey) if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { - return cached.data + return cached.data as T } // * Check if there's an identical request in flight - const pending = pendingRequests.get(requestKey) as PendingRequest | undefined + const pending = pendingRequests.get(requestKey) if (pending && Date.now() - pending.timestamp < 30000) { - return pending.promise + return pending.promise as Promise } } From 704a38f3df6caeb520eaca6dc364bd8339851cf7 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 09:24:01 +0100 Subject: [PATCH 08/16] chore: update CHANGELOG.md to include lighter dashboard data transfers for improved loading times and new focused dashboard endpoints for efficient data retrieval --- CHANGELOG.md | 1 + lib/api/stats.ts | 261 +++++++++++++++++++++++++++++++++++++++++-- lib/swr/dashboard.ts | 124 +++++++++++++++++++- 3 files changed, 377 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b7fe1..d10524c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Lighter dashboard data transfers.** Your dashboard now loads data in smaller, focused pieces instead of one massive bundle. This means faster loading times—especially on slower connections—and your analytics appear section by section as they become ready, rather than making you wait for everything at once. - **Smarter data fetching.** Your dashboard now automatically prevents duplicate requests when multiple components ask for the same data at the same time. It also briefly caches recent responses, so switching between pages feels instant while still keeping everything up to date. This reduces server load and makes the app feel snappier. - **Smarter dashboard updates.** Your dashboard now knows when you're actively viewing it versus when it's in the background. When you switch to another tab, we intelligently slow down data refreshes to save resources, then instantly catch up when you return. This keeps your analytics current without putting unnecessary load on the system. - **Instant real-time visitor counts.** Your dashboard's "current visitors" counter now updates lightning-fast using an optimized tracking system. Instead of scanning your entire database, we maintain a live session index that shows active visitors in milliseconds—even when thousands of people are browsing your sites simultaneously. diff --git a/lib/api/stats.ts b/lib/api/stats.ts index 205a166..2f4c871 100644 --- a/lib/api/stats.ts +++ b/lib/api/stats.ts @@ -332,11 +332,11 @@ export async function getDashboard(siteId: string, startDate?: string, endDate?: } export async function getPublicDashboard( - siteId: string, - startDate?: string, - endDate?: string, - limit = 10, - interval?: string, + siteId: string, + startDate?: string, + endDate?: string, + limit = 10, + interval?: string, password?: string, captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } ): Promise { @@ -344,9 +344,256 @@ export async function getPublicDashboard( if (startDate) params.append('start_date', startDate) if (endDate) params.append('end_date', endDate) if (interval) params.append('interval', interval) - + appendAuthParams(params, { password, captcha }) - + params.append('limit', limit.toString()) return apiRequest(`/public/sites/${siteId}/dashboard?${params.toString()}`) } + +// * ============================================================================ +// * Focused Dashboard Endpoints (Fix 4.2: Efficient Data Transfer) +// * These split the massive dashboard payload into smaller, focused chunks +// * ============================================================================ + +export interface DashboardOverviewData { + site: Site + stats: Stats + realtime_visitors: number + daily_stats: DailyStat[] +} + +export async function getDashboardOverview( + siteId: string, + startDate?: string, + endDate?: string, + interval?: string +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + if (interval) params.append('interval', interval) + return apiRequest(`/sites/${siteId}/dashboard/overview?${params.toString()}`) +} + +export async function getPublicDashboardOverview( + siteId: string, + startDate?: string, + endDate?: string, + interval?: string, + password?: string, + captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + if (interval) params.append('interval', interval) + appendAuthParams(params, { password, captcha }) + return apiRequest(`/public/sites/${siteId}/dashboard/overview?${params.toString()}`) +} + +export interface DashboardPagesData { + top_pages: TopPage[] + entry_pages: TopPage[] + exit_pages: TopPage[] +} + +export async function getDashboardPages( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10 +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest(`/sites/${siteId}/dashboard/pages?${params.toString()}`) +} + +export async function getPublicDashboardPages( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10, + password?: string, + captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + appendAuthParams(params, { password, captcha }) + return apiRequest(`/public/sites/${siteId}/dashboard/pages?${params.toString()}`) +} + +export interface DashboardLocationsData { + countries: CountryStat[] + cities: CityStat[] + regions: RegionStat[] +} + +export async function getDashboardLocations( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10, + countryLimit = 250 +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + params.append('country_limit', countryLimit.toString()) + return apiRequest(`/sites/${siteId}/dashboard/locations?${params.toString()}`) +} + +export async function getPublicDashboardLocations( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10, + countryLimit = 250, + password?: string, + captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + params.append('country_limit', countryLimit.toString()) + appendAuthParams(params, { password, captcha }) + return apiRequest(`/public/sites/${siteId}/dashboard/locations?${params.toString()}`) +} + +export interface DashboardDevicesData { + browsers: BrowserStat[] + os: OSStat[] + devices: DeviceStat[] + screen_resolutions: ScreenResolutionStat[] +} + +export async function getDashboardDevices( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10 +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest(`/sites/${siteId}/dashboard/devices?${params.toString()}`) +} + +export async function getPublicDashboardDevices( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10, + password?: string, + captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + appendAuthParams(params, { password, captcha }) + return apiRequest(`/public/sites/${siteId}/dashboard/devices?${params.toString()}`) +} + +export interface DashboardReferrersData { + top_referrers: TopReferrer[] +} + +export async function getDashboardReferrers( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10 +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest(`/sites/${siteId}/dashboard/referrers?${params.toString()}`) +} + +export async function getPublicDashboardReferrers( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10, + password?: string, + captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + appendAuthParams(params, { password, captcha }) + return apiRequest(`/public/sites/${siteId}/dashboard/referrers?${params.toString()}`) +} + +export interface DashboardPerformanceData { + performance?: PerformanceStats + performance_by_page?: PerformanceByPageStat[] +} + +export async function getDashboardPerformance( + siteId: string, + startDate?: string, + endDate?: string +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + return apiRequest(`/sites/${siteId}/dashboard/performance?${params.toString()}`) +} + +export async function getPublicDashboardPerformance( + siteId: string, + startDate?: string, + endDate?: string, + password?: string, + captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + appendAuthParams(params, { password, captcha }) + return apiRequest(`/public/sites/${siteId}/dashboard/performance?${params.toString()}`) +} + +export interface DashboardGoalsData { + goal_counts: GoalCountStat[] +} + +export async function getDashboardGoals( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10 +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + return apiRequest(`/sites/${siteId}/dashboard/goals?${params.toString()}`) +} + +export async function getPublicDashboardGoals( + siteId: string, + startDate?: string, + endDate?: string, + limit = 10, + password?: string, + captcha?: { captcha_id?: string, captcha_solution?: string, captcha_token?: string } +): Promise { + const params = new URLSearchParams() + if (startDate) params.append('start_date', startDate) + if (endDate) params.append('end_date', endDate) + params.append('limit', limit.toString()) + appendAuthParams(params, { password, captcha }) + return apiRequest(`/public/sites/${siteId}/dashboard/goals?${params.toString()}`) +} diff --git a/lib/swr/dashboard.ts b/lib/swr/dashboard.ts index 18f6b4c..a81657c 100644 --- a/lib/swr/dashboard.ts +++ b/lib/swr/dashboard.ts @@ -2,15 +2,44 @@ // * Implements stale-while-revalidate pattern for efficient data updates import useSWR from 'swr' -import { getDashboard, getRealtime, getStats, getDailyStats } from '@/lib/api/stats' +import { + getDashboard, + getDashboardOverview, + getDashboardPages, + getDashboardLocations, + getDashboardDevices, + getDashboardReferrers, + getDashboardPerformance, + getDashboardGoals, + getRealtime, + getStats, + getDailyStats, +} from '@/lib/api/stats' import { getSite } from '@/lib/api/sites' import type { Site } from '@/lib/api/sites' -import type { Stats, DailyStat } from '@/lib/api/stats' +import type { + Stats, + DailyStat, + DashboardOverviewData, + DashboardPagesData, + DashboardLocationsData, + DashboardDevicesData, + DashboardReferrersData, + DashboardPerformanceData, + DashboardGoalsData, +} from '@/lib/api/stats' // * SWR fetcher functions const fetchers = { site: (siteId: string) => getSite(siteId), dashboard: (siteId: string, start: string, end: string) => getDashboard(siteId, start, end), + dashboardOverview: (siteId: string, start: string, end: string) => getDashboardOverview(siteId, start, end), + dashboardPages: (siteId: string, start: string, end: string) => getDashboardPages(siteId, start, end), + dashboardLocations: (siteId: string, start: string, end: string) => getDashboardLocations(siteId, start, end), + dashboardDevices: (siteId: string, start: string, end: string) => getDashboardDevices(siteId, start, end), + dashboardReferrers: (siteId: string, start: string, end: string) => getDashboardReferrers(siteId, start, end), + dashboardPerformance: (siteId: string, start: string, end: string) => getDashboardPerformance(siteId, start, end), + dashboardGoals: (siteId: string, start: string, end: string) => getDashboardGoals(siteId, start, end), stats: (siteId: string, start: string, end: string) => getStats(siteId, start, end), dailyStats: (siteId: string, start: string, end: string, interval: 'hour' | 'day' | 'minute') => getDailyStats(siteId, start, end, interval), @@ -110,5 +139,96 @@ export function useRealtime(siteId: string, refreshInterval: number = 5000) { ) } +// * Hook for focused dashboard overview data (Fix 4.2: Efficient Data Transfer) +export function useDashboardOverview(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboardOverview', siteId, start, end] : null, + () => fetchers.dashboardOverview(siteId, start, end), + { + ...dashboardSWRConfig, + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for focused dashboard pages data +export function useDashboardPages(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboardPages', siteId, start, end] : null, + () => fetchers.dashboardPages(siteId, start, end), + { + ...dashboardSWRConfig, + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for focused dashboard locations data +export function useDashboardLocations(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboardLocations', siteId, start, end] : null, + () => fetchers.dashboardLocations(siteId, start, end), + { + ...dashboardSWRConfig, + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for focused dashboard devices data +export function useDashboardDevices(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboardDevices', siteId, start, end] : null, + () => fetchers.dashboardDevices(siteId, start, end), + { + ...dashboardSWRConfig, + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for focused dashboard referrers data +export function useDashboardReferrers(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboardReferrers', siteId, start, end] : null, + () => fetchers.dashboardReferrers(siteId, start, end), + { + ...dashboardSWRConfig, + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for focused dashboard performance data +export function useDashboardPerformance(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboardPerformance', siteId, start, end] : null, + () => fetchers.dashboardPerformance(siteId, start, end), + { + ...dashboardSWRConfig, + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + +// * Hook for focused dashboard goals data +export function useDashboardGoals(siteId: string, start: string, end: string) { + return useSWR( + siteId && start && end ? ['dashboardGoals', siteId, start, end] : null, + () => fetchers.dashboardGoals(siteId, start, end), + { + ...dashboardSWRConfig, + refreshInterval: 60 * 1000, + dedupingInterval: 10 * 1000, + } + ) +} + // * Re-export for convenience export { fetchers } From e7e217777a4e623cddb6c0b0518ecb569e56d4ef Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 09:34:43 +0100 Subject: [PATCH 09/16] chore: update CHANGELOG.md to include faster analytics processing for improved daily stats updates across multiple sites --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d10524c..b654ca9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Faster analytics processing for all sites.** We've upgraded how your daily analytics are calculated behind the scenes. Instead of processing sites one by one, we now analyze multiple sites simultaneously using a smart parallel system. This means your daily stats—like visitor counts and page views—are updated more quickly and consistently, even as we handle data from thousands of websites. - **Lighter dashboard data transfers.** Your dashboard now loads data in smaller, focused pieces instead of one massive bundle. This means faster loading times—especially on slower connections—and your analytics appear section by section as they become ready, rather than making you wait for everything at once. - **Smarter data fetching.** Your dashboard now automatically prevents duplicate requests when multiple components ask for the same data at the same time. It also briefly caches recent responses, so switching between pages feels instant while still keeping everything up to date. This reduces server load and makes the app feel snappier. - **Smarter dashboard updates.** Your dashboard now knows when you're actively viewing it versus when it's in the background. When you switch to another tab, we intelligently slow down data refreshes to save resources, then instantly catch up when you return. This keeps your analytics current without putting unnecessary load on the system. From a9aaf244564ff6ac66a62e15a2fcdfe80b9435de Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 10:04:13 +0100 Subject: [PATCH 10/16] chore: update CHANGELOG.md to include multiple performance enhancements, such as faster billing page loading, improved funnel analysis, and more reliable database connections under heavy load --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b654ca9..b1101dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **Faster billing page loading.** Your subscription details now load much quicker when you visit the billing page. Previously, several requests to our payment provider were made one after another, which could add 1-2 seconds to the page load. Now these happen simultaneously, cutting the wait time significantly. If any request takes too long, we gracefully continue so you always see your billing information without frustrating delays. +- **Faster funnel analysis for multi-step conversions.** We've significantly improved how conversion funnels are calculated. Instead of scanning your data multiple times for each step in a funnel, we now do it in a single efficient pass. This means complex funnels with multiple steps load almost instantly instead of taking seconds—or even timing out. We've also added a reasonable limit of 5 steps per funnel to ensure optimal performance. +- **More reliable database connections under heavy load.** We've optimized how Pulse manages its database connections to handle much higher traffic without issues. By increasing the connection pool size and improving how connections are reused, your dashboard stays responsive even when thousands of users are viewing analytics simultaneously. We also added better monitoring so we can detect and address connection issues before they affect you. +- **Better support for growing teams and traffic.** We've added infrastructure improvements that allow Pulse to run smoothly across multiple servers. When you scale up to handle more traffic, our background processes—like daily analytics calculations and data cleanup—will coordinate automatically so they don't conflict with each other. This ensures reliable performance as your team and data grow. +- **Smarter protection for heavy dashboard operations.** We've implemented a new tiered rate limiting system that treats complex dashboard queries differently from simple requests. Expensive operations—like loading your full dashboard with all its charts and data—now have their own dedicated limits to prevent anyone from accidentally overwhelming the system with too many rapid refreshes. This keeps everything running smoothly for everyone, especially during busy periods. +- **Smarter caching for faster dashboard loading.** We've added intelligent caching headers to our API responses, so your browser can remember recently loaded data and show it instantly when you navigate between pages. This works alongside our existing server-side caching to make your dashboard feel even more responsive—especially when switching between different date ranges or sections. +- **More flexible uptime monitoring.** We've made our uptime checker more adaptable to different needs. Instead of a fixed limit on how many websites we can check simultaneously, you can now configure this based on your requirements. This means faster uptime checks for busy sites with many monitors, while keeping things efficient for smaller setups. +- **Smarter data cleanup for better performance.** We've improved how old analytics data is cleaned up to keep everything running smoothly. Instead of deleting large amounts of data all at once—which could slow things down—we now remove old data in small, efficient batches. This ensures your dashboard stays fast and responsive even as we clean up months of historical data behind the scenes. - **Faster analytics processing for all sites.** We've upgraded how your daily analytics are calculated behind the scenes. Instead of processing sites one by one, we now analyze multiple sites simultaneously using a smart parallel system. This means your daily stats—like visitor counts and page views—are updated more quickly and consistently, even as we handle data from thousands of websites. - **Lighter dashboard data transfers.** Your dashboard now loads data in smaller, focused pieces instead of one massive bundle. This means faster loading times—especially on slower connections—and your analytics appear section by section as they become ready, rather than making you wait for everything at once. - **Smarter data fetching.** Your dashboard now automatically prevents duplicate requests when multiple components ask for the same data at the same time. It also briefly caches recent responses, so switching between pages feels instant while still keeping everything up to date. This reduces server load and makes the app feel snappier. From 0022e7b335b6b0e11092431b0a3d5b7e27bce57f Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 10:07:09 +0100 Subject: [PATCH 11/16] chore: update CHANGELOG.md to clarify improvements in visitor tracking accuracy, ensuring unique identifiers for analytics during high traffic periods --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1101dc..56ed5d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Smarter data fetching.** Your dashboard now automatically prevents duplicate requests when multiple components ask for the same data at the same time. It also briefly caches recent responses, so switching between pages feels instant while still keeping everything up to date. This reduces server load and makes the app feel snappier. - **Smarter dashboard updates.** Your dashboard now knows when you're actively viewing it versus when it's in the background. When you switch to another tab, we intelligently slow down data refreshes to save resources, then instantly catch up when you return. This keeps your analytics current without putting unnecessary load on the system. - **Instant real-time visitor counts.** Your dashboard's "current visitors" counter now updates lightning-fast using an optimized tracking system. Instead of scanning your entire database, we maintain a live session index that shows active visitors in milliseconds—even when thousands of people are browsing your sites simultaneously. -- **More accurate visitor tracking.** We've upgraded how we identify unique visitors to ensure your analytics are always precise, even during the busiest traffic spikes. Every visitor now gets a truly unique identifier that never overlaps with others, eliminating rare edge cases where visitor counts could be slightly off. - **Faster event tracking.** Your analytics data is now captured instantly without slowing down your website. We've switched to asynchronous processing that collects events in batches of 100, so your visitors' page views and interactions are recorded with zero impact on their browsing experience, even during traffic spikes. - **Faster dashboard loading.** Your site analytics now load almost instantly, even during busy periods. Behind the scenes, we've added intelligent caching that remembers your dashboard data for 30 seconds and refreshes it automatically in the background. Real-time visitor counts are updated every 5 seconds so you always see current activity without waiting. - **Better data management for long-term performance.** We've restructured how your analytics data is stored so the app stays fast even as you collect months of data. Old data is now automatically organized by month and cleaned up efficiently based on your retention settings, keeping everything running smoothly no matter how much traffic you get. @@ -36,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, stale refresh cookies caused the middleware to redirect away from the login page; now only a valid access token triggers that redirect, so you can complete OAuth sign-in when your session has expired. - **Frequent re-login.** You no longer have to sign in multiple times a day. When the access token expires after 15 minutes of inactivity, the app now automatically refreshes it using your refresh token on the next page load, so you stay logged in for up to 30 days. - **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA. +- **More accurate visitor tracking.** We fixed rare edge cases where visitor counts could be slightly off during busy traffic spikes. Previously, the timestamp-based session ID generation could occasionally create overlapping identifiers. Every visitor now gets a truly unique UUID that never overlaps with others, ensuring your analytics are always precise. ## [0.11.1-alpha] - 2026-02-23 From b4b1348a94c56eeca04845631bdddd7442a0fe49 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 11:52:20 +0100 Subject: [PATCH 12/16] chore: update CHANGELOG.md to include improvements in authentication flow, addressing CSRF handling and cookie management for seamless sign-in and enhanced security --- CHANGELOG.md | 1 + app/actions/auth.ts | 14 ++++++++++ app/api/auth/refresh/route.ts | 15 +++++++++++ lib/api/client.ts | 50 +++++++++++++++++++++++++++++++++-- 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56ed5d7..810dd0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed +- **Seamless sign-in from Auth.** When you click "Sign in" on Pulse and complete authentication in the Ciphera Auth portal, you now return to Pulse fully authenticated without any loading loops or errors. We fixed CSRF handling and cookie forwarding issues that were causing 403 errors after OAuth callback, so the transition between apps is now smooth and reliable. - **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, stale refresh cookies caused the middleware to redirect away from the login page; now only a valid access token triggers that redirect, so you can complete OAuth sign-in when your session has expired. - **Frequent re-login.** You no longer have to sign in multiple times a day. When the access token expires after 15 minutes of inactivity, the app now automatically refreshes it using your refresh token on the next page load, so you stay logged in for up to 30 days. - **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA. diff --git a/app/actions/auth.ts b/app/actions/auth.ts index dc38c5f..5e48d13 100644 --- a/app/actions/auth.ts +++ b/app/actions/auth.ts @@ -91,6 +91,20 @@ export async function exchangeAuthCode(code: string, codeVerifier: string | null maxAge: 60 * 60 * 24 * 30 // 30 days }) + // * Note: CSRF token should be set by Auth API login flow and available via cookie + // * If the Auth API returns a CSRF token in header, we forward it + const csrfToken = res.headers.get('X-CSRF-Token') + if (csrfToken) { + cookieStore.set('csrf_token', csrfToken, { + httpOnly: false, // * Must be readable by JS for CSRF protection + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + domain: cookieDomain, + maxAge: 60 * 60 * 24 * 30 + }) + } + return { success: true, user: { diff --git a/app/api/auth/refresh/route.ts b/app/api/auth/refresh/route.ts index 09894a3..1731599 100644 --- a/app/api/auth/refresh/route.ts +++ b/app/api/auth/refresh/route.ts @@ -37,6 +37,9 @@ export async function POST() { const data = await res.json() + // * Get CSRF token from Auth API response header (for cookie rotation) + const csrfToken = res.headers.get('X-CSRF-Token') + cookieStore.set('access_token', data.access_token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', @@ -55,6 +58,18 @@ export async function POST() { maxAge: 60 * 60 * 24 * 30 }) + // * Set/update CSRF token cookie (non-httpOnly, for JS access) + if (csrfToken) { + cookieStore.set('csrf_token', csrfToken, { + httpOnly: false, // * Must be readable by JS for CSRF protection + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + path: '/', + domain: cookieDomain, + maxAge: 60 * 60 * 24 * 30 + }) + } + return NextResponse.json({ success: true, access_token: data.access_token }) } catch (error) { return NextResponse.json({ error: 'Internal error' }, { status: 500 }) diff --git a/lib/api/client.ts b/lib/api/client.ts index 506a5d5..389462f 100644 --- a/lib/api/client.ts +++ b/lib/api/client.ts @@ -22,6 +22,36 @@ export function getSignupUrl(redirectPath = '/auth/callback') { return `${AUTH_URL}/signup?client_id=pulse-app&redirect_uri=${redirectUri}&response_type=code` } +// * ============================================================================ +// * CSRF Token Handling +// * ============================================================================ + +/** + * Get CSRF token from the csrf_token cookie (non-httpOnly) + * This is needed for state-changing requests to the Auth API + */ +function getCSRFToken(): string | null { + if (typeof document === 'undefined') return null + + const cookies = document.cookie.split(';') + for (const cookie of cookies) { + const [name, value] = cookie.trim().split('=') + if (name === 'csrf_token') { + return decodeURIComponent(value) + } + } + return null +} + +/** + * Check if a request method requires CSRF protection + * State-changing methods (POST, PUT, DELETE, PATCH) need CSRF tokens + */ +function isStateChangingMethod(method: string): boolean { + const stateChangingMethods = ['POST', 'PUT', 'DELETE', 'PATCH'] + return stateChangingMethods.includes(method.toUpperCase()) +} + export class ApiError extends Error { status: number data?: Record @@ -150,13 +180,29 @@ async function apiRequest( ? `${baseUrl}${endpoint}` : `${baseUrl}/api/v1${endpoint}` - const headers: HeadersInit = { + const headers: Record = { 'Content-Type': 'application/json', - ...options.headers, + } + + // * Merge any additional headers from options + if (options.headers) { + const additionalHeaders = options.headers as Record + Object.entries(additionalHeaders).forEach(([key, value]) => { + headers[key] = value + }) } // * We rely on HttpOnly cookies, so no manual Authorization header injection. // * We MUST set credentials: 'include' for the browser to send cookies cross-origin (or same-site). + + // * Add CSRF token for state-changing requests to Auth API + // * Auth API uses Double Submit Cookie pattern for CSRF protection + if (isAuthRequest && isStateChangingMethod(method)) { + const csrfToken = getCSRFToken() + if (csrfToken) { + headers['X-CSRF-Token'] = csrfToken + } + } const controller = new AbortController() const timeoutId = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) From e5ad4cf2f664221901f76c41fde38e75985a3c56 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 12:05:49 +0100 Subject: [PATCH 13/16] chore: update CHANGELOG.md to reflect improvements in authentication flow, including seamless sign-in from the Ciphera portal and enhanced cookie management for better security and user experience --- CHANGELOG.md | 4 ++-- app/actions/auth.ts | 40 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 810dd0b..42288dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,8 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed -- **Seamless sign-in from Auth.** When you click "Sign in" on Pulse and complete authentication in the Ciphera Auth portal, you now return to Pulse fully authenticated without any loading loops or errors. We fixed CSRF handling and cookie forwarding issues that were causing 403 errors after OAuth callback, so the transition between apps is now smooth and reliable. -- **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, stale refresh cookies caused the middleware to redirect away from the login page; now only a valid access token triggers that redirect, so you can complete OAuth sign-in when your session has expired. +- **Seamless sign-in from the Ciphera portal.** When you click "Sign in" on Pulse and authenticate through the Ciphera Auth portal, you now return to Pulse fully logged in without any loading loops or error messages. Previously, you might see console errors or have to sign in twice—now the handoff between apps is smooth and reliable. +- **Sign in after inactivity.** Clicking "Sign in" after a period of inactivity no longer does nothing. Previously, you could get stuck in a redirect loop and never reach the login page; now you can always complete sign-in, even when your session has expired. - **Frequent re-login.** You no longer have to sign in multiple times a day. When the access token expires after 15 minutes of inactivity, the app now automatically refreshes it using your refresh token on the next page load, so you stay logged in for up to 30 days. - **2FA disable now requires password confirmation.** Disabling 2FA sends the derived password to the backend for verification. This prevents an attacker with a hijacked session from stripping 2FA. - **More accurate visitor tracking.** We fixed rare edge cases where visitor counts could be slightly off during busy traffic spikes. Previously, the timestamp-based session ID generation could occasionally create overlapping identifiers. Every visitor now gets a truly unique UUID that never overlaps with others, ensuring your analytics are always precise. diff --git a/app/actions/auth.ts b/app/actions/auth.ts index 5e48d13..c412103 100644 --- a/app/actions/auth.ts +++ b/app/actions/auth.ts @@ -35,11 +35,15 @@ export type AuthExchangeErrorType = 'network' | 'expired' | 'invalid' | 'server' export async function exchangeAuthCode(code: string, codeVerifier: string | null, redirectUri: string) { try { + // * IMPORTANT: credentials: 'include' is required to receive httpOnly cookies from Auth API + // * The Auth API sets access_token, refresh_token, and csrf_token as httpOnly cookies + // * We must forward these to the browser for cross-subdomain auth to work const res = await fetch(`${AUTH_API_URL}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, + credentials: 'include', // * Critical: receives httpOnly cookies from Auth API body: JSON.stringify({ grant_type: 'authorization_code', code, @@ -91,10 +95,40 @@ export async function exchangeAuthCode(code: string, codeVerifier: string | null maxAge: 60 * 60 * 24 * 30 // 30 days }) - // * Note: CSRF token should be set by Auth API login flow and available via cookie - // * If the Auth API returns a CSRF token in header, we forward it + // * Forward cookies from Auth API response to browser + // * The Auth API sets httpOnly cookies on auth.ciphera.net - we need to mirror them on pulse.ciphera.net + const setCookieHeaders = res.headers.getSetCookie() + if (setCookieHeaders && setCookieHeaders.length > 0) { + for (const cookieStr of setCookieHeaders) { + // * Parse Set-Cookie header (format: name=value; attributes...) + const [nameValue] = cookieStr.split(';') + const [name, value] = nameValue.trim().split('=') + + if (name && value) { + // * Determine if httpOnly (default true for security) + const isHttpOnly = cookieStr.toLowerCase().includes('httponly') + // * Determine sameSite (default lax) + const sameSiteMatch = cookieStr.match(/samesite=(\w+)/i) + const sameSite = (sameSiteMatch?.[1]?.toLowerCase() as 'strict' | 'lax' | 'none') || 'lax' + // * Extract max-age if present + const maxAgeMatch = cookieStr.match(/max-age=(\d+)/i) + const maxAge = maxAgeMatch ? parseInt(maxAgeMatch[1], 10) : 60 * 60 * 24 * 30 + + cookieStore.set(name.trim(), decodeURIComponent(value.trim()), { + httpOnly: isHttpOnly, + secure: process.env.NODE_ENV === 'production', + sameSite: sameSite, + path: '/', + domain: cookieDomain, + maxAge: maxAge + }) + } + } + } + + // * Also check for CSRF token in response header (fallback) const csrfToken = res.headers.get('X-CSRF-Token') - if (csrfToken) { + if (csrfToken && !cookieStore.get('csrf_token')) { cookieStore.set('csrf_token', csrfToken, { httpOnly: false, // * Must be readable by JS for CSRF protection secure: process.env.NODE_ENV === 'production', From 908b8c09009751fd9879d3a0f3d047a8e02c1aef Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 12:50:05 +0100 Subject: [PATCH 14/16] chore: update CHANGELOG.md to include the addition of an App Switcher in the User Menu for easier navigation between Ciphera products, along with dependency updates for @ciphera-net/ui --- CHANGELOG.md | 1 + app/layout-content.tsx | 32 +++++++++++++++++++++++++++++++- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42288dd..41d1fc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **App Switcher in User Menu.** Click your profile in the top right and you'll now see a "Ciphera Apps" section. Expand it to quickly jump between Pulse, Drop (file sharing), and your Ciphera Account settings. This makes it easier to discover and navigate between Ciphera products without signing in again. - **Faster billing page loading.** Your subscription details now load much quicker when you visit the billing page. Previously, several requests to our payment provider were made one after another, which could add 1-2 seconds to the page load. Now these happen simultaneously, cutting the wait time significantly. If any request takes too long, we gracefully continue so you always see your billing information without frustrating delays. - **Faster funnel analysis for multi-step conversions.** We've significantly improved how conversion funnels are calculated. Instead of scanning your data multiple times for each step in a funnel, we now do it in a single efficient pass. This means complex funnels with multiple steps load almost instantly instead of taking seconds—or even timing out. We've also added a reasonable limit of 5 steps per funnel to ensure optimal performance. - **More reliable database connections under heavy load.** We've optimized how Pulse manages its database connections to handle much higher traffic without issues. By increasing the connection pool size and improving how connections are reused, your dashboard stays responsive even when thousands of users are viewing analytics simultaneously. We also added better monitoring so we can detect and address connection issues before they affect you. diff --git a/app/layout-content.tsx b/app/layout-content.tsx index 727254f..8e5c48a 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -2,7 +2,7 @@ import { OfflineBanner } from '@/components/OfflineBanner' import { Footer } from '@/components/Footer' -import { Header } from '@ciphera-net/ui' +import { Header, type CipheraApp } from '@ciphera-net/ui' import NotificationCenter from '@/components/notifications/NotificationCenter' import { useAuth } from '@/lib/auth/context' import { useOnlineStatus } from '@/lib/hooks/useOnlineStatus' @@ -16,6 +16,34 @@ import { useRouter } from 'next/navigation' const ORG_SWITCH_KEY = 'pulse_switching_org' +// * Available Ciphera apps for the app switcher +const CIPHERA_APPS: CipheraApp[] = [ + { + id: 'pulse', + name: 'Pulse', + description: 'Your current app — Privacy-first analytics', + icon: '/pulse_icon_no_margins.png', + href: 'https://pulse.ciphera.net', + isAvailable: false, // * Current app + }, + { + id: 'drop', + name: 'Drop', + description: 'Secure file sharing', + icon: '/drop_icon_no_margins.png', + href: 'https://drop.ciphera.net', + isAvailable: true, + }, + { + id: 'auth', + name: 'Ciphera Account', + description: 'Manage your account settings', + icon: '/auth_icon_no_margins.png', + href: 'https://auth.ciphera.net', + isAvailable: true, + }, +] + export default function LayoutContent({ children }: { children: React.ReactNode }) { const auth = useAuth() const router = useRouter() @@ -87,6 +115,8 @@ export default function LayoutContent({ children }: { children: React.ReactNode showPricing={true} topOffset={showOfflineBar ? `${barHeightRem}rem` : undefined} rightSideActions={auth.user ? : null} + apps={CIPHERA_APPS} + currentAppId="pulse" customNavItems={ <> {!auth.user && ( diff --git a/package-lock.json b/package-lock.json index 1d89a05..7472ae8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.64", + "@ciphera-net/ui": "^0.0.66", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@simplewebauthn/browser": "^13.2.2", @@ -1543,9 +1543,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.64", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.64/1630605518a705ba9e74f003b9c66646bcc699ac", - "integrity": "sha512-xY+yALuCqWtsH78t6xmy2JQnQ8WFSlihElHnetFr6GQp9mOTCA5rlQq+a8hyg4xW7uXtIbKmPBxVnH5TlH9lBQ==", + "version": "0.0.66", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.66/a70ac6be09bae9522063f0a741f711b189540dd4", + "integrity": "sha512-Ui8ehDiOkceiojoM4UULXDE12QrZCWDQYyhylncw1+AyyfyA3mdBsJiwiRtiHiOqDHFuckuDEnFSLOMozwFIJg==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 0463c28..1e42bc6 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.64", + "@ciphera-net/ui": "^0.0.66", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@simplewebauthn/browser": "^13.2.2", From f933c2fb71fc8447e4045027052dd47c9f05b1a4 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 13:02:30 +0100 Subject: [PATCH 15/16] chore: update @ciphera-net/ui dependency to version 0.0.68 and update icon URLs in layout-content.tsx for improved asset management --- app/layout-content.tsx | 10 +++++----- package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/layout-content.tsx b/app/layout-content.tsx index 8e5c48a..be8772c 100644 --- a/app/layout-content.tsx +++ b/app/layout-content.tsx @@ -22,7 +22,7 @@ const CIPHERA_APPS: CipheraApp[] = [ id: 'pulse', name: 'Pulse', description: 'Your current app — Privacy-first analytics', - icon: '/pulse_icon_no_margins.png', + icon: 'https://ciphera.net/pulse_icon_no_margins.png', href: 'https://pulse.ciphera.net', isAvailable: false, // * Current app }, @@ -30,15 +30,15 @@ const CIPHERA_APPS: CipheraApp[] = [ id: 'drop', name: 'Drop', description: 'Secure file sharing', - icon: '/drop_icon_no_margins.png', + icon: 'https://ciphera.net/drop_icon_no_margins.png', href: 'https://drop.ciphera.net', isAvailable: true, }, { id: 'auth', - name: 'Ciphera Account', - description: 'Manage your account settings', - icon: '/auth_icon_no_margins.png', + name: 'Auth', + description: 'Your Ciphera account settings', + icon: 'https://ciphera.net/auth_icon_no_margins.png', href: 'https://auth.ciphera.net', isAvailable: true, }, diff --git a/package-lock.json b/package-lock.json index 7472ae8..fb9ac84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.66", + "@ciphera-net/ui": "^0.0.68", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@simplewebauthn/browser": "^13.2.2", @@ -1543,9 +1543,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.66", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.66/a70ac6be09bae9522063f0a741f711b189540dd4", - "integrity": "sha512-Ui8ehDiOkceiojoM4UULXDE12QrZCWDQYyhylncw1+AyyfyA3mdBsJiwiRtiHiOqDHFuckuDEnFSLOMozwFIJg==", + "version": "0.0.68", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.68/4c31bce4d7686b305680bfc5c86c78fc7ea9467d", + "integrity": "sha512-QO9H4vhKAJFtH0DTMhEhI2Q3nedy7JUK4jFRV9ofZTRAF2JCrYx53eEIiGInQmaTD4bMR649EE6bpz8hLknqtw==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 1e42bc6..2f13cd1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.66", + "@ciphera-net/ui": "^0.0.68", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@simplewebauthn/browser": "^13.2.2", From 052c49ace2aec17b3f379b24d72aaad3e1838251 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Fri, 27 Feb 2026 13:22:36 +0100 Subject: [PATCH 16/16] chore: update @ciphera-net/ui dependency to version 0.0.69 in package.json and package-lock.json --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fb9ac84..030a969 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "pulse-frontend", "version": "0.11.1-alpha", "dependencies": { - "@ciphera-net/ui": "^0.0.68", + "@ciphera-net/ui": "^0.0.69", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@simplewebauthn/browser": "^13.2.2", @@ -1543,9 +1543,9 @@ } }, "node_modules/@ciphera-net/ui": { - "version": "0.0.68", - "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.68/4c31bce4d7686b305680bfc5c86c78fc7ea9467d", - "integrity": "sha512-QO9H4vhKAJFtH0DTMhEhI2Q3nedy7JUK4jFRV9ofZTRAF2JCrYx53eEIiGInQmaTD4bMR649EE6bpz8hLknqtw==", + "version": "0.0.69", + "resolved": "https://npm.pkg.github.com/download/@ciphera-net/ui/0.0.69/f4bdafba179e509c05209a984770b262bb1a8331", + "integrity": "sha512-ERx6Qs4A+igzNSN5FwkLqZlsnorh9wM9P9SrdyAeMAlf9Dxvwwjvu1vWM6NApEL4oVfRdSHswhdtrcK/PQIy0g==", "dependencies": { "@radix-ui/react-icons": "^1.3.0", "clsx": "^2.1.0", diff --git a/package.json b/package.json index 2f13cd1..6cc163e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "type-check": "tsc --noEmit" }, "dependencies": { - "@ciphera-net/ui": "^0.0.68", + "@ciphera-net/ui": "^0.0.69", "@ducanh2912/next-pwa": "^10.2.9", "@radix-ui/react-icons": "^1.3.0", "@simplewebauthn/browser": "^13.2.2",