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
|
|
|
|
// Shared presentation constants + small helpers for every page script — each
|
|
|
|
|
|
// constant and helper here previously existed as a copy in app.js, calendar.js,
|
|
|
|
|
|
// day.js, compare.js and/or legend.html; pages import what they need so a tier
|
|
|
|
|
|
// color, scale label or formatter has exactly one home.
|
|
|
|
|
|
// ---- tier colors ----
|
|
|
|
|
|
// style.css's :root custom properties are the source of truth (they're not
|
|
|
|
|
|
// re-themed, so reading them once at load is safe). The JS map exists because
|
|
|
|
|
|
// inline-SVG work — chart building and the PNG export in particular — needs
|
|
|
|
|
|
// literal color values, where CSS variables can't resolve. The hex fallbacks
|
|
|
|
|
|
// only cover a missing/blocked stylesheet.
|
|
|
|
|
|
const FALLBACK = {
|
|
|
|
|
|
"rec-hot": "#8f0e20", "very-hot": "#dd6b52", "hot": "#ef9351", "warm": "#f6c18a",
|
|
|
|
|
|
"normal": "#4a9d5b",
|
|
|
|
|
|
"cool": "#92c5de", "cold": "#4393c3", "very-cold": "#2166ac", "rec-cold": "#0a2f6b",
|
|
|
|
|
|
"dry": "#c9a24a",
|
|
|
|
|
|
"wet-1": "#d9f0d3", "wet-2": "#b7e2b1", "wet-3": "#8fd18f", "wet-4": "#5cba9f",
|
|
|
|
|
|
"wet-5": "#35a1c0", "wet-6": "#2b8cbe", "wet-7": "#226bb3", "wet-8": "#164a97",
|
|
|
|
|
|
"wet-9": "#08306b",
|
|
|
|
|
|
};
|
|
|
|
|
|
const rootCss = getComputedStyle(document.documentElement);
|
|
|
|
|
|
export const TIER_COLORS = {};
|
|
|
|
|
|
for (const [cls, fb] of Object.entries(FALLBACK)) {
|
|
|
|
|
|
TIER_COLORS[cls] = rootCss.getPropertyValue(`--${cls}`).trim() || fb;
|
|
|
|
|
|
}
|
|
|
|
|
|
// The rain-intensity subset + the dry tint, for pages that color them apart.
|
|
|
|
|
|
export const PRECIP_COLORS = Object.fromEntries(
|
|
|
|
|
|
Object.entries(TIER_COLORS).filter(([c]) => c.startsWith("wet-")));
|
|
|
|
|
|
export const DRY_COLOR = TIER_COLORS.dry;
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
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
|
|
|
|
// ---- percentile tier scales, low → high ----
|
|
|
|
|
|
// Names are deliberately *relative* to each place's own climate history (a
|
|
|
|
|
|
// percentile), not absolute temperature words — see the README design note.
|
|
|
|
|
|
export const SCALE_TEMP = [
|
|
|
|
|
|
{ c: "rec-cold", label: "Near Record", range: "<1" },
|
|
|
|
|
|
{ c: "very-cold", label: "Very Low", range: "1–10" },
|
|
|
|
|
|
{ c: "cold", label: "Low", range: "10–25" },
|
|
|
|
|
|
{ c: "cool", label: "Below Normal", range: "25–40" },
|
|
|
|
|
|
{ c: "normal", label: "Normal", range: "40–60" },
|
|
|
|
|
|
{ c: "warm", label: "Above Normal", range: "60–75" },
|
|
|
|
|
|
{ c: "hot", label: "High", range: "75–90" },
|
|
|
|
|
|
{ c: "very-hot", label: "Very High", range: "90–99" },
|
|
|
|
|
|
{ c: "rec-hot", label: "Near Record", range: ">99" },
|
|
|
|
|
|
];
|
|
|
|
|
|
// Rain-intensity tiers by rain-day percentile (dry days show their dry streak).
|
|
|
|
|
|
export const SCALE_RAIN = [
|
|
|
|
|
|
{ c: "wet-1", label: "Trace", range: "<1" },
|
|
|
|
|
|
{ c: "wet-2", label: "Very Light", range: "1–10" },
|
|
|
|
|
|
{ c: "wet-3", label: "Light", range: "10–25" },
|
|
|
|
|
|
{ c: "wet-4", label: "Light–Mod", range: "25–40" },
|
|
|
|
|
|
{ c: "wet-5", label: "Moderate", range: "40–60" },
|
|
|
|
|
|
{ c: "wet-6", label: "Mod–Heavy", range: "60–75" },
|
|
|
|
|
|
{ c: "wet-7", label: "Heavy", range: "75–90" },
|
|
|
|
|
|
{ c: "wet-8", label: "Very Heavy", range: "90–99" },
|
|
|
|
|
|
{ c: "wet-9", label: "Extreme", range: ">99" },
|
|
|
|
|
|
];
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
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
|
|
|
|
// ---- dry-streak ramp ----
|
|
|
|
|
|
// "Days since last rain": rain days are blue; each dry day warms from a light
|
|
|
|
|
|
// base toward dark red, saturating at 14 consecutive dry days.
|
|
|
|
|
|
const DRY_BASE = [247, 217, 176], DRY_MAX = [122, 13, 26], DRY_CAP = 14;
|
|
|
|
|
|
const lerpRgb = (a, b, t) =>
|
|
|
|
|
|
`rgb(${a.map((x, i) => Math.round(x + (b[i] - x) * t)).join(",")})`;
|
|
|
|
|
|
export function drynessColor(dsr) {
|
|
|
|
|
|
if (dsr == null) return "";
|
|
|
|
|
|
if (dsr === 0) return "#2b7bba"; // measurable rain that day
|
|
|
|
|
|
return lerpRgb(DRY_BASE, DRY_MAX, Math.min(dsr, DRY_CAP) / DRY_CAP);
|
|
|
|
|
|
}
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
2026-07-11 22:33:38 +00:00
|
|
|
|
// ---- category distribution (shared by the calendar totals + the compare page) ----
|
|
|
|
|
|
// Bucket a set of daily records into the chosen metric's ordered categories
|
|
|
|
|
|
// (low→high), each entry [label, color, dayCount, scaleGroup]. Temperature-style
|
|
|
|
|
|
// metrics (High/Low/Feels/Humid/Wind/Gust) use the diverging Record-Low→Record-High
|
|
|
|
|
|
// tiers; precip uses the rain-intensity tiers plus a Dry bucket; dry-streak uses
|
|
|
|
|
|
// run-length bands. The scaleGroup ("wet"/"dry"/"temp") lets distStrip scale and
|
|
|
|
|
|
// percent wet vs. dry days apart so a lopsided count on one side can't crush the
|
|
|
|
|
|
// other. Categories are percentile classes on the records, so they're unit-agnostic.
|
|
|
|
|
|
export function metricBuckets(days, metric) {
|
|
|
|
|
|
if (metric === "dsr") {
|
|
|
|
|
|
const bands = [
|
|
|
|
|
|
["Rain day", drynessColor(0), (d) => d === 0],
|
|
|
|
|
|
["1–3 dry", drynessColor(2), (d) => d >= 1 && d <= 3],
|
|
|
|
|
|
["4–6 dry", drynessColor(5), (d) => d >= 4 && d <= 6],
|
|
|
|
|
|
["7–13 dry", drynessColor(10), (d) => d >= 7 && d <= 13],
|
|
|
|
|
|
["14+ dry", drynessColor(14), (d) => d >= 14],
|
|
|
|
|
|
];
|
|
|
|
|
|
const vals = days.map((r) => r.dsr).filter((d) => d != null);
|
|
|
|
|
|
// The lone "Rain day" (wet) scales apart from the dry-streak buckets (dry).
|
|
|
|
|
|
return bands.map(([label, color, fn], i) => [label, color, vals.filter(fn).length, i === 0 ? "wet" : "dry"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (metric === "precip") {
|
|
|
|
|
|
const classes = days.map((r) => r.precip && r.precip.c).filter(Boolean);
|
|
|
|
|
|
// Dry days scale independently of the rain-intensity buckets (wet), and vice versa.
|
|
|
|
|
|
// Index 4 carries the rain-tier key (e.g. "wet-5") so a single crowded label —
|
|
|
|
|
|
// "Moderate", wedged between the two-line "Light–Mod"/"Mod–Heavy" — can be nudged
|
|
|
|
|
|
// in CSS (.ct-wet-5) without brittle text matching.
|
|
|
|
|
|
return [["Dry", drynessColor(7), classes.filter((c) => c === "dry").length, "dry"]]
|
|
|
|
|
|
.concat(SCALE_RAIN.map((t) => [t.label, PRECIP_COLORS[t.c], classes.filter((c) => c === t.c).length, "wet", t.c]));
|
|
|
|
|
|
}
|
|
|
|
|
|
const classes = days.map((r) => { const g = r[metric]; return g && g.c; }).filter(Boolean);
|
|
|
|
|
|
// Temperature tiers all share one scale group, so they scale together.
|
|
|
|
|
|
return SCALE_TEMP.map((t) => [t.label, TIER_COLORS[t.c], classes.filter((c) => c === t.c).length, "temp"]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Render buckets from metricBuckets() as the compact distribution strip: one
|
|
|
|
|
|
// status-colored line per category, low→high, each figure printed above as a share
|
|
|
|
|
|
// (default) or, when showCount is set, the raw day count. Bar heights and shares are
|
|
|
|
|
|
// scoped to each bucket's scale group (index 3): wet and dry days scale apart, and a
|
|
|
|
|
|
// multi-bar group shows each bar's share within that group, while a lone opposing
|
|
|
|
|
|
// column (Dry on precip, Rain day on dry streak) stays a share of all shown days
|
|
|
|
|
|
// (a group-relative percentage there would trivially read 100%). Temperature tiers
|
|
|
|
|
|
// share one group, so they scale together. Assumes ≥1 day (callers guard empty).
|
|
|
|
|
|
export function distStrip(buckets, showCount) {
|
|
|
|
|
|
const total = buckets.reduce((n, b) => n + b[2], 0);
|
|
|
|
|
|
const groupTotal = {}, groupCount = {}, maxByGroup = {};
|
|
|
|
|
|
for (const b of buckets) {
|
|
|
|
|
|
groupTotal[b[3]] = (groupTotal[b[3]] || 0) + b[2];
|
|
|
|
|
|
groupCount[b[3]] = (groupCount[b[3]] || 0) + 1;
|
|
|
|
|
|
maxByGroup[b[3]] = Math.max(maxByGroup[b[3]] || 1, b[2]);
|
|
|
|
|
|
}
|
|
|
|
|
|
const pctText = (n, group) => {
|
|
|
|
|
|
const d = groupCount[group] > 1 ? (groupTotal[group] || 1) : total;
|
|
|
|
|
|
const p = 100 * n / d, r = Math.round(p);
|
|
|
|
|
|
return n > 0 && r === 0 ? `${p.toFixed(1)}%` : `${r}%`;
|
|
|
|
|
|
};
|
|
|
|
|
|
const valText = (n, group) => !n ? "0" : (showCount ? n.toLocaleString() : pctText(n, group));
|
|
|
|
|
|
const LINE_MAX = 30; // px — height of the tallest category in its group
|
|
|
|
|
|
const cols = buckets.map(([label, color, n, group, cls]) => {
|
|
|
|
|
|
const h = !n ? 0 : Math.max(2, Math.round(LINE_MAX * n / maxByGroup[group]));
|
|
|
|
|
|
const line = h ? `<span class="ct-line" style="height:${h}px"></span>` : "";
|
|
|
|
|
|
// `cls` (a rain-tier key on precip) becomes a ct-<cls> hook for label nudging.
|
|
|
|
|
|
return `<div class="ct-col${cls ? ` ct-${cls}` : ""}" style="--c:${color}" ` +
|
|
|
|
|
|
`title="${label} · ${pctText(n, group)} (${n})">` +
|
|
|
|
|
|
`<span class="ct-cap"><span class="ct-label">${label}</span>` +
|
|
|
|
|
|
`<span class="ct-num${n ? "" : " ct-num-zero"}">${valText(n, group)}</span></span>${line}</div>`;
|
|
|
|
|
|
}).join("");
|
|
|
|
|
|
return `<div class="ct-strip" style="--line-max:${LINE_MAX}px">${cols}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// ---- formatters & tiny utils ----
|
|
|
|
|
|
// (Temperatures go through nav.js's unit-aware fmtTemp; these are unit-agnostic.)
|
|
|
|
|
|
export const fmtPrecip = (v) => (v == null ? "—" : `${v.toFixed(2)}"`);
|
|
|
|
|
|
export const fmtWind = (v) => (v == null ? "—" : `${Math.round(v)} mph`);
|
|
|
|
|
|
export const fmtHumid = (v) => (v == null ? "—" : `${v.toFixed(1)} g/m³`);
|
|
|
|
|
|
export const ord = (n) => {
|
|
|
|
|
|
const r = Math.round(n), s = ["th", "st", "nd", "rd"], v = r % 100;
|
|
|
|
|
|
return r + (s[(v - 20) % 10] || s[v] || s[0]);
|
|
|
|
|
|
};
|
|
|
|
|
|
export const todayISO = () => new Date().toISOString().slice(0, 10);
|
|
|
|
|
|
export const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
|
|
|
|
// The heading label for a graded response: the resolved place name, or the
|
|
|
|
|
|
// cell-center coordinates while (or if) no name resolves.
|
|
|
|
|
|
export const placeLabel = (data) =>
|
|
|
|
|
|
data.place || `${data.cell.center_lat.toFixed(3)}, ${data.cell.center_lon.toFixed(3)}`;
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
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
|
|
|
|
// ---- dates & range chunking ----
|
|
|
|
|
|
export const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
|
|
|
|
export const pad = (n) => String(n).padStart(2, "0");
|
|
|
|
|
|
export const isoOfDate = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
|
|
|
|
export const monthStart = (ym) => `${ym}-01`; // 1st of a "YYYY-MM"
|
|
|
|
|
|
export const monthEnd = (ym) => isoOfDate(new Date(+ym.slice(0, 4), +ym.slice(5, 7), 0));
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
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
|
|
|
|
// Split [startIso, endIso] into consecutive ≤2-year windows (oldest first) so
|
|
|
|
|
|
// each stays within the calendar endpoint's per-request cap; the calendar grid
|
|
|
|
|
|
// and the compare series both fill chunk by chunk.
|
|
|
|
|
|
export const CHUNK_MONTHS = 24;
|
|
|
|
|
|
export function buildChunks(startIso, endIso) {
|
|
|
|
|
|
const chunks = [];
|
|
|
|
|
|
let s = startIso, guard = 0;
|
|
|
|
|
|
while (s <= endIso && guard++ < 200) {
|
|
|
|
|
|
const sd = new Date(s + "T00:00:00");
|
|
|
|
|
|
const ed = new Date(sd.getFullYear(), sd.getMonth() + CHUNK_MONTHS, sd.getDate());
|
|
|
|
|
|
ed.setDate(ed.getDate() - 1); // 24 months inclusive
|
|
|
|
|
|
let e = isoOfDate(ed);
|
|
|
|
|
|
if (e > endIso) e = endIso;
|
|
|
|
|
|
chunks.push({ start: s, end: e });
|
|
|
|
|
|
const ns = new Date(e + "T00:00:00"); ns.setDate(ns.getDate() + 1);
|
|
|
|
|
|
s = isoOfDate(ns);
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
}
|
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
|
|
|
|
return chunks;
|
|
|
|
|
|
}
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
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
|
|
|
|
// Clicking anywhere in a month/date field opens the native picker. Desktop
|
|
|
|
|
|
// Chrome otherwise only opens it from the tiny calendar glyph and just focuses
|
|
|
|
|
|
// a segment; showPicker is missing in some browsers (Safari/Firefox) — ignore.
|
|
|
|
|
|
export function clickOpensPicker(...els) {
|
|
|
|
|
|
for (const el of els) {
|
|
|
|
|
|
el.addEventListener("click", () => { try { el.showPicker(); } catch (e) {} });
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
}
|
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
|
|
|
|
}
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
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
|
|
|
|
// ---- weather summary ----
|
|
|
|
|
|
// Monochrome line icons (currentColor) — no emoji, matching the site's dark
|
|
|
|
|
|
// look. Feather-style paths.
|
|
|
|
|
|
const WX = (paths) =>
|
|
|
|
|
|
`<svg class="wx" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
|
|
|
|
const WX_CLOUD = `<path d="M20 16.6A5 5 0 0 0 18 7h-1.26A8 8 0 1 0 4 15.25"/>`;
|
|
|
|
|
|
export const WX_ICONS = {
|
|
|
|
|
|
sun: WX(`<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.2" y1="4.2" x2="5.6" y2="5.6"/><line x1="18.4" y1="18.4" x2="19.8" y2="19.8"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.2" y1="19.8" x2="5.6" y2="18.4"/><line x1="18.4" y1="5.6" x2="19.8" y2="4.2"/>`),
|
|
|
|
|
|
drizzle: WX(`${WX_CLOUD}<line x1="8" y1="19" x2="8" y2="21"/><line x1="16" y1="19" x2="16" y2="21"/><line x1="12" y1="20" x2="12" y2="22"/>`),
|
|
|
|
|
|
rain: WX(`${WX_CLOUD}<line x1="8" y1="19" x2="8" y2="22"/><line x1="16" y1="19" x2="16" y2="22"/><line x1="12" y1="19" x2="12" y2="23"/>`),
|
|
|
|
|
|
storm: WX(`<path d="M19 16.9A5 5 0 0 0 18 7h-1.26a8 8 0 1 0-11.62 9"/><polyline points="13 11 9 17 15 17 11 23"/>`),
|
|
|
|
|
|
snow: WX(`<path d="M20 17.6A5 5 0 0 0 18 8h-1.26A8 8 0 1 0 4 16.25"/><line x1="8" y1="18" x2="8" y2="18"/><line x1="12" y1="20" x2="12" y2="20"/><line x1="16" y1="18" x2="16" y2="18"/><line x1="12" y1="16" x2="12" y2="16"/>`),
|
|
|
|
|
|
};
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
|
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
|
|
|
|
// Plain-language "what was the day like" descriptor from the raw values: a
|
|
|
|
|
|
// temperature word (from the daily high, °F) crossed with a sky/precip word
|
|
|
|
|
|
// (precip in inches — falling as snow at/below freezing). `dsr` is optional:
|
|
|
|
|
|
// when a long dry streak is known, a no-rain day reads "dry" instead of
|
|
|
|
|
|
// "clear" (the calendar passes it; the day page doesn't track streaks).
|
|
|
|
|
|
export function weatherType(t, p, dsr) {
|
|
|
|
|
|
const wet = p != null && p >= 0.01;
|
|
|
|
|
|
const freezing = t != null && t <= 34;
|
|
|
|
|
|
const temp = t == null ? "" :
|
|
|
|
|
|
t >= 95 ? "Scorching" : t >= 85 ? "Hot" : t >= 72 ? "Warm" :
|
|
|
|
|
|
t >= 58 ? "Mild" : t >= 45 ? "Cool" : t >= 32 ? "Cold" : "Frigid";
|
|
|
|
|
|
let sky, skyKey, icon;
|
|
|
|
|
|
if (wet && freezing) { sky = p >= 0.3 ? "heavy snow" : "snow"; skyKey = "snow"; icon = WX_ICONS.snow; }
|
|
|
|
|
|
else if (wet && p >= 1.0) { sky = "downpour"; skyKey = "downpour"; icon = WX_ICONS.storm; }
|
|
|
|
|
|
else if (wet && p >= 0.3) { sky = "rain"; skyKey = "rain"; icon = WX_ICONS.rain; }
|
|
|
|
|
|
else if (wet) { sky = "light rain"; skyKey = "drizzle"; icon = WX_ICONS.drizzle; }
|
|
|
|
|
|
else {
|
|
|
|
|
|
skyKey = dsr != null && dsr >= 10 ? "dry" : "clear";
|
|
|
|
|
|
sky = skyKey === "dry" ? "dry" : "clear";
|
|
|
|
|
|
icon = WX_ICONS.sun;
|
Extract shared.js: one home for tier colors, scales, formatters, helpers (#47)
The page scripts each re-declared the shared presentation layer — the tier
color table existed in four places (app.js, calendar.js, day.js, style.css)
and the scale label tables in three (plus legend.html's own drifting copy),
alongside per-page copies of the dryness ramp, formatters, ord, todayISO,
esc, the weather icons/summary, month helpers and the 2-year range chunker.
~240 duplicated lines deleted (net -236 with the new module included).
- frontend/shared.js (IIFE, extends window.Thermograph): tier colors read
from style.css's :root custom properties at load — the CSS is now the
single source of truth; the JS map exists only because inline-SVG work
(chart + PNG export) needs literal values, with hex fallbacks for a
missing stylesheet. Plus SCALE_TEMP/SCALE_RAIN, drynessColor, fmt*, ord,
todayISO, esc, placeLabel, month/chunk date helpers, clickOpensPicker,
and the weatherType summary (dsr-aware; the day page just omits dsr).
- nav.js: wrapped in an IIFE — its ~15 top-level functions were globals in
the shared classic-script scope, and a leaked locHash collided with page
destructuring (caught by the browser smoke, not by node --check).
locHash is now exported and used by all 8 former hand-built hash sites.
- mappicker.js: initFindButton/setFindLabel replace the Find-button block
each page rebuilt.
- legend.html renders its scales from the shared tables, so the guide can
no longer drift from what the app shows.
Verified: node --check on all JS; 108 backend tests; headless-Chromium
smoke over all five pages against live data — no console/page errors,
legend rows 9/9, weekly 7 cards + colored chart/key/table + metric toggle,
calendar grid + key + metric switch, day 7 ladders + weather icon,
compare seeded rank card.
2026-07-11 20:21:48 +00:00
|
|
|
|
}
|
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
|
|
|
|
const text = !temp ? sky : wet ? `${temp}, ${sky}` : `${temp} & ${sky}`;
|
|
|
|
|
|
return { icon, text: text.charAt(0).toUpperCase() + text.slice(1),
|
|
|
|
|
|
feel: temp.toLowerCase(), sky: skyKey };
|
|
|
|
|
|
}
|