diff --git a/cmd/ingestd/web.go b/cmd/ingestd/web.go index 1a6c277..e11b604 100644 --- a/cmd/ingestd/web.go +++ b/cmd/ingestd/web.go @@ -47,6 +47,7 @@ func runWebServer(ctx context.Context, d *db.DB, site providers.Site, model, add } mux := http.NewServeMux() + staticFiles := http.FileServer(http.FS(sub)) mux.HandleFunc("/api/dashboard", ws.handleDashboard) mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -58,7 +59,8 @@ func runWebServer(ctx context.Context, d *db.DB, site providers.Site, model, add mux.HandleFunc("/chart/", func(w http.ResponseWriter, r *http.Request) { serveIndex(w, sub) }) - mux.Handle("/", http.FileServer(http.FS(sub))) + mux.Handle("/vendor/", withCacheControl("public, max-age=31536000, immutable", staticFiles)) + mux.Handle("/", staticFiles) srv := &http.Server{ Addr: addr, @@ -196,3 +198,10 @@ func parseTimeParam(v string) (time.Time, error) { } return time.Time{}, errors.New("unsupported time format") } + +func withCacheControl(cacheControl string, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Cache-Control", cacheControl) + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/ingestd/web/app.js b/cmd/ingestd/web/app.js index 83dea8f..994493b 100644 --- a/cmd/ingestd/web/app.js +++ b/cmd/ingestd/web/app.js @@ -363,6 +363,10 @@ function sum(values) { return seen ? total : null; } +function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} + function updateText(id, text) { const el = document.getElementById(id); if (el) el.textContent = text; @@ -430,48 +434,123 @@ function lastNonNull(points, key) { return null; } -function computeRainProbability(latest, pressureTrend1h) { - if (!latest) { +function classifyRainProbability(prob) { + if (prob >= 0.6) return "High"; + if (prob >= 0.35) return "Medium"; + return "Low"; +} + +function computeDewPointC(tempC, rh) { + if (tempC === null || tempC === undefined || rh === null || rh === undefined) { + return null; + } + const safeRh = clamp(rh, 1, 100); + const a = 17.625; + const b = 243.04; + const gamma = Math.log(safeRh / 100) + (a * tempC) / (b + tempC); + return (b * gamma) / (a - gamma); +} + +function computeRainProbabilityFromInputs(tempC, rh, pressureHpa) { + if (tempC === null || tempC === undefined || rh === null || rh === undefined || pressureHpa === null || pressureHpa === undefined) { return null; } - let prob = 0.1; - if (pressureTrend1h !== null && pressureTrend1h !== undefined) { - if (pressureTrend1h <= -3.0) { - prob += 0.5; - } else if (pressureTrend1h <= -2.0) { - prob += 0.35; - } else if (pressureTrend1h <= -1.0) { - prob += 0.2; - } else if (pressureTrend1h <= -0.5) { - prob += 0.1; - } + const dewPointC = computeDewPointC(tempC, rh); + if (dewPointC === null) { + return null; } - if (latest.rh !== null && latest.rh !== undefined) { - if (latest.rh >= 95) { - prob += 0.2; - } else if (latest.rh >= 90) { - prob += 0.15; - } else if (latest.rh >= 85) { - prob += 0.1; - } + const saturationSpread = Math.max(0, tempC - dewPointC); + const humidityFactor = clamp((rh - 55) / 45, 0, 1); + const pressureFactor = clamp((1016 - pressureHpa) / 18, 0, 1); + const saturationFactor = clamp((6 - saturationSpread) / 6, 0, 1); + + const score = 0.45 * humidityFactor + 0.35 * pressureFactor + 0.2 * saturationFactor; + const prob = clamp(0.02 + 0.93 * score, 0.02, 0.98); + + return { prob, label: classifyRainProbability(prob) }; +} + +function computeRainProbability(latest) { + if (!latest) { + return null; + } + return computeRainProbabilityFromInputs(latest.temp_c, latest.rh, latest.pressure_hpa); +} + +function buildRainProbabilitySeries(points) { + const out = []; + for (const p of points) { + const t = new Date(p.ts).getTime(); + if (Number.isNaN(t)) continue; + const rp = computeRainProbabilityFromInputs(p.temp_c, p.rh, p.pressure_hpa); + out.push({ + x: t, + y: rp ? Math.round(rp.prob * 1000) / 10 : null, + }); + } + return out; +} + +function updateWeatherIcons(latest, rainProb) { + const sunEl = document.getElementById("live-icon-sun"); + const cloudEl = document.getElementById("live-icon-cloud"); + const rainEl = document.getElementById("live-icon-rain"); + const textEl = document.getElementById("live-weather-text"); + + [sunEl, cloudEl, rainEl].forEach((el) => { + if (el) el.classList.remove("active"); + }); + + if (!latest) { + if (textEl) textEl.textContent = "--"; + return; } - if (latest.wind_m_s !== null && latest.wind_m_s !== undefined && latest.wind_m_s >= 6) { - prob += 0.05; + const prob = rainProb ? rainProb.prob : null; + const rh = latest.rh; + const pressure = latest.pressure_hpa; + const uvi = latest.uvi; + + let sunActive = false; + let cloudActive = false; + let rainActive = false; + let label = "Partly cloudy"; + + if (prob !== null && prob >= 0.6) { + rainActive = true; + cloudActive = true; + label = "Rain likely"; + } else if ( + (prob !== null && prob >= 0.35) || + (rh !== null && rh !== undefined && rh >= 80) || + (pressure !== null && pressure !== undefined && pressure <= 1008) + ) { + cloudActive = true; + label = "Cloudy"; + } else if ( + uvi !== null && + uvi !== undefined && + uvi >= 4 && + rh !== null && + rh !== undefined && + rh < 75 && + pressure !== null && + pressure !== undefined && + pressure >= 1012 + ) { + sunActive = true; + label = "Sunny"; + } else { + sunActive = true; + cloudActive = true; } - prob = Math.max(0.05, Math.min(0.95, prob)); - - let label = "Low"; - if (prob >= 0.6) { - label = "High"; - } else if (prob >= 0.35) { - label = "Medium"; - } - - return { prob, label }; + if (sunEl) sunEl.classList.toggle("active", sunActive); + if (cloudEl) cloudEl.classList.toggle("active", cloudActive); + if (rainEl) rainEl.classList.toggle("active", rainActive); + if (textEl) textEl.textContent = label; } function extendForecastTo(points, endTime) { @@ -607,12 +686,13 @@ function renderDashboard(data) { const forecast = filterRange(forecastAll, rangeStart, rangeEnd); const forecastLine = extendForecastTo(forecast, rangeEnd); const lastPressureTrend = lastNonNull(obsFiltered, "pressure_trend_1h"); - const rainProb = computeRainProbability(latest, lastPressureTrend); + const rainProb = computeRainProbability(latest); if (rainProb) { updateText("live-rain-prob", `${Math.round(rainProb.prob * 100)}% (${rainProb.label})`); } else { updateText("live-rain-prob", "--"); } + updateWeatherIcons(latest, rainProb); updateText("baro-outlook", describeBarometer(latest ? latest.pressure_hpa : null, lastPressureTrend)); const obsTemps = obsFiltered.map((p) => p.temp_c); @@ -757,9 +837,12 @@ function renderDashboard(data) { upsertChart("chart-power", powerChart); const rainOptions = baseOptions(range); + rainOptions.scales.y.ticks.color = colors.rain; + rainOptions.scales.y.title = { display: true, text: "Observed Rain (mm)", color: colors.rain }; rainOptions.scales.y1 = { position: "right", - ticks: { color: "#a4c4c4" }, + ticks: { color: colors.forecast }, + title: { display: true, text: "Forecast Rain (mm)", color: colors.forecast }, grid: { drawOnChartArea: false }, }; @@ -788,7 +871,7 @@ function renderDashboard(data) { label: "forecast precip (mm)", data: series(forecastRain, "precip_mm"), backgroundColor: colors.forecast, - yAxisID: "y", + yAxisID: "y1", }, ], }, @@ -796,6 +879,31 @@ function renderDashboard(data) { }; upsertChart("chart-rain", rainChart); + const rainProbOptions = baseOptions(range); + rainProbOptions.scales.y.min = 0; + rainProbOptions.scales.y.max = 100; + rainProbOptions.scales.y.ticks.color = colors.rain; + rainProbOptions.scales.y.ticks.callback = (value) => `${value}%`; + rainProbOptions.scales.y.title = { display: true, text: "Probability (%)", color: colors.rain }; + + const rainProbChart = { + type: "line", + data: { + datasets: [ + { + label: "predicted rain probability (%)", + data: buildRainProbabilitySeries(obsFiltered), + borderColor: colors.rain, + backgroundColor: "rgba(78, 168, 222, 0.18)", + fill: true, + yAxisID: "y", + }, + ], + }, + options: rainProbOptions, + }; + upsertChart("chart-rain-prob", rainProbChart); + updateSingleChartMode(); } diff --git a/cmd/ingestd/web/index.html b/cmd/ingestd/web/index.html index fc928d3..5f82823 100644 --- a/cmd/ingestd/web/index.html +++ b/cmd/ingestd/web/index.html @@ -9,9 +9,7 @@ - - - +
@@ -50,6 +48,15 @@