diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 3b7a528..2d17bb0 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -28,6 +28,8 @@ export function buildApp() { "img-src": ["'self'", "data:"], "connect-src": ["'self'", "wss://relay.primal.net"], "font-src": ["'self'", "data:"], + "manifest-src": ["'self'"], + "worker-src": ["'self'"], "frame-ancestors": ["'none'"], "upgrade-insecure-requests": null, }, @@ -67,10 +69,21 @@ export function buildApp() { if (staticDir && fs.existsSync(staticDir)) { logger.info({ staticDir }, "serving web assets"); - app.use(express.static(staticDir, { index: false, maxAge: "1h" })); + app.use( + express.static(staticDir, { + index: false, + maxAge: "1h", + setHeaders: (res, filePath) => { + if (filePath.endsWith("sw.js") || filePath.endsWith("manifest.webmanifest")) { + res.setHeader("Cache-Control", "no-cache"); + } + }, + }), + ); app.get(/.*/, (_req, res, next) => { const indexFile = path.join(staticDir, "index.html"); if (!fs.existsSync(indexFile)) return next(); + res.setHeader("Cache-Control", "no-cache"); res.sendFile(indexFile); }); } else { diff --git a/apps/web/index.html b/apps/web/index.html index 32bfe30..4d29fdf 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -4,6 +4,13 @@ + + + + + + + gashboard diff --git a/apps/web/public/icons/icon.svg b/apps/web/public/icons/icon.svg new file mode 100644 index 0000000..d6ec436 --- /dev/null +++ b/apps/web/public/icons/icon.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/apps/web/public/icons/maskable.svg b/apps/web/public/icons/maskable.svg new file mode 100644 index 0000000..242e9b7 --- /dev/null +++ b/apps/web/public/icons/maskable.svg @@ -0,0 +1,7 @@ + + + + + + G + diff --git a/apps/web/public/manifest.webmanifest b/apps/web/public/manifest.webmanifest new file mode 100644 index 0000000..a19024e --- /dev/null +++ b/apps/web/public/manifest.webmanifest @@ -0,0 +1,34 @@ +{ + "name": "gashboard", + "short_name": "gashboard", + "description": "Solo mining dashboard for Datum miners.", + "start_url": "/", + "scope": "/", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#07090f", + "theme_color": "#0a0e1a", + "categories": ["utilities", "finance"], + "icons": [ + { + "src": "/icons/icon.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "any" + }, + { + "src": "/icons/maskable.svg", + "sizes": "any", + "type": "image/svg+xml", + "purpose": "maskable" + } + ], + "shortcuts": [ + { + "name": "Graphs", + "short_name": "Graphs", + "url": "/graphs", + "description": "Open telemetry graphs" + } + ] +} diff --git a/apps/web/public/miners/avalon-mini-3.svg b/apps/web/public/miners/avalon-mini-3.svg new file mode 100644 index 0000000..a08c9ec --- /dev/null +++ b/apps/web/public/miners/avalon-mini-3.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/apps/web/public/miners/avalon-nano-3.svg b/apps/web/public/miners/avalon-nano-3.svg new file mode 100644 index 0000000..8689c6f --- /dev/null +++ b/apps/web/public/miners/avalon-nano-3.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/apps/web/public/miners/bitaxe.svg b/apps/web/public/miners/bitaxe.svg new file mode 100644 index 0000000..88731d6 --- /dev/null +++ b/apps/web/public/miners/bitaxe.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/public/miners/boomer-heater.svg b/apps/web/public/miners/boomer-heater.svg new file mode 100644 index 0000000..5db3115 --- /dev/null +++ b/apps/web/public/miners/boomer-heater.svg @@ -0,0 +1,12 @@ + + + + + + + + + NO HASHES. JUST HEAT. + + + diff --git a/apps/web/public/miners/nerdqaxe.svg b/apps/web/public/miners/nerdqaxe.svg new file mode 100644 index 0000000..8f0ac27 --- /dev/null +++ b/apps/web/public/miners/nerdqaxe.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/public/sw.js b/apps/web/public/sw.js new file mode 100644 index 0000000..81d351b --- /dev/null +++ b/apps/web/public/sw.js @@ -0,0 +1,74 @@ +const CACHE_VERSION = "gashboard-pwa-v1"; +const APP_SHELL = ["/", "/manifest.webmanifest", "/icons/icon.svg", "/icons/maskable.svg"]; +const MINER_ASSETS = [ + "/miners/nerdqaxe.svg", + "/miners/bitaxe.svg", + "/miners/avalon-nano-3.svg", + "/miners/avalon-mini-3.svg", + "/miners/boomer-heater.svg", +]; + +self.addEventListener("install", (event) => { + event.waitUntil(caches.open(CACHE_VERSION).then((cache) => cache.addAll([...APP_SHELL, ...MINER_ASSETS]))); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches + .keys() + .then((keys) => Promise.all(keys.filter((key) => key !== CACHE_VERSION).map((key) => caches.delete(key)))) + .then(() => self.clients.claim()), + ); +}); + +self.addEventListener("fetch", (event) => { + const req = event.request; + if (req.method !== "GET") return; + + const url = new URL(req.url); + if (url.origin !== self.location.origin) return; + if (url.pathname.startsWith("/api/") || url.pathname === "/healthz") return; + + if (req.mode === "navigate") { + event.respondWith(networkFirst(req, "/")); + return; + } + + if (url.pathname.startsWith("/assets/") || url.pathname.startsWith("/icons/")) { + event.respondWith(cacheFirst(req)); + return; + } + + event.respondWith(networkFirst(req)); +}); + +self.addEventListener("message", (event) => { + if (event.data?.type === "SKIP_WAITING") self.skipWaiting(); +}); + +async function cacheFirst(req) { + const cache = await caches.open(CACHE_VERSION); + const cached = await cache.match(req); + if (cached) return cached; + const res = await fetch(req); + if (res.ok) cache.put(req, res.clone()); + return res; +} + +async function networkFirst(req, fallbackPath) { + const cache = await caches.open(CACHE_VERSION); + try { + const res = await fetch(req); + if (res.ok) cache.put(req, res.clone()); + return res; + } catch { + const cached = await cache.match(req); + if (cached) return cached; + if (fallbackPath) { + const fallback = await cache.match(fallbackPath); + if (fallback) return fallback; + } + throw new Error("offline"); + } +} diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue index e641dfa..1c5e362 100644 --- a/apps/web/src/App.vue +++ b/apps/web/src/App.vue @@ -1,11 +1,20 @@