Three operator decisions change v1's premises, so the design is now two tiers with separate flags: Tier A (THERMOGRAPH_EVENTS) is v1 unchanged -- anonymous hourly aggregates, no identifier, nothing stored on the device, no consent needed, runs for everyone. It stays the primary signal precisely so a poor consent rate cannot take the numbers down with it, and so Tier B's rates can be calibrated against it to measure the consent bias rather than ignoring it. Tier B (THERMOGRAPH_EVENT_SESSIONS) adds an ephemeral per-tab id -- 16 random bytes in sessionStorage, rotating on a 30-minute idle and a 2-hour absolute cap, capped at 200 events, never linked to another session, device, or to the account (api_event does not read the auth cookie and there is no user column). Rows land in a 30-day hypertable with minute-granularity timestamps and an in-session sequence number instead of precise clock times. Storing an identifier engages ePrivacy Art. 5(3) and analytics is not strictly necessary, so it is gated on opt-in consent: an equal-weight banner that mints nothing before "Allow", one-click withdrawal in every footer that drops the live id immediately, and GPC/DNT treated as a refusal already given. THERMOGRAPH_SEARCH_MISS captures zero-result search text server-side in api_suggest only -- normalised, rejected outright on any personal-data smell, stored as a per-day count, and pruned below a three-person floor after a week. The privacy page is rewritten rather than deferred: "no per-visitor identifier" becomes false the moment Tier B ships. Tests assert the load-bearing promises so the copy and the code cannot drift apart silently. Raw-IP truncation is NOT implemented here (the logging pipeline owns it) but UI-EVENTS.md states what the app side must do, and records that Caddy's default JSON log already stores full request URIs -- so every search query is in Loki with the client IP today, which the search-miss mitigations depend on fixing. UI-EVENTS.md carries the v1-to-v2 diff, the consent reasoning including why legitimate interest is not available, and an explicit argument that the session identifier is the wrong trade at this traffic volume.
124 lines
11 KiB
HTML
124 lines
11 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="How Thermograph grades weather: percentiles of a location's own ±7-day seasonal history, the scale from Below Normal to Near Record, and what every metric means." />
|
|
<meta name="theme-color" content="#f0803c" />
|
|
<!-- iOS/Android home-screen install: run standalone (no browser chrome) when
|
|
added to the Home Screen. On iOS 16.4+ this is what unlocks Web Push. -->
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
<meta name="apple-mobile-web-app-title" content="Thermograph" />
|
|
<!-- 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="How Thermograph grades weather: percentiles of a location's own ±7-day seasonal history, the scale from Below Normal to Near Record, and what every metric means." />
|
|
<meta property="og:url" content="__ORIGIN__/legend" />
|
|
<meta property="og:image" content="__ORIGIN__/logo.png?v=3" />
|
|
<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="canonical" href="__ORIGIN__/legend" />
|
|
<link rel="icon" href="favicon.svg?v=3" type="image/svg+xml" />
|
|
<link rel="icon" href="favicon-48.png?v=3" type="image/png" sizes="48x48" />
|
|
<link rel="icon" href="favicon-32.png?v=3" type="image/png" sizes="32x32" />
|
|
<link rel="icon" href="favicon-16.png?v=3" type="image/png" sizes="16x16" />
|
|
<link rel="apple-touch-icon" href="apple-touch-icon.png?v=3" />
|
|
<link rel="manifest" href="manifest.webmanifest" />
|
|
<link rel="stylesheet" href="style.css" />
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="brand">
|
|
<div>
|
|
<h1><a href="./"><span class="logo"><svg viewBox="0 0 512 512" width="28" height="28" aria-hidden="true"><rect x="32" y="32" width="448" height="448" rx="96" fill="#10141B" stroke="#FFFFFF" stroke-opacity="0.08" stroke-width="4"/><rect x="115" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="163" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="211" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="259" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="307" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="355" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="403" y="403" width="42" height="42" rx="10" fill="#111924"/><rect x="163" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="211" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="259" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="307" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="355" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="403" y="355" width="42" height="42" rx="10" fill="#131C25"/><rect x="67" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="163" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="211" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="259" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="307" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="355" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="403" y="307" width="42" height="42" rx="10" fill="#181F27"/><rect x="67" y="259" width="42" height="42" rx="10" fill="#131C1F"/><rect x="211" y="259" width="42" height="42" rx="10" fill="#131C1F"/><rect x="403" y="259" width="42" height="42" rx="10" fill="#131C1F"/><rect x="67" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="115" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="211" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="307" y="211" width="42" height="42" rx="10" fill="#1E1E22"/><rect x="67" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="115" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="307" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="355" y="163" width="42" height="42" rx="10" fill="#1D1C1E"/><rect x="67" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="115" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="163" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="211" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="259" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="307" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="355" y="115" width="42" height="42" rx="10" fill="#1C191E"/><rect x="67" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="115" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="163" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="211" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="259" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="307" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="355" y="67" width="42" height="42" rx="10" fill="#18141B"/><rect x="67" y="403" width="42" height="42" rx="10" fill="#2166ac"/><rect x="67" y="355" width="42" height="42" rx="10" fill="#4393c3"/><rect x="115" y="355" width="42" height="42" rx="10" fill="#4393c3"/><rect x="115" y="307" width="42" height="42" rx="10" fill="#92c5de"/><rect x="115" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="163" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="163" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="163" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="211" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="259" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="259" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="259" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="307" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="355" y="259" width="42" height="42" rx="10" fill="#4a9d5b"/><rect x="355" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="403" y="211" width="42" height="42" rx="10" fill="#f6c18a"/><rect x="403" y="163" width="42" height="42" rx="10" fill="#ef9351"/><rect x="403" y="115" width="42" height="42" rx="10" fill="#dd6b52"/><rect x="403" y="67" width="42" height="42" rx="10" fill="#8f0e20"/></svg></span>Thermograph</a></h1>
|
|
<p class="tag">What the metrics and percentile grades mean</p>
|
|
</div>
|
|
<details class="nav-menu">
|
|
<summary class="nav-toggle" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg></summary>
|
|
<div class="nav-panel">
|
|
<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 Detail</a>
|
|
<a href="compare" data-view="compare">Compare</a>
|
|
<a href="score" data-view="score">Score</a>
|
|
<a href="climate">Climate</a>
|
|
<a href="alerts">Alerts</a>
|
|
</nav>
|
|
</div>
|
|
</details>
|
|
</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 (<span data-unit-label="temp">°F</span>).</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 (<span data-unit-label="wind">mph</span>).</dd>
|
|
<dt>Gust</dt><dd>Peak wind gust for the day (<span data-unit-label="wind">mph</span>).</dd>
|
|
<dt>Precip</dt><dd>Total precipitation (<span data-unit-label="precip">inches</span>), 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>
|
|
<!-- Product-event beacon. Inert unless the server flagged this page
|
|
on the root element; see frontend/static/track.js. -->
|
|
<script type="module" src="consent.js"></script>
|
|
<script type="module" src="track.js"></script>
|
|
<script type="module" src="ios-install.js"></script>
|
|
</body>
|
|
</html>
|