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.
93 lines
9.2 KiB
HTML
93 lines
9.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: a single day</title>
|
||
<meta name="description" content="One day's weather in full detail: every percentile tier for that date and place, and exactly where the day's high, low, feels-like, wind and rain landed against local history." />
|
||
<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: a single day" />
|
||
<meta property="og:description" content="One day's weather in full detail: every percentile tier for that date and place, and exactly where the day's high, low, feels-like, wind and rain landed against local history." />
|
||
<meta property="og:url" content="__ORIGIN__/day" />
|
||
<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__/day" />
|
||
<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="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<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">Every percentile boundary for one day vs its typical climate</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" class="active">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>
|
||
<section class="controls">
|
||
<div class="find-bar">
|
||
<button type="button" id="find-btn" class="find-btn"></button>
|
||
<span id="loc-label" class="loc-label" hidden></span>
|
||
</div>
|
||
<div class="day-nav">
|
||
<button type="button" id="prev-day" class="day-step" title="Previous day" aria-label="Previous day">‹</button>
|
||
<label class="day-date">Day
|
||
<input id="date-input" type="date" />
|
||
</label>
|
||
<button type="button" id="next-day" class="day-step" title="Next day" aria-label="Next day">›</button>
|
||
</div>
|
||
</section>
|
||
|
||
<div id="day-head" class="cal-head" hidden></div>
|
||
<div class="placeholder" id="day-placeholder">
|
||
<p>Pick a place and a day, or open this from a day on the map or calendar, to see
|
||
the value at every tier boundary for that day's typical climate.</p>
|
||
</div>
|
||
<div id="day-body"></div>
|
||
</main>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></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="day.js"></script>
|
||
<script type="module" src="ios-install.js"></script>
|
||
</body>
|
||
</html>
|