diff --git a/frontend/default.conf.template b/frontend/default.conf.template index e32f4fe..9da8b6a 100644 --- a/frontend/default.conf.template +++ b/frontend/default.conf.template @@ -1,4 +1,9 @@ server { + + types { + application/manifest+json webmanifest; + } + listen 80; server_name localhost; root /usr/share/nginx/html; @@ -13,6 +18,9 @@ server { # Handle client-side routing location / { + autoindex off; + expires off; + add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always; try_files $uri $uri/ /index.html; } @@ -26,7 +34,7 @@ server { } # Cache static assets - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { + location ~* \.(css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } diff --git a/frontend/src/components/PWAPrompts.tsx b/frontend/src/components/PWAPrompts.tsx deleted file mode 100644 index 08d1eba..0000000 --- a/frontend/src/components/PWAPrompts.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useEffect, useState } from "react"; -import { X, Download, RotateCcw } from "lucide-react"; - -interface BeforeInstallPromptEvent extends Event { - prompt(): Promise; - userChoice: Promise<{ outcome: "accepted" | "dismissed" }>; -} - -interface PWAPromptProps { - onInstall?: () => void; -} - -export function PWAInstallPrompt({ onInstall }: PWAPromptProps) { - const [deferredPrompt, setDeferredPrompt] = - useState(null); - const [showPrompt, setShowPrompt] = useState(false); - - useEffect(() => { - const handler = (e: Event) => { - // Prevent the mini-infobar from appearing on mobile - e.preventDefault(); - setDeferredPrompt(e as BeforeInstallPromptEvent); - setShowPrompt(true); - }; - - window.addEventListener("beforeinstallprompt", handler); - - return () => window.removeEventListener("beforeinstallprompt", handler); - }, []); - - const handleInstall = async () => { - if (!deferredPrompt) return; - - try { - await deferredPrompt.prompt(); - const { outcome } = await deferredPrompt.userChoice; - - if (outcome === "accepted") { - onInstall?.(); - } - - setDeferredPrompt(null); - setShowPrompt(false); - } catch (error) { - console.error("Error installing PWA:", error); - } - }; - - const handleDismiss = () => { - setShowPrompt(false); - setDeferredPrompt(null); - }; - - if (!showPrompt || !deferredPrompt) return null; - - return ( -
-
-
- -
-
-

- Install Leggen -

-

- Add to your home screen for quick access -

-
- -
-
- - -
-
- ); -} - -interface PWAUpdatePromptProps { - updateAvailable: boolean; - onUpdate: () => void; -} - -export function PWAUpdatePrompt({ - updateAvailable, - onUpdate, -}: PWAUpdatePromptProps) { - const [showPrompt, setShowPrompt] = useState(false); - - useEffect(() => { - if (updateAvailable) { - setShowPrompt(true); - } - }, [updateAvailable]); - - const handleUpdate = () => { - onUpdate(); - setShowPrompt(false); - }; - - const handleDismiss = () => { - setShowPrompt(false); - }; - - if (!showPrompt || !updateAvailable) return null; - - return ( -
-
-
- -
-
-

- Update Available -

-

- A new version of Leggen is ready to install -

-
- -
-
- - -
-
- ); -} diff --git a/frontend/src/hooks/usePWA.ts b/frontend/src/hooks/usePWA.ts deleted file mode 100644 index 8aac559..0000000 --- a/frontend/src/hooks/usePWA.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect, useState } from "react"; - -interface PWAUpdate { - updateAvailable: boolean; - updateSW: () => Promise; - forceReload: () => Promise; -} - -export function usePWA(): PWAUpdate { - const [updateAvailable, setUpdateAvailable] = useState(false); - const [updateSW, setUpdateSW] = useState<() => Promise>( - () => async () => {}, - ); - - const forceReload = async (): Promise => { - try { - // Clear all caches - if ("caches" in window) { - const cacheNames = await caches.keys(); - await Promise.all( - cacheNames.map((cacheName) => caches.delete(cacheName)), - ); - console.log("All caches cleared"); - } - - // Unregister service worker - if ("serviceWorker" in navigator) { - const registrations = await navigator.serviceWorker.getRegistrations(); - await Promise.all( - registrations.map((registration) => registration.unregister()), - ); - console.log("All service workers unregistered"); - } - - // Force reload - window.location.reload(); - } catch (error) { - console.error("Error during force reload:", error); - // Fallback: just reload the page - window.location.reload(); - } - }; - - useEffect(() => { - // Check if SW registration is available - if ("serviceWorker" in navigator) { - // Import the registerSW function - import("virtual:pwa-register") - .then(({ registerSW }) => { - const updateSWFunction = registerSW({ - onNeedRefresh() { - setUpdateAvailable(true); - setUpdateSW(() => updateSWFunction); - }, - onOfflineReady() { - console.log("App ready to work offline"); - }, - }); - }) - .catch(() => { - // PWA not available in development mode or when disabled - console.log("PWA registration not available"); - }); - } - }, []); - - return { - updateAvailable, - updateSW, - forceReload, - }; -} diff --git a/frontend/src/hooks/useVersionCheck.ts b/frontend/src/hooks/useVersionCheck.ts deleted file mode 100644 index 1354d2e..0000000 --- a/frontend/src/hooks/useVersionCheck.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect } from "react"; -import { useQuery } from "@tanstack/react-query"; -import { apiClient } from "../lib/api"; - -const VERSION_STORAGE_KEY = "leggen_app_version"; - -export function useVersionCheck(forceReload: () => Promise) { - const { data: healthStatus, isSuccess: healthSuccess } = useQuery({ - queryKey: ["health"], - queryFn: apiClient.getHealth, - refetchInterval: 30000, - retry: false, - staleTime: 0, // Always consider data stale to ensure fresh version checks - }); - - useEffect(() => { - if (healthSuccess && healthStatus?.version) { - const currentVersion = healthStatus.version; - const storedVersion = localStorage.getItem(VERSION_STORAGE_KEY); - - if (storedVersion && storedVersion !== currentVersion) { - console.log( - `Version mismatch detected: stored=${storedVersion}, current=${currentVersion}`, - ); - console.log("Clearing cache and reloading..."); - - // Update stored version first - localStorage.setItem(VERSION_STORAGE_KEY, currentVersion); - - // Force reload to clear cache - forceReload(); - } else if (!storedVersion) { - // First time loading, store the version - localStorage.setItem(VERSION_STORAGE_KEY, currentVersion); - console.log(`Version stored: ${currentVersion}`); - } - } - }, [healthSuccess, healthStatus?.version, forceReload]); -} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index a0de3a6..d991913 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,6 +5,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ThemeProvider } from "./contexts/ThemeContext"; import "./index.css"; import { routeTree } from "./routeTree.gen"; +import { registerSW } from "virtual:pwa-register"; const router = createRouter({ routeTree }); @@ -17,6 +18,57 @@ const queryClient = new QueryClient({ }, }); +const intervalMS = 60 * 60 * 1000; + +registerSW({ + onRegisteredSW(swUrl, r) { + console.log("[PWA] Service worker registered successfully"); + + if (r) { + setInterval(async () => { + console.log("[PWA] Checking for updates..."); + + if (r.installing) { + console.log("[PWA] Update already installing, skipping check"); + return; + } + + if (!navigator) { + console.log("[PWA] Navigator not available, skipping check"); + return; + } + + if ("connection" in navigator && !navigator.onLine) { + console.log("[PWA] Device is offline, skipping check"); + return; + } + + try { + const resp = await fetch(swUrl, { + cache: "no-store", + headers: { + cache: "no-store", + "cache-control": "no-cache", + }, + }); + + if (resp?.status === 200) { + console.log("[PWA] Update check successful, triggering update"); + await r.update(); + } else { + console.log(`[PWA] Update check returned status: ${resp?.status}`); + } + } catch (error) { + console.error("[PWA] Error checking for updates:", error); + } + }, intervalMS); + } + }, + onOfflineReady() { + console.log("[PWA] App ready to work offline"); + }, +}); + createRoot(document.getElementById("root")!).render( diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index f26821e..8399b35 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,31 +1,10 @@ import { createRootRoute, Outlet } from "@tanstack/react-router"; import { AppSidebar } from "../components/AppSidebar"; import { SiteHeader } from "../components/SiteHeader"; -import { PWAInstallPrompt, PWAUpdatePrompt } from "../components/PWAPrompts"; -import { usePWA } from "../hooks/usePWA"; -import { useVersionCheck } from "../hooks/useVersionCheck"; import { SidebarInset, SidebarProvider } from "../components/ui/sidebar"; import { Toaster } from "../components/ui/sonner"; function RootLayout() { - const { updateAvailable, updateSW, forceReload } = usePWA(); - - // Check for version mismatches and force reload if needed - useVersionCheck(forceReload); - - const handlePWAInstall = () => { - console.log("PWA installed successfully"); - }; - - const handlePWAUpdate = async () => { - try { - await updateSW(); - console.log("PWA updated successfully"); - } catch (error) { - console.error("Error updating PWA:", error); - } - }; - return ( - {/* PWA Prompts */} - - - {/* Toast Notifications */} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index efd3abb..b0ad211 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,10 +11,7 @@ export default defineConfig({ VitePWA({ registerType: "autoUpdate", includeAssets: [ - "favicon.ico", - "apple-touch-icon-180x180.png", - "maskable-icon-512x512.png", - "robots.txt", + "robots.txt" ], manifest: { name: "Leggen",