feat(pwa): make dashboard installable as a progressive web app
All checks were successful
CI / Clippy (push) Successful in 2m24s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Format (pull_request) Successful in 2s
CI / Format (push) Successful in 3s
CI / Clippy (pull_request) Successful in 2m19s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Deploy (push) Has been skipped
CI / Deploy (pull_request) Has been skipped
All checks were successful
CI / Clippy (push) Successful in 2m24s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Format (pull_request) Successful in 2s
CI / Format (push) Successful in 3s
CI / Clippy (pull_request) Successful in 2m19s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Deploy (push) Has been skipped
CI / Deploy (pull_request) Has been skipped
Add web manifest, service worker with cache-first static assets and network-first navigation, and register from the App component head. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
17
assets/manifest.json
Normal file
17
assets/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "CERTifAI Dashboard",
|
||||||
|
"short_name": "CERTifAI",
|
||||||
|
"description": "Self-hosted GenAI infrastructure management dashboard",
|
||||||
|
"start_url": "/dashboard",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#0f1117",
|
||||||
|
"theme_color": "#4B3FE0",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/assets/logo.svg",
|
||||||
|
"sizes": "any",
|
||||||
|
"type": "image/svg+xml",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
67
assets/sw.js
Normal file
67
assets/sw.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
// CERTifAI Service Worker — network-first with offline fallback cache.
|
||||||
|
// Static assets (CSS, JS, WASM, fonts) use cache-first for speed.
|
||||||
|
// API and navigation requests always try the network first.
|
||||||
|
|
||||||
|
const CACHE_NAME = "certifai-v1";
|
||||||
|
|
||||||
|
// Pre-cache the app shell on install
|
||||||
|
self.addEventListener("install", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) =>
|
||||||
|
cache.addAll([
|
||||||
|
"/",
|
||||||
|
"/dashboard",
|
||||||
|
"/assets/logo.svg",
|
||||||
|
"/assets/favicon.ico",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.skipWaiting();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clean up old caches on activate
|
||||||
|
self.addEventListener("activate", (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((keys) =>
|
||||||
|
Promise.all(
|
||||||
|
keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
self.clients.claim();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener("fetch", (event) => {
|
||||||
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
|
// Skip non-GET and API requests (let them go straight to network)
|
||||||
|
if (event.request.method !== "GET" || url.pathname.startsWith("/api/")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache-first for static assets (hashed filenames make this safe)
|
||||||
|
const isStatic = /\.(js|wasm|css|ico|svg|png|jpg|woff2?)(\?|$)/.test(url.pathname);
|
||||||
|
if (isStatic) {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((cached) =>
|
||||||
|
cached || fetch(event.request).then((resp) => {
|
||||||
|
const clone = resp.clone();
|
||||||
|
caches.open(CACHE_NAME).then((c) => c.put(event.request, clone));
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network-first for navigation / HTML
|
||||||
|
event.respondWith(
|
||||||
|
fetch(event.request)
|
||||||
|
.then((resp) => {
|
||||||
|
const clone = resp.clone();
|
||||||
|
caches.open(CACHE_NAME).then((c) => c.put(event.request, clone));
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
.catch(() => caches.match(event.request))
|
||||||
|
);
|
||||||
|
});
|
||||||
20
src/app.rs
20
src/app.rs
@@ -52,6 +52,7 @@ pub enum Route {
|
|||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
||||||
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
const TAILWIND_CSS: Asset = asset!("/assets/tailwind.css");
|
||||||
|
const MANIFEST: Asset = asset!("/assets/manifest.json");
|
||||||
|
|
||||||
/// Google Fonts URL for Inter (body) and Space Grotesk (headings).
|
/// Google Fonts URL for Inter (body) and Space Grotesk (headings).
|
||||||
const GOOGLE_FONTS: &str = "https://fonts.googleapis.com/css2?\
|
const GOOGLE_FONTS: &str = "https://fonts.googleapis.com/css2?\
|
||||||
@@ -64,6 +65,14 @@ const GOOGLE_FONTS: &str = "https://fonts.googleapis.com/css2?\
|
|||||||
pub fn App() -> Element {
|
pub fn App() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
document::Link { rel: "icon", href: FAVICON }
|
document::Link { rel: "icon", href: FAVICON }
|
||||||
|
document::Link { rel: "manifest", href: MANIFEST }
|
||||||
|
document::Meta { name: "theme-color", content: "#4B3FE0" }
|
||||||
|
document::Meta { name: "apple-mobile-web-app-capable", content: "yes" }
|
||||||
|
document::Meta {
|
||||||
|
name: "apple-mobile-web-app-status-bar-style",
|
||||||
|
content: "black-translucent",
|
||||||
|
}
|
||||||
|
document::Link { rel: "apple-touch-icon", href: FAVICON }
|
||||||
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
|
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
|
||||||
document::Link {
|
document::Link {
|
||||||
rel: "preconnect",
|
rel: "preconnect",
|
||||||
@@ -73,6 +82,17 @@ pub fn App() -> Element {
|
|||||||
document::Link { rel: "stylesheet", href: GOOGLE_FONTS }
|
document::Link { rel: "stylesheet", href: GOOGLE_FONTS }
|
||||||
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
document::Link { rel: "stylesheet", href: TAILWIND_CSS }
|
||||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||||
|
|
||||||
|
// Register PWA service worker
|
||||||
|
document::Script {
|
||||||
|
r#"
|
||||||
|
if ('serviceWorker' in navigator) {{
|
||||||
|
navigator.serviceWorker.register('/assets/sw.js')
|
||||||
|
.catch(function(e) {{ console.warn('SW registration failed:', e); }});
|
||||||
|
}}
|
||||||
|
"#
|
||||||
|
}
|
||||||
|
|
||||||
div { "data-theme": "certifai-dark", Router::<Route> {} }
|
div { "data-theme": "certifai-dark", Router::<Route> {} }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user