PageSourceSearch

https://alphalockandsafe.com/js/local-corner.js?v=20261001b

js alphalockandsafe.com collected 2026-10-04 19:14:11 UTC 17,800 bytes, 422 lines download raw bytes

1/* local-corner.js for alpha-lock, built 2026-10-02 by gotham-ops/local-corner/build.mjs. Do not hand-edit. */
2(function () {
3  var CONFIG = {"id":"local-corner","mode":"fill","tz":"America/New_York","weather":{"url":"https://api.weather.gov/gridpoints/OKX/39,58/forecast/hourly","rule":"lock-cold","tz":"America/New_York","place":"White Plains","lat":41.034,"lon":-73.7629}};
4/*
5 * Local corner: a small, dependency-free block that adds local value to a client homepage.
6 * Built per site by gotham-ops/local-corner/build.mjs, which wraps this file with that site's CONFIG.
7 * Owner's request 2026-10-01. Do not hand-edit the built copy in a client repo; edit the site JSON
8 * under gotham-ops/local-corner/sites/ and rebuild.
9 *
10 * Two modes:
11 *   fill   - the block is already in the page HTML (static sites). This script only adds the live
12 *            weather line and marks the current season. If it never runs, the page is complete.
13 *   inject - the page is a hydrated Next.js (React 19) export, so the block is inserted only after
14 *            React has hydrated <main> (same pattern as the fleet's holiday notices). It re-inserts
15 *            itself if React re-renders, and removes itself on client-side navigation away from "/".
16 *
17 * Weather comes from the US National Weather Service (api.weather.gov, public domain, CORS open,
18 * cached 1h by NWS). One request per visit, cached 30 min in localStorage when available.
19 */
20  "use strict";
21
22  var ID = CONFIG.id || "local-corner";
23  var CACHE_MS = 30 * 60 * 1000;
24
25  // ---------- helpers ----------
26  function nowParts() {
27    var tz = (CONFIG.weather && CONFIG.weather.tz) || CONFIG.tz || undefined;
28    try {
29      var f = new Intl.DateTimeFormat("en-US", { timeZone: tz, month: "numeric", hour: "numeric", hour12: false });
30      var p = f.formatToParts(new Date());
31      var m = 0, h = 0;
32      for (var i = 0; i < p.length; i++) {
33        if (p[i].type === "month") m = parseInt(p[i].value, 10);
34        if (p[i].type === "hour") h = parseInt(p[i].value, 10) % 24;
35      }
36      if (m) return { month: m, hour: h };
37    } catch (e) {}
38    var d = new Date();
39    return { month: d.getMonth() + 1, hour: d.getHours() };
40  }
41
42  function hourLabel(iso) {
43    try {
44      return new Intl.DateTimeFormat("en-US", { timeZone: CONFIG.weather.tz, hour: "numeric" }).format(new Date(iso));
45    } catch (e) {
46      return "";
47    }
48  }
49
50  function dayKey(d) {
51    try {
52      return new Intl.DateTimeFormat("en-CA", { timeZone: CONFIG.weather.tz, year: "numeric", month: "2-digit", day: "2-digit" }).format(d);
53    } catch (e) {
54      return d.toDateString();
55    }
56  }
57
58  // "today" / "tomorrow" for an hourly period, in the site's own time zone.
59  function dayWord(iso) {
60    var t = new Date(iso);
61    if (dayKey(t) === dayKey(new Date())) return "today";
62    if (dayKey(t) === dayKey(new Date(Date.now() + 86400000))) return "tomorrow";
63    return "";
64  }
65
66  function rainWords(pop) {
67    return pop >= 60 ? "Rain is likely (" + pop + "% chance)" : "Rain is possible (up to " + pop + "% chance)";
68  }
69
70  function cToF(c) {
71    return c == null ? null : Math.round(c * 9 / 5 + 32);
72  }
73
74  function num(v) {
75    if (v == null) return null;
76    if (typeof v === "number") return v;
77    if (typeof v === "object" && "value" in v) return v.value == null ? null : Number(v.value);
78    return Number(v);
79  }
80
81  function windMph(s) {
82    var m = String(s || "").match(/(\d+)(?:\s*to\s*(\d+))?/);
83    if (!m) return null;
84    return parseInt(m[2] || m[1], 10);
85  }
86
87  function cacheGet(key) {
88    try {
89      var raw = window.localStorage && localStorage.getItem(key);
90      if (!raw) return null;
91      var o = JSON.parse(raw);
92      if (!o || Date.now() - o.t > CACHE_MS) return null;
93      return o.v;
94    } catch (e) {
95      return null;
96    }
97  }
98
99  function cacheSet(key, v) {
100    try {
101      if (window.localStorage) localStorage.setItem(key, JSON.stringify({ t: Date.now(), v: v }));
102    } catch (e) {}
103  }
104
105  function fetchJson(url, ms) {
106    return new Promise(function (resolve, reject) {
107      var done = false;
108      var ctrl = window.AbortController ? new AbortController() : null;
109      var timer = setTimeout(function () {
110        if (done) return;
111        done = true;
112        if (ctrl) ctrl.abort();
113        reject(new Error("timeout"));
114      }, ms);
115      fetch(url, { headers: { Accept: "application/geo+json" }, signal: ctrl ? ctrl.signal : undefined })
116        .then(function (r) {
117          if (!r.ok) throw new Error("http " + r.status);
118          return r.json();
119        })
120        .then(function (j) {
121          if (done) return;
122          done = true;
123          clearTimeout(timer);
124          resolve(j);
125        })
126        .catch(function (e) {
127          if (done) return;
128          done = true;
129          clearTimeout(timer);
130          reject(e);
131        });
132    });
133  }
134
135  // Compact hourly series: [{t, f, dewF, rh, pop, wind, day, sf}]
136  function loadHours() {
137    var url = CONFIG.weather.url;
138    var key = "lc:" + url;
139    var hit = cacheGet(key);
140    if (hit) return Promise.resolve(hit);
141    return fetchJson(url, 8000).then(function (j) {
142      var periods = (j && j.properties && j.properties.periods) || [];
143      var now = Date.now();
144      var out = [];
145      for (var i = 0; i < periods.length && out.length < 36; i++) {
146        var p = periods[i];
147        if (Date.parse(p.endTime) <= now) continue;
148        out.push({
149          t: p.startTime,
150          f: num(p.temperature),
151          dewF: cToF(num(p.dewpoint)),
152          rh: num(p.relativeHumidity),
153          pop: num(p.probabilityOfPrecipitation) || 0,
154          wind: windMph(p.windSpeed),
155          day: !!p.isDaytime,
156          sf: p.shortForecast || ""
157        });
158      }
159      if (!out.length) throw new Error("no periods");
160      cacheSet(key, out);
161      return out;
162    });
163  }
164
165  function minOf(hs, k) {
166    var m = null;
167    for (var i = 0; i < hs.length; i++) if (hs[i][k] != null && (m == null || hs[i][k] < m)) m = hs[i][k];
168    return m;
169  }
170  function maxOf(hs, k) {
171    var m = null;
172    for (var i = 0; i < hs.length; i++) if (hs[i][k] != null && (m == null || hs[i][k] > m)) m = hs[i][k];
173    return m;
174  }
175  function avgOf(hs, k) {
176    var s = 0, n = 0;
177    for (var i = 0; i < hs.length; i++) if (hs[i][k] != null) { s += hs[i][k]; n++; }
178    return n ? Math.round(s / n) : null;
179  }
180  function frozenPrecip(hs) {
181    for (var i = 0; i < hs.length; i++) {
182      if (hs[i].f != null && hs[i].f <= 34 && hs[i].pop >= 40) return true;
183      if (/snow|sleet|freezing|ice/i.test(hs[i].sf) && hs[i].pop >= 30) return true;
184    }
185    return false;
186  }
187
188  // ---------- rules: each returns {level: "good"|"caution"|"poor", text} ----------
189  var RULES = {
190    // Exterior painting: most exterior latex paints want 50F+ for the whole application and drying
191    // window, air temperature at least 5F above the dew point, and no rain while the film sets.
192    "paint-exterior": function (hs) {
193      var next = hs.slice(0, 24);
194      var best = null, run = [];
195      for (var i = 0; i < next.length; i++) {
196        var h = next[i];
197        var ok = h.day && h.f >= 50 && h.f <= 90 && h.pop < 30 && (h.dewF == null || h.f - h.dewF >= 5);
198        if (ok) {
199          run.push(h);
200          if (!best || run.length > best.length) best = run.slice();
201        } else run = [];
202      }
203      if (best && best.length >= 4) {
204        return {
205          level: "good",
206          text: "Good window for exterior painting " + dayWord(best[0].t) + " from " + hourLabel(best[0].t) + " to " +
207            hourLabel(new Date(Date.parse(best[best.length - 1].t) + 3600000).toISOString()) +
208            " (" + minOf(best, "f") + " to " + maxOf(best, "f") + "°F, low rain chance). Many exterior latex paints need 50°F and up while they dry; check the label, some are rated lower."
209        };
210      }
211      var days = next.filter(function (h) { return h.day; });
212      var hi = maxOf(days.length ? days : next, "f");
213      if (hi != null && hi < 50) return { level: "poor", text: "Too cold for most exterior paints today (high near " + hi + "°F). Interior work is the better plan; many exterior latex paints need 50°F and up while they dry." };
214      var dpop = maxOf(days.length ? days : next, "pop");
215      if (dpop >= 30) return { level: "poor", text: rainWords(dpop) + " in the daytime hours, so exterior paint may not have time to set. A good day for interior rooms instead." };
216      return { level: "caution", text: "Conditions are borderline for exterior paint today (dew or short dry spells). Interior work is the safer choice." };
217    },
218
219    // Garage doors: torsion springs are under the most stress in hard cold.
220    "garage-cold": function (hs) {
221      var low = minOf(hs.slice(0, 24), "f");
222      if (low == null) return null;
223      if (low <= 10) return { level: "poor", text: "Deep cold in the next 24 hours (low near " + low + "°F). Torsion springs are most likely to snap in hard cold. If the door feels heavy or a spring shows a gap, stop using the opener." };
224      if (low <= 32) return { level: "caution", text: "Freezing temperatures in the next 24 hours (low near " + low + "°F). Spring breaks climb with the first hard freezes, so a quick look at the springs and cables now is worth it." };
225      return { level: "good", text: "No freeze in the next 24 hours (low near " + low + "°F). A good day for a balance check: lift the door halfway by hand with the opener disengaged; it should stay put." };
226    },
227
228    // Locks: ice in the cylinder and keys snapped by force.
229    "lock-cold": function (hs) {
230      var next = hs.slice(0, 24);
231      var low = minOf(next, "f");
232      if (low == null) return null;
233      if (low <= 32 && frozenPrecip(next)) return { level: "poor", text: "Freezing temperatures with precipitation in the next 24 hours (low near " + low + "°F). Locks and car doors can ice up: use a lock de-icer and never pour hot water into a lock, it refreezes deeper." };
234      if (low <= 32) return { level: "caution", text: "Below freezing in the next 24 hours (low near " + low + "°F). A frozen lock needs de-icer and patience, not force; forcing a key is how keys snap." };
235      return { level: "good", text: "No freeze in the next 24 hours (low near " + low + "°F). If a key feels stiff, a dry lubricant such as graphite keeps a lock turning through the winter; oil-based sprays attract grit." };
236    },
237
238    // Flooring: wood moves with humidity, vinyl plank far less so.
239    "flooring-humidity": function (hs) {
240      var rh = avgOf(hs.slice(0, 12), "rh");
241      if (rh == null) return null;
242      if (rh >= 65) return { level: "caution", text: "Humid today (relative humidity around " + rh + "%). Solid hardwood needs extra acclimation time in damp air; luxury vinyl plank is far less sensitive to humidity." };
243      if (rh <= 30) return { level: "caution", text: "Dry air today (relative humidity around " + rh + "%). Wood floors shrink in dry air, which is when seasonal gaps show; a humidifier helps protect hardwood." };
244      return { level: "good", text: "Moderate humidity today (around " + rh + "%), good conditions for flooring work and for acclimating new wood." };
245    },
246
247    // Drywall: joint compound dries by evaporation, slowly when it is cold or humid.
248    "drywall-drying": function (hs) {
249      var next = hs.slice(0, 12);
250      var rh = avgOf(next, "rh");
251      var t = avgOf(next, "f");
252      if (rh == null) return null;
253      var cold = t != null && t < 50 ? " In unheated spaces such as garages, the cold slows drying further." : "";
254      if (rh >= 70) return { level: "caution", text: "Humid air today (around " + rh + "% relative humidity). Joint compound dries more slowly in damp air, so allow extra time between coats, keep air moving, or use a setting-type compound that cures chemically." + cold };
255      if (rh <= 35) return { level: "good", text: "Dry air today (around " + rh + "% relative humidity), so joint compound dries quickly between coats." + cold };
256      return { level: "good", text: "Moderate humidity today (around " + rh + "%), normal drying time for joint compound between coats." + cold };
257    },
258
259    // Exterior doors: sealants and caulk want dry weather and roughly 40F and up.
260    "door-exterior": function (hs) {
261      var days = hs.slice(0, 24).filter(function (h) { return h.day; }).slice(0, 8);
262      if (!days.length) days = hs.slice(0, 8);
263      var lo = minOf(days, "f"), pop = maxOf(days, "pop");
264      if (lo == null) return null;
265      if (lo >= 40 && pop < 30) return { level: "good", text: "Good conditions for exterior door work today (" + lo + "°F and up, low rain chance). Most exterior sealants need about 40°F and dry weather to cure." };
266      if (lo < 40) return { level: "caution", text: "Cold for exterior sealants today (daytime low near " + lo + "°F). Most need about 40°F to cure; interior door work is unaffected." };
267      return { level: "caution", text: rainWords(pop) + " in the daytime hours, which is hard on fresh exterior sealant. Interior door work is unaffected." };
268    }
269  };
270
271  // ---------- render ----------
272  function el(tag, cls, text) {
273    var e = document.createElement(tag);
274    if (cls) e.className = cls;
275    if (text != null) e.appendChild(document.createTextNode(text));
276    return e;
277  }
278
279  function renderWeather(root) {
280    if (!CONFIG.weather || !window.fetch || !window.Promise) return;
281    var slot = root.querySelector("[data-lc-weather]");
282    if (!slot || slot.getAttribute("data-lc-live") === "1") return;
283    slot.setAttribute("data-lc-live", "1");
284    loadHours().then(function (hs) {
285      var rule = RULES[CONFIG.weather.rule];
286      var res = rule && rule(hs);
287      if (!res || !res.text) return;
288      var now = hs[0];
289      var p = el("p", "lc-live lc-" + res.level);
290      var lead = el("strong", null, "Today in " + CONFIG.weather.place + ": ");
291      
292      p.appendChild(lead);
293      p.appendChild(document.createTextNode("now " + now.f + "°F, " + (now.sf || "").toLowerCase() + ". " + res.text));
294      var src = el("p", "lc-src");
295      src.appendChild(document.createTextNode("Live forecast from the "));
296      var a = el("a", null, "National Weather Service");
297      a.href = "https://forecast.weather.gov/MapClick.php?lat=" + CONFIG.weather.lat + "&lon=" + CONFIG.weather.lon;
298      a.rel = "noopener";
299      a.target = "_blank";
300      src.appendChild(a);
301      src.appendChild(document.createTextNode(", refreshed when you load this page."));
302      while (slot.firstChild) slot.removeChild(slot.firstChild);
303      slot.appendChild(p);
304      slot.appendChild(src);
305    }, function () { /* keep the static fallback text */ });
306  }
307
308  function markSeason(root) {
309    var m = nowParts().month;
310    var items = root.querySelectorAll("[data-lc-months]");
311    for (var i = 0; i < items.length; i++) {
312      var months = String(items[i].getAttribute("data-lc-months")).split(",");
313      var on = false;
314      for (var k = 0; k < months.length; k++) if (parseInt(months[k], 10) === m) on = true;
315      if (on) {
316        items[i].classList.add("lc-now");
317        items[i].setAttribute("aria-current", "true");
318      } else {
319        items[i].classList.remove("lc-now");
320        items[i].removeAttribute("aria-current");
321      }
322    }
323  }
324
325  function enhance(root) {
326    markSeason(root);
327    renderWeather(root);
328  }
329
330  // ---------- fill mode ----------
331  function fillMode() {
332    var root = document.getElementById(ID);
333    if (root) enhance(root);
334  }
335
336  // ---------- inject mode ----------
337  function onHome() {
338    var p = window.location.pathname;
339    return p === "/" || p === "/index.html";
340  }
341
342  function hydrated(node) {
343    var keys = Object.keys(node);
344    for (var i = 0; i < keys.length; i++) if (keys[i].indexOf("__reactFiber$") === 0) return true;
345    return false;
346  }
347
348  function buildBlock() {
349    var tpl = document.createElement("div");
350    tpl.innerHTML = CONFIG.html;
351    return tpl.firstElementChild;
352  }
353
354  function host() {
355    var main = document.querySelector("main");
356    if (!main) return null;
357    var sel = CONFIG.insert && CONFIG.insert.before;
358    var ref = sel ? main.querySelector(sel) : null;
359    return { parent: ref ? ref.parentNode : main, ref: ref };
360  }
361
362  function place() {
363    var existing = document.getElementById(ID);
364    if (!onHome()) {
365      if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
366      return;
367    }
368    if (existing) return;
369    var h = host();
370    if (!h) return;
371    var block = buildBlock();
372    if (!block) return;
373    h.parent.insertBefore(block, h.ref);
374    enhance(block);
375  }
376
377  var queued = false;
378  function schedule() {
379    if (queued) return;
380    queued = true;
381    setTimeout(function () {
382      queued = false;
383      place();
384    }, 0);
385  }
386
387  function go() {
388    place();
389    if (window.MutationObserver && document.body) {
390      new MutationObserver(schedule).observe(document.body, { childList: true, subtree: true });
391    }
392  }
393
394  function injectMode() {
395    var t0 = Date.now();
396    (function tick() {
397      var main = document.querySelector("main");
398      if (main && hydrated(main)) return go();
399      var waited = Date.now() - t0;
400      var booted = !!(window.next && window.next.version);
401      if (!booted && waited > 8000 && document.readyState === "complete") return go();
402      if (waited > 30000) return go();
403      setTimeout(tick, 120);
404    })();
405  }
406
407  var started = false;
408  function begin() {
409    if (started) return;
410    started = true;
411    if (CONFIG.mode === "inject") injectMode();
412    else fillMode();
413  }
414
415  if (document.readyState === "complete" || document.readyState === "interactive") {
416    begin();
417  } else {
418    document.addEventListener("DOMContentLoaded", begin);
419    window.addEventListener("load", begin);
420  }
421
422})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.