// Weather-alerts management page (/alerts). Lists a user's subscriptions and lets
// them add a city (via the shared map picker), tune the percentile threshold and
// watched metrics, toggle active, and remove. Auth-gated: signed-out visitors get
// a sign-in prompt. All calls go through account.js's cookie-aware apiFetch.
//
// Also the closest thing to an account page, so it doubles as the Saved
// locations surface: bookmarks.js is the single source of truth for those (see
// its header comment), this just lists/renders/edits them the same way it does
// alert subscriptions, reusing .sub-list/.sub-card.
import "./nav.js";
import { apiFetch, openAuth, onAuthChange, uv } from "./account.js";
import { open as openPicker } from "./mappicker.js";
import * as pushClient from "./push-client.js";
import * as bookmarks from "./bookmarks.js";
// Metric keys a user can watch, with friendly labels. (fmax/fmin exist server-side
// but are omitted from the picker to keep it focused; labelled here if present.)
const METRICS = [
["tmax", "High temp"],
["tmin", "Low temp"],
["feels", "Feels-like"],
["humid", "Humidity"],
["wind", "Wind"],
["gust", "Gust"],
["precip", "Precipitation"],
];
const METRIC_LABEL = Object.fromEntries(METRICS);
METRIC_LABEL.fmax = "Feels-like high";
METRIC_LABEL.fmin = "Feels-like low";
const DEFAULT_METRICS = ["tmax", "feels", "precip"];
const TRASH_IC = ``;
const PENCIL_IC = ``;
const body = document.getElementById("alerts-body");
let subs = [];
function esc(s) {
return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
}
// Small stylesheet for the bits of the Saved locations section that don't
// already have a home in .sub-list/.sub-card — injected once, not appended to
// the shared style.css, so this stays self-contained.
(function injectBkmStyle() {
if (document.getElementById("tg-bkm-alerts-style")) return;
const s = document.createElement("style");
s.id = "tg-bkm-alerts-style";
s.textContent = `
.bkm-heading { font-size: 15px; margin: 26px 0 4px; }
.bkm-import-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin: 0 0 12px; }
.bkm-import-status { margin: -4px 0 10px; }
.bkm-card-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
.bkm-rename { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 7px; border-radius: 6px; display: inline-flex; }
.bkm-rename:hover { color: var(--text); background: var(--border); }
.bkm-card-loc { color: var(--muted); font-size: 12.5px; }
`;
document.head.appendChild(s);
})();
async function readErr(res) {
try {
const d = await res.json();
if (typeof d.detail === "string") return d.detail;
} catch (e) {}
return `Request failed (${res.status}).`;
}
// --- load + top-level render -------------------------------------------------
async function load() {
let res;
try { res = await apiFetch(uv("subscriptions")); }
catch (e) { body.innerHTML = `
Offline. Can't load alerts.
`; return; }
if (res.status === 401) { renderGate(); return; }
if (!res.ok) { body.innerHTML = `
Thermograph can watch any city and tell you when its weather turns
unusually extreme for that place (a heat spell, a cold snap, heavy rain), graded
against ~45 years of local climate history.
Pick a city and the metrics to watch: high, low, feels-like, rain, wind…
Set a threshold. Alert when a day passes, say, the 97th percentile for
that location. Higher = rarer, so fewer but more significant alerts.
Get notified in-app (and via OS push on this device) when a recorded day, or
the upcoming forecast, crosses your threshold. At most one notification per week per alert.
For example: “Tell me when Tokyo's high passes the 97th percentile.”
You'll need an account to create and manage alerts.
`;
li.querySelector(".bkm-rename").onclick = () => {
const next = window.prompt("Rename this saved location:", b.label || "");
if (next && next.trim()) bookmarks.rename(b.id, next.trim());
};
li.querySelector(".sub-remove").onclick = () => bookmarks.remove(b.id);
return li;
}
function renderBookmarksSection() {
const el = document.getElementById("bkm-alerts-section");
if (!el) return;
const list = bookmarks.list();
el.innerHTML = `
Saved locations
Places you've starred on the map, kept in sync with your account.
`;
const ul = el.querySelector("#bkm-list");
if (!list.length) {
ul.innerHTML = `
No saved locations yet. Star a spot on the map to see it here.
`;
} else {
list.forEach((b) => ul.appendChild(bkmCard(b)));
}
el.querySelector("#bkm-import-btn").onclick = async () => {
const btn = el.querySelector("#bkm-import-btn");
const status = el.querySelector("#bkm-import-status");
btn.disabled = true;
try {
const res = await bookmarks.importAllLocal();
status.hidden = false;
status.textContent = res.imported
? `Imported ${res.imported} location${res.imported === 1 ? "" : "s"}${res.skipped ? ` (${res.skipped} already saved)` : ""}.`
: "Nothing new to import from this device.";
} catch (e) {
status.hidden = false;
status.textContent = e.message || "Couldn't import right now.";
} finally {
btn.disabled = false;
}
};
}
// Bookmarks can change from outside this render cycle (rename/remove above,
// or an import banner) — keep the section live rather than only painting once.
bookmarks.subscribe(() => renderBookmarksSection());
// --- push notifications toggle (this device) ---------------------------------
// Delivery over OS push is per-device: a subscription lives in each browser, so
// this control reflects/toggles *this* device, separate from the account-wide
// alert list below.
async function renderPushBar() {
const el = document.getElementById("push-bar");
if (!el) return;
if (!pushClient.supported()) {
// Insecure context (plain-HTTP LAN) or an old browser — push simply isn't
// available. Keep it quiet: a single muted line, no dead controls.
el.hidden = false;
el.innerHTML = `
Open this page over HTTPS (or on localhost) to get
OS notifications on this device. In-app alerts via the bell still work anywhere.