Files
openclaw/ui/index.html
eduardocruz 15437b1153 feat: add PWA support and Web Push notifications to Control UI
Make the Control UI installable as a Progressive Web App and add Web Push
notification infrastructure so the gateway can send browser notifications
to subscribed clients.

PWA shell:
- Add manifest.webmanifest with app name, icons, standalone display
- Add service worker with cache-first for hashed assets, network-first
  for HTML/API, push event handler, and notification click focus
- Register service worker from main.ts
- Serve .webmanifest with correct content type from gateway
- Add worker-src 'self' to CSP

Web Push server (src/infra/push-web.ts):
- VAPID key auto-generation and persistence to state dir
- Subscription CRUD with async-locked atomic JSON storage
- Send and broadcast via web-push library
- Mirrors push-apns.ts patterns

Gateway RPC (4 new methods):
- push.web.vapidPublicKey: returns VAPID public key for PushManager
- push.web.subscribe: registers browser push subscription
- push.web.unsubscribe: removes subscription by endpoint
- push.web.test: broadcasts test notification to all subscribers

Client-side push manager (ui/src/ui/push-subscription.ts):
- Permission request, VAPID key fetch, PushManager subscribe/unsubscribe
- Gateway RPC wiring for registration and test

Settings UI:
- New "Notifications" virtual section in Communications tab
- Shows browser support, permission state, subscription status
- Subscribe/unsubscribe/test buttons

Includes 10 unit tests for push-web subscription CRUD and VAPID keys.
2026-04-25 04:40:01 -05:00

71 lines
2.5 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenClaw Control</title>
<meta name="color-scheme" content="dark light" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="manifest.webmanifest" />
<script>
(function () {
var THEMES = { claw: 1, knot: 1, dash: 1 };
var MODES = { system: 1, light: 1, dark: 1 };
var LEGACY = {
dark: "claw:dark",
light: "claw:light",
openknot: "knot:dark",
fieldmanual: "dash:dark",
clawdash: "dash:light",
system: "claw:system",
};
try {
var keys = Object.keys(localStorage);
var raw;
for (var i = 0; i < keys.length; i++) {
if (keys[i].indexOf("openclaw.control.settings.v1") === 0) {
raw = localStorage.getItem(keys[i]);
if (raw) break;
}
}
if (!raw) return;
var s = JSON.parse(raw);
var t = s && s.theme;
var m = s && s.themeMode;
if (typeof t !== "string") t = "";
if (typeof m !== "string") m = "";
var legacy = LEGACY[t];
var theme = THEMES[t] ? t : legacy ? legacy.split(":")[0] : "claw";
var mode = MODES[m] ? m : legacy ? legacy.split(":")[1] : "system";
if (mode === "system") {
mode = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
}
var resolved =
theme === "knot"
? mode === "light"
? "openknot-light"
: "openknot"
: theme === "dash"
? mode === "light"
? "dash-light"
: "dash"
: mode === "light"
? "light"
: "dark";
document.documentElement.setAttribute("data-theme", resolved);
document.documentElement.setAttribute(
"data-theme-mode",
resolved.indexOf("light") !== -1 ? "light" : "dark",
);
} catch (e) {}
})();
</script>
</head>
<body>
<openclaw-app></openclaw-app>
<script type="module" src="/src/main.ts"></script>
</body>
</html>