remove web dependencies

This commit is contained in:
2026-02-06 16:13:54 +11:00
parent 730811b76e
commit c68c063ff1
11 changed files with 265 additions and 42 deletions

View File

@@ -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)
})
}

View File

@@ -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();
}

View File

@@ -9,9 +9,7 @@
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/vendor/fonts/fonts.css">
<link rel="stylesheet" href="/styles.css">
</head>
<body>
@@ -50,6 +48,15 @@
<div class="panel-meta" id="last-updated">--</div>
</div>
<div class="live-grid">
<div class="metric metric-weather">
<div class="label">Weather</div>
<div class="weather-icons" aria-label="weather icons">
<span class="weather-icon" id="live-icon-sun" title="Sunny">☀️</span>
<span class="weather-icon" id="live-icon-cloud" title="Cloudy">☁️</span>
<span class="weather-icon" id="live-icon-rain" title="Rain">🌧️</span>
</div>
<div class="weather-caption" id="live-weather-text">--</div>
</div>
<div class="metric">
<div class="label">Temp C</div>
<div class="value" id="live-temp">--</div>
@@ -189,12 +196,21 @@
<canvas id="chart-rain"></canvas>
</div>
</div>
<div class="chart-card" data-chart="chart-rain-prob">
<div class="chart-header">
<div class="chart-title">Predicted Rain Probability (Observed Inputs)</div>
<button class="chart-link" data-chart="chart-rain-prob" title="Copy chart link">Share</button>
</div>
<div class="chart-canvas">
<canvas id="chart-rain-prob"></canvas>
</div>
</div>
</div>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/vendor/js/chart.umd.min.js"></script>
<script src="/vendor/js/chartjs-adapter-date-fns.bundle.min.js"></script>
<script src="/app.js"></script>
</body>
</html>

View File

@@ -177,6 +177,38 @@ body {
margin-top: 6px;
}
.metric-weather .label {
margin-bottom: 8px;
}
.weather-icons {
display: flex;
align-items: center;
gap: 10px;
min-height: 28px;
}
.weather-icon {
font-size: 24px;
line-height: 1;
opacity: 0.28;
filter: saturate(0.65);
transition: opacity 0.2s ease, transform 0.2s ease, filter 0.2s ease;
}
.weather-icon.active {
opacity: 1;
filter: saturate(1);
transform: translateY(-1px);
}
.weather-caption {
margin-top: 8px;
color: var(--muted);
font-size: 13px;
font-family: "IBM Plex Mono", monospace;
}
.callouts {
display: grid;
gap: 12px;

31
cmd/ingestd/web/vendor/fonts/fonts.css vendored Normal file
View File

@@ -0,0 +1,31 @@
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/vendor/fonts/ibm-plex-mono-400.ttf") format("truetype");
}
@font-face {
font-family: "IBM Plex Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/vendor/fonts/ibm-plex-mono-600.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/vendor/fonts/space-grotesk-400.ttf") format("truetype");
}
@font-face {
font-family: "Space Grotesk";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/vendor/fonts/space-grotesk-600.ttf") format("truetype");
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long