2026-07-11 00:29:47 +00:00
<!DOCTYPE html>
< html lang = "en" >
< head >
< meta charset = "utf-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1" / >
< title > Thermograph — how unusual is your weather?< / title >
2026-07-11 15:59:14 +00:00
< 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 — how unusual is your weather?" / >
< 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__/" / >
< 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" / >
2026-07-11 00:29:47 +00:00
< 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" >
< span class = "logo" > ▚< / span >
< div >
< h1 > Thermograph< / h1 >
< p class = "tag" > Grading today's weather against ~45 years of local climate history< / p >
< / div >
< nav class = "view-nav" aria-label = "Views" >
< a href = "./" data-view = "map" class = "active" > Weekly< / a >
< a href = "calendar" data-view = "calendar" > Calendar< / a >
< a href = "day" data-view = "day" > Day< / a >
2026-07-11 02:58:56 +00:00
< a href = "compare" data-view = "compare" > Compare< / a >
2026-07-11 00:29:47 +00:00
< / nav >
< / 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 = "date-row" >
< label > Target date
< input id = "date-input" type = "date" / >
< / label >
< button type = "button" id = "today-btn" class = "today-chip" title = "Reset the target date to today" > Today< / button >
2026-07-11 07:10:46 +00:00
< span class = "hint" > < a href = "legend" > What do the grades mean? →< / a > < / span >
2026-07-11 00:29:47 +00:00
< / div >
< / section >
< section id = "panel" class = "panel panel-full" >
< div class = "placeholder" id = "placeholder" >
< p > Find a location to begin.< / p >
< p class = "muted" > Thermograph fetches decades of daily highs, lows, and precipitation
for your ~4 sq mi cell, builds a ± 7-day seasonal distribution for each
day of the year, and grades recent weather by where it falls on that distribution.< / p >
< / div >
< div id = "results" hidden > < / div >
< / section >
< / main >
< script src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" > < / script >
Convert the frontend to ES modules; split nav.js by concern (#48)
The frontend was classic scripts sharing one global scope, with a
load-order contract enforced only by comments (leaflet -> nav ->
shared -> mappicker -> page) and hand-rolled window.Thermograph /
window.LocationPicker namespaces. Page scripts now import what they use;
the dependency graph replaces the ordering contract, and no app globals
remain (Leaflet stays a classic script / global L, loaded first).
nav.js had grown four concerns; it's now three single-purpose modules:
- nav.js: last-location memory + header view-links + locHash.
- units.js: the °F/°C toggle and unit-aware formatting. The compare
special case is gone — pages that want the toggle import units.js;
compare simply doesn't.
- cache.js: the IndexedDB response cache, SWR getJSON, bundle-seeded
view prefetch and neighbor warming. prefetchViews(lat, lon, ownViews)
now takes the calling page's own view names instead of a page-identity
map (VIEW_OWN) — adding a page no longer means editing this module.
The slice->URL map stays here as bundle-contract knowledge.
frontend/package.json ({"type": "module"}) makes CI's node --check
parse the files as modules.
Verified: node --check on all files as modules; 108 backend tests;
headless-Chromium smoke across all five pages against live data — zero
console/page errors, all render assertions pass (cards, chart, calendar
grid + metric switch, ladders, compare ranking, legend scales).
2026-07-11 20:28:33 +00:00
< script type = "module" src = "app.js" > < / script >
2026-07-11 00:29:47 +00:00
< / body >
< / html >