Make the app installable and deliver existing alert notifications as OS push, alongside the in-app bell. Backend: - PushSubscription model (per-device endpoint + keys, owned by a user) and register/unregister/test endpoints under /api/v2/push, cookie-auth scoped to the user like the subscription routes. - push.py: VAPID key management (env -> data/vapid.json -> generated) and a pywebpush send helper that reports gone endpoints for pruning. No DB coupling. - notify.py: after creating an in-app Notification, dispatch Web Push to the user's devices (guarded — a push failure never affects the in-app write; endpoints reported gone are pruned). - Serve the .webmanifest with the correct media type. Frontend: - manifest.webmanifest + 192/maskable icons; <link rel="manifest"> on all pages. - sw.js: push + notificationclick handlers (push-only; no fetch caching, so it doesn't fight the existing IndexedDB cache). Registered globally in nav.js in secure contexts. - push-client.js + a "Notifications on this device" toggle and test-send on the /alerts page, subscribing through the existing cookie-aware apiFetch. Push and service workers require a secure context, so this is active over HTTPS (or http://localhost) and cleanly no-ops on a plain-HTTP LAN origin.
67 lines
2.6 KiB
JavaScript
67 lines
2.6 KiB
JavaScript
// Cross-view navigation + last-location memory. Remembers the most recently
|
|
// selected spot in localStorage and keeps the header's view links
|
|
// (Map · Calendar · Day · Compare) pointing at that spot — so switching views,
|
|
// or coming back to the map, lands you where you were instead of an empty map.
|
|
|
|
const LOC_KEY = "thermograph:loc";
|
|
|
|
function readStored() {
|
|
try {
|
|
const o = JSON.parse(localStorage.getItem(LOC_KEY));
|
|
if (o && typeof o.lat === "number" && typeof o.lon === "number") return o;
|
|
} catch (e) {}
|
|
return null;
|
|
}
|
|
|
|
// Where should this page start? A hash (a shared/opened link) always wins;
|
|
// otherwise fall back to the last place the user looked at.
|
|
export function loadLastLocation() {
|
|
const p = new URLSearchParams(location.hash.slice(1));
|
|
const lat = parseFloat(p.get("lat")), lon = parseFloat(p.get("lon"));
|
|
if (!isNaN(lat) && !isNaN(lon)) return { lat, lon, date: p.get("date") || null };
|
|
return readStored();
|
|
}
|
|
|
|
// Remember a spot. Passing no `date` preserves whatever date was stored before,
|
|
// so hopping through the calendar (which has no date) doesn't wipe the day you
|
|
// were on elsewhere.
|
|
export function saveLastLocation(lat, lon, date) {
|
|
let d = date;
|
|
if (d === undefined) { const prev = readStored(); d = prev ? prev.date : null; }
|
|
try { localStorage.setItem(LOC_KEY, JSON.stringify({ lat, lon, date: d || null })); } catch (e) {}
|
|
refreshNav(lat, lon, d);
|
|
}
|
|
|
|
export function locHash(lat, lon, date) {
|
|
let h = `#lat=${lat.toFixed(5)}&lon=${lon.toFixed(5)}`;
|
|
if (date) h += `&date=${date}`;
|
|
return h;
|
|
}
|
|
|
|
// Point every header view-link at the current spot so a tap keeps your place.
|
|
function refreshNav(lat, lon, date) {
|
|
if (lat == null || lon == null) return;
|
|
const h = locHash(lat, lon, date);
|
|
document.querySelectorAll(".view-nav a[data-view]").forEach((a) => {
|
|
a.href = (a.dataset.base || "") + h;
|
|
});
|
|
}
|
|
|
|
// Point the header links at the current spot as soon as the page loads.
|
|
(function initNav() {
|
|
document.querySelectorAll(".view-nav a[data-view]").forEach((a) => {
|
|
a.dataset.base = a.getAttribute("href");
|
|
});
|
|
const loc = loadLastLocation();
|
|
if (loc) refreshNav(loc.lat, loc.lon, loc.date);
|
|
})();
|
|
|
|
// Register the service worker so the app is installable and can receive Web Push
|
|
// (see sw.js). Base-relative, so it resolves under /thermograph automatically and
|
|
// takes /thermograph/ as its scope. A no-op in insecure contexts (plain-HTTP LAN)
|
|
// — registration just rejects and we swallow it; in-app features are unaffected.
|
|
if ("serviceWorker" in navigator && window.isSecureContext) {
|
|
window.addEventListener("load", () => {
|
|
navigator.serviceWorker.register("sw.js").catch(() => {});
|
|
});
|
|
}
|