Notifications: staff turn them on per device from a new Notifications page, pick each type (bans, gags, mutes and silences added or lifted, server down and back up) and quiet hours. A watcher polls SimpleAdmin's tables, so in-game penalties are sent too. Web Push with VAPID and RFC 8291 encryption in internal/push, standard library only. Tables in sql/push.sql, created at startup; the VAPID key is made on first start and kept in saw_settings. Penalty notifications link to the public list searching for the player (#bans?q=<steamid64>). The Android app delegates notifications (versionCode 3). Caching: pages, scripts and styles are served with Cache-Control: no-cache and an ETag, so a deploy shows up at once instead of after Cloudflare's default 4-hour browser cache.
75 lines
2.9 KiB
JavaScript
75 lines
2.9 KiB
JavaScript
// The service worker lets the site be installed as an app, and shows staff notifications. It caches
|
|
// nothing but the offline page: every request still goes to the network, so nobody sees stale bans
|
|
// or ranks.
|
|
|
|
const CACHE = "offline-v1";
|
|
const OFFLINE = "/static/offline.html";
|
|
|
|
self.addEventListener("install", (e) => {
|
|
e.waitUntil(caches.open(CACHE).then((c) => c.add(OFFLINE)).then(() => self.skipWaiting()));
|
|
});
|
|
|
|
self.addEventListener("activate", (e) => {
|
|
e.waitUntil(
|
|
caches.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim()),
|
|
);
|
|
});
|
|
|
|
// Only page loads get the offline page; API calls fail as they always have, and the pages say so.
|
|
self.addEventListener("fetch", (e) => {
|
|
if (e.request.mode !== "navigate") return;
|
|
e.respondWith(fetch(e.request).catch(() => caches.match(OFFLINE)));
|
|
});
|
|
|
|
// Staff notifications (internal/web/notify.go). The panel sends title, body, icon, tag, url and
|
|
// actions; a notification with the same tag replaces the last one quietly.
|
|
self.addEventListener("push", (e) => {
|
|
let d = {};
|
|
try { d = e.data ? e.data.json() : {}; } catch { d = { body: e.data.text() }; }
|
|
e.waitUntil(self.registration.showNotification(d.title || "zio.sh", {
|
|
body: d.body || "",
|
|
icon: d.icon || "/static/icon-192.png",
|
|
badge: "/static/badge-96.png",
|
|
tag: d.tag,
|
|
timestamp: d.timestamp,
|
|
actions: d.actions || [],
|
|
data: { url: d.url || "/" },
|
|
}));
|
|
});
|
|
|
|
// Tapping the notification or its "View player" button opens its page, in an open window if
|
|
// there is one.
|
|
self.addEventListener("notificationclick", (e) => {
|
|
e.notification.close();
|
|
const url = new URL(e.notification.data?.url || "/", self.location.origin).href;
|
|
e.waitUntil((async () => {
|
|
const wins = await self.clients.matchAll({ type: "window", includeUncontrolled: true });
|
|
const win = wins.find((w) => new URL(w.url).origin === self.location.origin);
|
|
if (win) {
|
|
try {
|
|
await win.focus();
|
|
await win.navigate(url);
|
|
return;
|
|
} catch { /* not ours to navigate; open a new one */ }
|
|
}
|
|
await self.clients.openWindow(url);
|
|
})());
|
|
});
|
|
|
|
// Browsers sometimes replace a subscription. Hand the new one to the panel so notifications keep
|
|
// coming; this only works while the staff member's session cookie is still valid.
|
|
self.addEventListener("pushsubscriptionchange", (e) => {
|
|
e.waitUntil((async () => {
|
|
const opts = e.oldSubscription?.options;
|
|
const sub = e.newSubscription || (opts && await self.registration.pushManager.subscribe(opts));
|
|
if (!sub) return;
|
|
await fetch("/api/admin/notifications/subscribe", {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: { "Content-Type": "application/json", "X-Requested-With": "simpleadmin-web" },
|
|
body: JSON.stringify({ ...sub.toJSON(), userAgent: navigator.userAgent }),
|
|
});
|
|
})());
|
|
});
|