v0.0.10: Progressive Web App (PWA)

- Web manifest (standalone mode, theme, icon)
- Service worker: caches shell (HTML, WASM, icon) for offline
- SVG app icon (chat bubble with encryption indicator)
- iOS meta tags: apple-mobile-web-app-capable, status bar style
- Android: beforeinstallprompt → /install command
- Offline fallback: loads cached shell, shows reconnecting state
- Cache versioning with automatic old cache cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 12:32:59 +04:00
parent 4fb3973403
commit 9811248b7c
3 changed files with 99 additions and 7 deletions

10
warzone/Cargo.lock generated
View File

@@ -2647,7 +2647,7 @@ dependencies = [
[[package]]
name = "warzone-client"
version = "0.0.9"
version = "0.0.10"
dependencies = [
"anyhow",
"argon2",
@@ -2680,7 +2680,7 @@ dependencies = [
[[package]]
name = "warzone-mule"
version = "0.0.9"
version = "0.0.10"
dependencies = [
"anyhow",
"clap",
@@ -2689,7 +2689,7 @@ dependencies = [
[[package]]
name = "warzone-protocol"
version = "0.0.9"
version = "0.0.10"
dependencies = [
"base64",
"bincode",
@@ -2712,7 +2712,7 @@ dependencies = [
[[package]]
name = "warzone-server"
version = "0.0.9"
version = "0.0.10"
dependencies = [
"anyhow",
"axum",
@@ -2739,7 +2739,7 @@ dependencies = [
[[package]]
name = "warzone-wasm"
version = "0.0.9"
version = "0.0.10"
dependencies = [
"base64",
"bincode",

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "0.0.9"
version = "0.0.10"
edition = "2021"
license = "MIT"
rust-version = "1.75"

View File

@@ -12,6 +12,9 @@ pub fn routes() -> Router<AppState> {
.route("/", get(web_ui))
.route("/wasm/warzone_wasm.js", get(wasm_js))
.route("/wasm/warzone_wasm_bg.wasm", get(wasm_binary))
.route("/manifest.json", get(pwa_manifest))
.route("/sw.js", get(service_worker))
.route("/icon.svg", get(pwa_icon))
}
async fn web_ui() -> Html<&'static str> {
@@ -32,12 +35,85 @@ async fn wasm_binary() -> impl IntoResponse {
)
}
async fn pwa_manifest() -> impl IntoResponse {
([(header::CONTENT_TYPE, "application/manifest+json")], r##"{
"name": "Warzone Messenger",
"short_name": "Warzone",
"description": "End-to-end encrypted messenger",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a1a",
"theme_color": "#0a0a1a",
"icons": [{"src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable"}]
}"##)
}
async fn service_worker() -> impl IntoResponse {
([(header::CONTENT_TYPE, "application/javascript")], r##"
const CACHE = 'wz-v1';
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL)));
self.skipWaiting();
});
self.addEventListener('activate', e => {
e.waitUntil(caches.keys().then(keys =>
Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
));
e.waitUntil(clients.claim());
});
self.addEventListener('fetch', e => {
const url = new URL(e.request.url);
// API calls: network only
if (url.pathname.startsWith('/v1/')) return;
// WS: skip
if (url.protocol === 'ws:' || url.protocol === 'wss:') return;
// Shell: cache first, network fallback
e.respondWith(
caches.match(e.request).then(cached => cached || fetch(e.request).then(resp => {
if (resp.ok && SHELL.includes(url.pathname)) {
const clone = resp.clone();
caches.open(CACHE).then(c => c.put(e.request, clone));
}
return resp;
}).catch(() => {
if (e.request.mode === 'navigate') {
return caches.match('/');
}
}))
);
});
"##)
}
async fn pwa_icon() -> impl IntoResponse {
([(header::CONTENT_TYPE, "image/svg+xml")], r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="100" fill="#0a0a1a"/>
<path d="M128 140h256c22 0 40 18 40 40v152c0 22-18 40-40 40H210l-70 52v-52h-12c-22 0-40-18-40-40V180c0-22 18-40 40-40z" fill="#e94560"/>
<circle cx="200" cy="256" r="18" fill="#0a0a1a"/>
<circle cx="256" cy="256" r="18" fill="#0a0a1a"/>
<circle cx="312" cy="256" r="18" fill="#0a0a1a"/>
<rect x="180" y="340" width="30" height="8" rx="4" fill="#4ade80"/>
<rect x="220" y="340" width="30" height="8" rx="4" fill="#4ade80"/>
</svg>"##)
}
const WEB_HTML: &str = r##"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover">
<meta name="theme-color" content="#0a0a1a">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Warzone">
<meta name="mobile-web-app-capable" content="yes">
<link rel="manifest" href="/manifest.json">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/icon.svg">
<title>Warzone</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
@@ -160,7 +236,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection
let wasmReady = false;
const VERSION = '0.0.9';
const VERSION = '0.0.10';
let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ──
@@ -684,6 +760,11 @@ async function doSend() {
return;
}
if (text === '/clear') { $messages.innerHTML = ''; return; }
if (text === '/install') {
if (deferredInstall) { deferredInstall.prompt(); deferredInstall = null; }
else addSys('Install not available (already installed or not supported)');
return;
}
if (text === '/debug') { DEBUG = !DEBUG; addSys('Debug logging: ' + (DEBUG ? 'ON (check browser console)' : 'OFF')); return; }
if (text === '/reset') {
localStorage.clear();
@@ -812,6 +893,17 @@ document.getElementById('btn-recover').onclick = () => doRecover();
document.getElementById('btn-enter').onclick = () => enterChat();
document.getElementById('send-btn').onclick = () => doSend();
// PWA: service worker + install prompt
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js').catch(e => dbg('SW failed:', e));
}
let deferredInstall = null;
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault();
deferredInstall = e;
addSys('Tip: install as app for fullscreen + notifications. Type /install');
});
// Initialize WASM and auto-load
(async function() {
try {