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.
102 lines
5.2 KiB
HTML
102 lines
5.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Thermograph — reading the grades</title>
|
|
<meta name="description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
|
<meta name="theme-color" content="#f0803c" />
|
|
<!-- Link previews (Discord, Slack, iMessage…). The server fills the og:url /
|
|
og:image origins in from this request's scheme://host + base path —
|
|
crawlers need absolute URLs and don't run JS. -->
|
|
<meta property="og:type" content="website" />
|
|
<meta property="og:site_name" content="Thermograph" />
|
|
<meta property="og:title" content="Thermograph — reading the grades" />
|
|
<meta property="og:description" content="Pick any point on Earth and see how its recent weather stacks up against ~45 years of local climate history — graded days, calendars, forecasts, and side-by-side comparisons." />
|
|
<meta property="og:url" content="__ORIGIN__/legend" />
|
|
<meta property="og:image" content="__ORIGIN__/logo.png" />
|
|
<meta property="og:image:width" content="512" />
|
|
<meta property="og:image:height" content="512" />
|
|
<meta property="og:image:alt" content="Thermograph logo" />
|
|
<meta name="twitter:card" content="summary" />
|
|
<link rel="icon" href="logo.svg" type="image/svg+xml" />
|
|
<link rel="apple-touch-icon" href="logo.png" />
|
|
<link rel="manifest" href="manifest.webmanifest" />
|
|
<link rel="stylesheet" href="style.css" />
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="brand">
|
|
<span class="logo">▚</span>
|
|
<div>
|
|
<h1>Thermograph · Guide</h1>
|
|
<p class="tag">What the metrics and percentile grades mean</p>
|
|
</div>
|
|
<nav class="view-nav" aria-label="Views">
|
|
<a href="./" data-view="map">Weekly</a>
|
|
<a href="calendar" data-view="calendar">Calendar</a>
|
|
<a href="day" data-view="day">Day</a>
|
|
<a href="compare" data-view="compare">Compare</a>
|
|
</nav>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="guide">
|
|
<section class="guide-block">
|
|
<h2>How the grades work</h2>
|
|
<p>Every grade is <b>relative to this place's own climate history</b>, not an absolute
|
|
temperature. For each day Thermograph builds a ±7-day seasonal distribution from
|
|
decades of local records, then reports the <b>percentile</b> where the day falls in it.</p>
|
|
<p>So a 60° day can be “Above Normal” in one place or season and
|
|
“Below Normal” in another. That's why the categories read
|
|
“Above/Below Normal”, “High/Low”, and “Near Record” —
|
|
never “hot” or “cold”.</p>
|
|
</section>
|
|
|
|
<section class="guide-block">
|
|
<h2>Grade scale — percentile of the local ±7-day history</h2>
|
|
<div class="guide-scale" id="temp-scale"></div>
|
|
</section>
|
|
|
|
<section class="guide-block">
|
|
<h2>Rain intensity — percentile among rain days</h2>
|
|
<p class="muted">Precipitation is graded only across days that actually saw rain, so
|
|
“Heavy” means heavy <i>for a rainy day here</i>. Dry days are colored by
|
|
their dry streak instead (below).</p>
|
|
<div class="guide-scale" id="rain-scale"></div>
|
|
</section>
|
|
|
|
<section class="guide-block">
|
|
<h2>The metrics</h2>
|
|
<dl class="guide-metrics">
|
|
<dt>High / Low</dt><dd>The day's highest and lowest air temperature (°F).</dd>
|
|
<dt>Feels</dt><dd>Apparent temperature — what the air actually felt like once humidity
|
|
and wind are factored in, taking whichever apparent extreme (heat-index high or
|
|
wind-chill low) sits further from a temperate baseline, graded against its own history.</dd>
|
|
<dt>Humid</dt><dd>Absolute humidity: grams of water vapor per cubic meter of air (g/m³).</dd>
|
|
<dt>Wind</dt><dd>Average sustained wind speed (mph).</dd>
|
|
<dt>Gust</dt><dd>Peak wind gust for the day (mph).</dd>
|
|
<dt>Precip</dt><dd>Total precipitation (inches), graded by intensity among rain days.</dd>
|
|
<dt>Dry streak</dt><dd>Consecutive days since the last measurable rain — the color deepens
|
|
the longer it's been dry, and resets to blue on a rain day.</dd>
|
|
</dl>
|
|
</section>
|
|
|
|
<p class="guide-back"><a href="./">← Back to Thermograph</a></p>
|
|
</main>
|
|
|
|
<script type="module">
|
|
import "./nav.js"; // header view-links follow the last location
|
|
import "./units.js"; // the °F/°C toggle
|
|
import "./account.js"; // header sign-in entry + notification bell
|
|
// The scale rows come from the shared tier tables, so this guide can't
|
|
// drift from what the app actually shows.
|
|
import { SCALE_TEMP, SCALE_RAIN } from "./shared.js";
|
|
const row = ({ c, label, range }) =>
|
|
`<div class="guide-seg"><span class="guide-sw" style="background:var(--${c})"></span>` +
|
|
`<span class="guide-lbl">${label}</span><span class="guide-rng">${range}</span></div>`;
|
|
document.getElementById("temp-scale").innerHTML = SCALE_TEMP.map(row).join("");
|
|
document.getElementById("rain-scale").innerHTML = SCALE_RAIN.map(row).join("");
|
|
</script>
|
|
</body>
|
|
</html>
|