diff --git a/AppImage/ProxMenux-1.2.2.2-beta.AppImage b/AppImage/ProxMenux-1.2.2.2-beta.AppImage index f2919c27..9cd95dc1 100755 Binary files a/AppImage/ProxMenux-1.2.2.2-beta.AppImage and b/AppImage/ProxMenux-1.2.2.2-beta.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index 3f64fe6e..7f2aaaf1 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -eae86502621e2ebb7d021b123d8161195e8c8e6441cf1a0e1c752fb206420e59 ProxMenux-1.2.2.2-beta.AppImage +30c427079c3b2b829ece1b801f045c76b8dd7d16286b5ce243f56a5df9928eb8 ProxMenux-1.2.2.2-beta.AppImage diff --git a/AppImage/app/layout.tsx b/AppImage/app/layout.tsx index 1f13d3e9..2dd4be27 100644 --- a/AppImage/app/layout.tsx +++ b/AppImage/app/layout.tsx @@ -3,6 +3,7 @@ import type { Metadata, Viewport } from "next" import { GeistSans } from "geist/font/sans" import { GeistMono } from "geist/font/mono" import { ThemeProvider } from "../components/theme-provider" +import { PwaRegister } from "../components/pwa-register" import { Suspense } from "react" import "./globals.css" @@ -46,6 +47,7 @@ export default function RootLayout({ {children} + ) diff --git a/AppImage/components/pwa-register.tsx b/AppImage/components/pwa-register.tsx new file mode 100644 index 00000000..94196a59 --- /dev/null +++ b/AppImage/components/pwa-register.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useEffect } from "react" + +// =========================================================== +// PwaRegister +// =========================================================== +// Registers /sw.js once on mount. Chrome (Android) only +// surfaces the "Install app" prompt when a service worker +// is active, so even though the SW itself does nothing +// (network-only, no caching), its mere presence flips the +// PWA-installability check from no-to-yes. +// +// Mounted from app/layout.tsx so it runs on every route. +// =========================================================== +export function PwaRegister() { + useEffect(() => { + if (typeof window === "undefined") return + if (!("serviceWorker" in navigator)) return + // Wait for the load event to avoid competing with the + // initial render — the SW registration is not on the + // critical render path. + const register = () => { + navigator.serviceWorker + .register("/sw.js", { scope: "/" }) + .catch((err) => { + // Surface the failure only in DevTools — silent in prod. + console.warn("[pwa] service worker registration failed:", err) + }) + } + if (document.readyState === "complete") { + register() + } else { + window.addEventListener("load", register, { once: true }) + return () => window.removeEventListener("load", register) + } + }, []) + return null +} diff --git a/AppImage/public/icons/icon-192.png b/AppImage/public/icons/icon-192.png new file mode 100644 index 00000000..45e42f44 Binary files /dev/null and b/AppImage/public/icons/icon-192.png differ diff --git a/AppImage/public/icons/icon-512.png b/AppImage/public/icons/icon-512.png new file mode 100644 index 00000000..6d2513d1 Binary files /dev/null and b/AppImage/public/icons/icon-512.png differ diff --git a/AppImage/public/icons/icon-maskable-512.png b/AppImage/public/icons/icon-maskable-512.png new file mode 100644 index 00000000..eba5600a Binary files /dev/null and b/AppImage/public/icons/icon-maskable-512.png differ diff --git a/AppImage/public/logo.png b/AppImage/public/logo.png new file mode 100644 index 00000000..905d971a Binary files /dev/null and b/AppImage/public/logo.png differ diff --git a/AppImage/public/manifest.json b/AppImage/public/manifest.json index e033acf7..bd2e5aec 100644 --- a/AppImage/public/manifest.json +++ b/AppImage/public/manifest.json @@ -3,14 +3,29 @@ "short_name": "ProxMenux", "description": "Proxmox System Dashboard and Monitor", "start_url": "/", + "scope": "/", "display": "standalone", + "orientation": "any", "background_color": "#2b2f36", "theme_color": "#2b2f36", "icons": [ { - "src": "/images/proxmenux-logo.png", - "sizes": "256x256", - "type": "image/png" + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/icons/icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ] } diff --git a/AppImage/public/sw.js b/AppImage/public/sw.js new file mode 100644 index 00000000..324873bf --- /dev/null +++ b/AppImage/public/sw.js @@ -0,0 +1,40 @@ +// ========================================================== +// ProxMenux Monitor — Service Worker +// ========================================================== +// Minimal SW whose only job is to make Chrome (Android) treat +// the Monitor as an installable PWA. The Monitor lives on the +// operator's LAN, hits a self-signed HTTPS endpoint and has +// ZERO offline value (every page calls /api/* over the wire), +// so we do NOT cache pages or API responses — caching them +// would only cause stale-data bugs after an AppImage update. +// +// The install / activate handlers clean up old SW caches from +// previous Monitor versions so a beta-to-stable upgrade never +// strands the browser on a cached old shell. +// ========================================================== + +const SW_VERSION = 'proxmenux-monitor-v1'; + +self.addEventListener('install', (event) => { + // Take over as soon as installed; no skipWaiting handshake. + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil((async () => { + // Wipe any cache name that isn't ours — survives renames / + // version bumps without piling up stale entries. + const names = await caches.keys(); + await Promise.all( + names.filter((n) => n !== SW_VERSION).map((n) => caches.delete(n)) + ); + await self.clients.claim(); + })()); +}); + +// Network-only fetch. The SW exists so Chrome marks the site as +// installable; we deliberately do not serve cached responses. +self.addEventListener('fetch', (event) => { + // Let the browser handle it normally — no respondWith → no cache. + return; +});