Merge Forecast into Live, four tabs; state solo-project policy

The Live tab now carries the observed-and-forecast chart (a superset of the
old rolling window, which plotted the same observed series without the
prediction) and the precipitation panel. Estimator internals moves to Models,
beside the calibration input that sets the coefficient it reports. The seven
day outlook moves to History. Verified in Chromium at 1600x900: all four tabs
scrollHeight 900 against innerHeight 900, zero clipped elements outside
internal scrollers, zero console errors.

CONTRIBUTING.md now states plainly that this is a solo project: bug reports
welcome, pull requests unlikely to be merged, fork it instead. No other
developer was ever named anywhere in the repository.
This commit is contained in:
2026-08-15 21:17:50 +01:00
parent dbbea1a95f
commit 49c0aee2e1
4 changed files with 92 additions and 119 deletions
+29 -10
View File
@@ -1,7 +1,21 @@
# Contributing to Ashvale Station # Contributing to Ashvale Station
Thanks for taking an interest. This is a small project maintained by one person, Read this first, so nobody wastes an afternoon.
so the bar here is "make it easy to say yes", not "follow a 40-page process".
**This is a solo project.** It is written and maintained by one person, for one
weather station, and it is published because the methods may be useful to
someone else, not because it is looking for a team.
**Bug reports are genuinely welcome.** If something crashes, forecasts badly, or
the documentation is wrong, open an issue. That is useful and I will read it.
**Pull requests are unlikely to be merged.** Not from lack of gratitude: this
codebase carries a lot of hard-won reasoning in its comments and docstrings, and
reviewing changes to it properly costs more time than I have. If you want it to
do something different, fork it. Apache 2.0 exists precisely so you can.
The rest of this file documents how the project holds itself to a standard. It
is written for anyone reading the code, including future me.
## Ground rules that actually matter ## Ground rules that actually matter
@@ -54,7 +68,10 @@ python run.py --no-led # dashboard on :8000
No Sense HAT needed. The simulator kicks in automatically and exercises every No Sense HAT needed. The simulator kicks in automatically and exercises every
code path. code path.
## Before you open a pull request ## The bar any change has to clear
Whether it is my own commit or a fork of yours, a change to the forecasting
path is not finished until it can show:
- [ ] `python scripts/evaluate.py` runs clean, and you have posted before/after numbers - [ ] `python scripts/evaluate.py` runs clean, and you have posted before/after numbers
- [ ] Those numbers came from a backfill with `--seed` and `--end` both pinned - [ ] Those numbers came from a backfill with `--seed` and `--end` both pinned
@@ -63,16 +80,18 @@ code path.
- [ ] New model code explains its failure mode in the docstring - [ ] New model code explains its failure mode in the docstring
- [ ] The dashboard still fits one viewport at 1280x800 if you changed the UI - [ ] The dashboard still fits one viewport at 1280x800 if you changed the UI
## Good first contributions ## Roadmap
Where this is going, in rough order of value. Listed so a forker knows what is
already planned rather than as an invitation.
- **DS18B20 or BME280 support.** An outdoor sensor removes the single biggest - **DS18B20 or BME280 support.** An outdoor sensor removes the single biggest
limitation in the project. High impact, self-contained. limitation in the project. High impact, self-contained.
- **Tipping-bucket rain gauge on GPIO.** Real precipitation labels would - **Tipping-bucket rain gauge on GPIO.** Real precipitation labels would
transform the precipitation model. transform the precipitation model.
- **METAR ingestion** from a nearby airfield as a calibration reference. - **METAR ingestion** from a nearby airfield as a calibration reference.
- **Translations** for the dashboard.
- **Tests.** There is a walk-forward backtest but no unit test suite. A pytest - **Tests.** There is a walk-forward backtest but no unit test suite. A pytest
suite over `physics.py`, `estimation.py` and `models/rls.py` would be very welcome. suite over `physics.py`, `estimation.py` and `models/rls.py` is the main gap.
## Reporting bugs ## Reporting bugs
@@ -81,8 +100,8 @@ output of `GET /api/status`, and what you expected instead. If it is a
forecasting problem rather than a crash, the output of `scripts/evaluate.py` forecasting problem rather than a crash, the output of `scripts/evaluate.py`
helps enormously. helps enormously.
## Licensing of contributions ## Licensing
By contributing you agree that your work is licensed under the Apache License The project is Apache 2.0. Fork it, modify it, ship it, subject to the licence
2.0, the same terms as the project. You keep the copyright in your own terms. In the unlikely event a patch is accepted, it is taken under the same
contributions. terms and you keep the copyright in your own work.
+7 -4
View File
@@ -160,18 +160,21 @@ permanently.
## The dashboard ## The dashboard
Five tabs, one viewport, no scrolling on desktop. Below 1024 px the constraint is Four tabs, one viewport, no scrolling on desktop. Below 1024 px the constraint is
released, because pinning five panels into a phone viewport produces unreadable released, because pinning four panels into a phone viewport produces unreadable
eight-pixel type. eight-pixel type.
| Tab | Answers | | Tab | Answers |
| --- | --- | | --- | --- |
| **Live** | What is it doing right now | | **Live** | What is it doing now, what it expects next, and how sure it is |
| **Forecast** | What is it about to do, and how sure are we |
| **History** | What did it do, over any timeframe you ask for | | **History** | What did it do, over any timeframe you ask for |
| **Models** | Has the model earned its confidence | | **Models** | Has the model earned its confidence |
| **Methods** | How the whole thing is wired, and how each stage fails | | **Methods** | How the whole thing is wired, and how each stage fails |
Live carries the current readings, the observed-and-forecast chart with its 90%
conformal band, and the precipitation panel together, so the question "what is
it doing and what happens next" is answered without changing tab.
### History ### History
Presets from 6 hours to a year, plus an explicit from/to range picker. Aggregation Presets from 6 hours to a year, plus an explicit from/to range picker. Aggregation
+55 -104
View File
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""The dashboard: five tabs, one viewport, no scrolling. """The dashboard: four tabs, one viewport, no scrolling.
Layout contract. The page is a fixed three-row grid pinned to the Layout contract. The page is a fixed three-row grid pinned to the
viewport height: header, tab bar, then a content region that takes the viewport height: header, tab bar, then a content region that takes the
@@ -119,7 +119,6 @@ DASHBOARD_HTML = r"""
<nav role="tablist" class="glass rounded-2xl p-1.5 flex gap-1.5 overflow-x-auto"> <nav role="tablist" class="glass rounded-2xl p-1.5 flex gap-1.5 overflow-x-auto">
<button role="tab" data-tab="live" aria-selected="true" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">Live</button> <button role="tab" data-tab="live" aria-selected="true" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">Live</button>
<button role="tab" data-tab="forecast" aria-selected="false" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">Forecast</button>
<button role="tab" data-tab="history" aria-selected="false" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">History</button> <button role="tab" data-tab="history" aria-selected="false" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">History</button>
<button role="tab" data-tab="models" aria-selected="false" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">Models</button> <button role="tab" data-tab="models" aria-selected="false" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">Models</button>
<button role="tab" data-tab="methods" aria-selected="false" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">Methods</button> <button role="tab" data-tab="methods" aria-selected="false" class="tabbtn shrink-0 px-4 py-2 rounded-xl text-xs font-semibold text-slate-400 border border-transparent hover:text-slate-200">Methods</button>
@@ -191,65 +190,17 @@ DASHBOARD_HTML = r"""
</div> </div>
<div class="glass rounded-2xl p-4 lg:col-span-3 flex flex-col min-h-0"> <div class="glass rounded-2xl p-4 lg:col-span-3 flex flex-col min-h-0">
<div class="flex items-center justify-between pb-2 mb-2 border-b border-slate-800 shrink-0">
<div class="flex items-center gap-2">
<h2 class="text-sm font-bold">Rolling window</h2>
<span class="px-1.5 py-0.5 text-[9px] font-mono rounded bg-emerald-500/10 text-emerald-300 border border-emerald-500/20 uppercase">2 s stream</span>
</div>
<div class="flex gap-1" id="live-span">
<button data-min="60" class="px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 bg-indigo-600/20 text-indigo-300">1 h</button>
<button data-min="360" class="px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 text-slate-400 hover:text-slate-200">6 h</button>
<button data-min="1440" class="px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 text-slate-400 hover:text-slate-200">24 h</button>
</div>
</div>
<div class="flex-1 min-h-0 relative"><canvas id="liveChart"></canvas></div>
</div>
<div class="glass rounded-2xl p-4 flex flex-col min-h-0">
<div class="pb-2 mb-2 border-b border-slate-800 shrink-0">
<h2 class="text-sm font-bold">Estimator internals</h2>
<p class="text-[10px] text-indigo-400 font-mono">what the filter is doing right now</p>
</div>
<div class="flex-1 min-h-0 scroller space-y-2 font-mono text-[10px] pr-1">
<div class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5">
<div class="flex justify-between text-slate-400"><span>self-heating k</span><span id="e-k" class="text-emerald-300 font-bold">--</span></div>
<div class="flex justify-between text-slate-500 mt-1"><span>cpu offset</span><span id="e-off">--</span></div>
<div class="text-[9px] text-slate-600 mt-1.5 leading-snug">Removes the SoC bias. Calibrate it on the Models tab.</div>
</div>
<div class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5">
<div class="flex justify-between text-slate-400 mb-1"><span>novelty d&sup2;</span><span id="e-nov" class="text-slate-200 font-bold">--</span></div>
<div class="h-1.5 bg-slate-950 rounded-full overflow-hidden border border-slate-800"><div id="e-nov-bar" class="tick h-full bg-gradient-to-r from-emerald-500 to-amber-500" style="width:0%"></div></div>
<div class="flex justify-between text-slate-400 mt-2 mb-1"><span>drift pressure</span><span id="e-drift" class="text-slate-200 font-bold">--</span></div>
<div class="h-1.5 bg-slate-950 rounded-full overflow-hidden border border-slate-800"><div id="e-drift-bar" class="tick h-full bg-gradient-to-r from-indigo-500 to-rose-500" style="width:0%"></div></div>
<div class="text-[9px] text-slate-600 mt-1.5 leading-snug">Novelty is a multivariate departure from the recent norm. Drift reaching 100% queues a retrain.</div>
</div>
<div id="e-health" class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5 space-y-1"></div>
</div>
</div>
<div class="glass rounded-2xl px-4 py-2.5 lg:col-span-4 grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-10 gap-x-4 gap-y-1.5 font-mono text-[10px]">
<div><div class="text-slate-600 uppercase">wet bulb</div><div id="d-wb" class="text-slate-200 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">vpd</div><div id="d-vpd" class="text-slate-200 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">abs hum</div><div id="d-ah" class="text-slate-200 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">heat idx</div><div id="d-hi" class="text-slate-200 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">solar el</div><div id="d-el" class="text-slate-200 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">pitch</div><div id="d-pitch" class="text-amber-300 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">roll</div><div id="d-roll" class="text-cyan-300 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">yaw</div><div id="d-yaw" class="text-indigo-300 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">compass</div><div id="d-comp" class="text-emerald-300 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">accel z</div><div id="d-az" class="text-slate-200 font-semibold">--</div></div>
</div>
</section>
<!-- ---------------- FORECAST ---------------- -->
<section id="pane-forecast" class="pane h-full min-h-0 gap-3 grid-cols-1 lg:grid-cols-3 lg:grid-rows-[1fr_auto]">
<div class="glass rounded-2xl p-4 lg:col-span-2 flex flex-col min-h-0">
<div class="flex flex-wrap items-center justify-between gap-2 pb-2 mb-2 border-b border-slate-800 shrink-0"> <div class="flex flex-wrap items-center justify-between gap-2 pb-2 mb-2 border-b border-slate-800 shrink-0">
<div> <div class="flex items-center gap-2">
<h2 class="text-sm font-bold">Observed and forecast</h2> <h2 class="text-sm font-bold">Observed and forecast</h2>
<p class="text-[10px] text-slate-500 font-mono">shaded band is the 90% conformal interval</p> <span class="px-1.5 py-0.5 text-[9px] font-mono rounded bg-indigo-500/10 text-indigo-300 border border-indigo-500/20 uppercase">90% band</span>
</div> </div>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<div class="flex gap-1" id="live-span">
<button data-min="60" class="px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 text-slate-400 hover:text-slate-200">1 h</button>
<button data-min="360" class="px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 text-slate-400 hover:text-slate-200">6 h</button>
<button data-min="1440" class="px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 bg-indigo-600/20 text-indigo-300">24 h</button>
</div>
<select id="fc-target" class="bg-slate-900/90 border border-slate-800 text-slate-300 text-[11px] font-mono rounded-lg px-2 py-1"> <select id="fc-target" class="bg-slate-900/90 border border-slate-800 text-slate-300 text-[11px] font-mono rounded-lg px-2 py-1">
<option value="temperature">temperature</option><option value="humidity">humidity</option><option value="pressure">pressure</option> <option value="temperature">temperature</option><option value="humidity">humidity</option><option value="pressure">pressure</option>
</select> </select>
@@ -294,20 +245,22 @@ DASHBOARD_HTML = r"""
</div> </div>
</div> </div>
<div class="glass rounded-2xl p-4 lg:col-span-3 shrink-0"> <div class="glass rounded-2xl px-4 py-2.5 lg:col-span-4 grid grid-cols-3 sm:grid-cols-5 lg:grid-cols-10 gap-x-4 gap-y-1.5 font-mono text-[10px]">
<div class="flex items-center justify-between mb-2"> <div><div class="text-slate-600 uppercase">wet bulb</div><div id="d-wb" class="text-slate-200 font-semibold">--</div></div>
<div> <div><div class="text-slate-600 uppercase">vpd</div><div id="d-vpd" class="text-slate-200 font-semibold">--</div></div>
<h2 class="text-sm font-bold">Seven day outlook</h2> <div><div class="text-slate-600 uppercase">abs hum</div><div id="d-ah" class="text-slate-200 font-semibold">--</div></div>
<p class="text-[10px] text-slate-500 font-mono">climatology plus decaying anomaly, not a synoptic forecast</p> <div><div class="text-slate-600 uppercase">heat idx</div><div id="d-hi" class="text-slate-200 font-semibold">--</div></div>
</div> <div><div class="text-slate-600 uppercase">solar el</div><div id="d-el" class="text-slate-200 font-semibold">--</div></div>
<span id="o-badge" class="px-2 py-0.5 rounded-lg bg-amber-500/10 text-amber-300 border border-amber-500/20 text-[9px] font-mono uppercase font-semibold">warming up</span> <div><div class="text-slate-600 uppercase">pitch</div><div id="d-pitch" class="text-amber-300 font-semibold">--</div></div>
</div> <div><div class="text-slate-600 uppercase">roll</div><div id="d-roll" class="text-cyan-300 font-semibold">--</div></div>
<div id="o-strip" class="grid grid-cols-4 sm:grid-cols-7 gap-2"></div> <div><div class="text-slate-600 uppercase">yaw</div><div id="d-yaw" class="text-indigo-300 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">compass</div><div id="d-comp" class="text-emerald-300 font-semibold">--</div></div>
<div><div class="text-slate-600 uppercase">accel z</div><div id="d-az" class="text-slate-200 font-semibold">--</div></div>
</div> </div>
</section> </section>
<!-- ---------------- HISTORY ---------------- --> <!-- ---------------- HISTORY ---------------- -->
<section id="pane-history" class="pane h-full min-h-0 gap-3 grid-cols-1 lg:grid-cols-4 lg:grid-rows-[auto_1fr]"> <section id="pane-history" class="pane h-full min-h-0 gap-3 grid-cols-1 lg:grid-cols-4 lg:grid-rows-[auto_1fr_auto]">
<div class="glass rounded-2xl px-4 py-3 lg:col-span-4 flex flex-wrap items-end gap-3"> <div class="glass rounded-2xl px-4 py-3 lg:col-span-4 flex flex-wrap items-end gap-3">
<div class="flex gap-1 flex-wrap" id="h-presets"> <div class="flex gap-1 flex-wrap" id="h-presets">
<button data-h="6" class="px-2.5 py-1.5 rounded-lg text-[11px] font-mono border border-slate-800 text-slate-400 hover:text-slate-200">6 h</button> <button data-h="6" class="px-2.5 py-1.5 rounded-lg text-[11px] font-mono border border-slate-800 text-slate-400 hover:text-slate-200">6 h</button>
@@ -346,6 +299,17 @@ DASHBOARD_HTML = r"""
<div id="rec-daily" class="flex-1 min-h-0 scroller pr-1"></div> <div id="rec-daily" class="flex-1 min-h-0 scroller pr-1"></div>
<div id="rec-all" class="flex-1 min-h-0 scroller pr-1 hidden space-y-1.5"></div> <div id="rec-all" class="flex-1 min-h-0 scroller pr-1 hidden space-y-1.5"></div>
</div> </div>
<div class="glass rounded-2xl p-4 lg:col-span-4 shrink-0">
<div class="flex items-center justify-between mb-2">
<div>
<h2 class="text-sm font-bold">Seven day outlook</h2>
<p class="text-[10px] text-slate-500 font-mono">climatology plus decaying anomaly, not a synoptic forecast</p>
</div>
<span id="o-badge" class="px-2 py-0.5 rounded-lg bg-amber-500/10 text-amber-300 border border-amber-500/20 text-[9px] font-mono uppercase font-semibold">warming up</span>
</div>
<div id="o-strip" class="grid grid-cols-4 sm:grid-cols-7 gap-2"></div>
</div>
</section> </section>
<!-- ---------------- MODELS ---------------- --> <!-- ---------------- MODELS ---------------- -->
@@ -379,6 +343,11 @@ DASHBOARD_HTML = r"""
<div class="glass rounded-2xl p-4 flex flex-col min-h-0"> <div class="glass rounded-2xl p-4 flex flex-col min-h-0">
<div class="pb-2 mb-2 border-b border-slate-800 shrink-0"><h2 class="text-sm font-bold">Calibration and state</h2></div> <div class="pb-2 mb-2 border-b border-slate-800 shrink-0"><h2 class="text-sm font-bold">Calibration and state</h2></div>
<div class="flex-1 min-h-0 scroller space-y-2.5 pr-1 font-mono text-[10px]"> <div class="flex-1 min-h-0 scroller space-y-2.5 pr-1 font-mono text-[10px]">
<div class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5">
<div class="flex justify-between text-slate-400"><span>self-heating k</span><span id="e-k" class="text-emerald-300 font-bold">--</span></div>
<div class="flex justify-between text-slate-500 mt-1"><span>cpu offset</span><span id="e-off">--</span></div>
<div class="text-[9px] text-slate-600 mt-1.5 leading-snug">Removes the SoC bias. Set it from the reading below.</div>
</div>
<div class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5 space-y-2"> <div class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5 space-y-2">
<div class="text-slate-400">Trusted thermometer reading</div> <div class="text-slate-400">Trusted thermometer reading</div>
<div class="flex gap-1.5"> <div class="flex gap-1.5">
@@ -396,6 +365,14 @@ DASHBOARD_HTML = r"""
<div class="text-slate-400 mb-1.5">Precipitation coefficients</div> <div class="text-slate-400 mb-1.5">Precipitation coefficients</div>
<div id="m-coef" class="space-y-0.5"></div> <div id="m-coef" class="space-y-0.5"></div>
</div> </div>
<div class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5">
<div class="flex justify-between text-slate-400 mb-1"><span>novelty d&sup2;</span><span id="e-nov" class="text-slate-200 font-bold">--</span></div>
<div class="h-1.5 bg-slate-950 rounded-full overflow-hidden border border-slate-800"><div id="e-nov-bar" class="tick h-full bg-gradient-to-r from-emerald-500 to-amber-500" style="width:0%"></div></div>
<div class="flex justify-between text-slate-400 mt-2 mb-1"><span>drift pressure</span><span id="e-drift" class="text-slate-200 font-bold">--</span></div>
<div class="h-1.5 bg-slate-950 rounded-full overflow-hidden border border-slate-800"><div id="e-drift-bar" class="tick h-full bg-gradient-to-r from-indigo-500 to-rose-500" style="width:0%"></div></div>
<div class="text-[9px] text-slate-600 mt-1.5 leading-snug">Novelty is a multivariate departure from the recent norm. Drift reaching 100% queues a retrain.</div>
</div>
<div id="e-health" class="bg-slate-900/70 rounded-xl border border-slate-800/80 p-2.5 space-y-1"></div>
</div> </div>
</div> </div>
@@ -464,43 +441,19 @@ function pushSpark(k,v) {
sparks[k].update('none'); sparks[k].update('none');
} }
charts.live = new Chart(el('liveChart').getContext('2d'), { // The Live tab now carries the forecast chart, which already plots observed
type:'line', // history alongside the prediction, so a separate rolling-window chart would
data:{ datasets:[ // only duplicate its left half. These buttons resize the observed portion.
{ label:'temperature', data:[], borderColor:'#f59e0b', backgroundColor:'rgba(245,158,11,.10)', let liveMin = 1440;
fill:true, borderWidth:2, pointRadius:0, tension:.3, parsing:false, yAxisID:'y' },
{ label:'humidity', data:[], borderColor:'#06b6d4', borderWidth:2, pointRadius:0,
tension:.3, parsing:false, yAxisID:'y1' } ]},
options:{ responsive:true, maintainAspectRatio:false, animation:false,
interaction:{ mode:'index', intersect:false },
scales:{
x:{ type:'linear', grid:{color:GRID}, ticks:{ color:'#64748b', font:MONO, maxTicksLimit:7,
callback:v=>new Date(v*1000).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) } },
y:{ position:'left', grid:{color:GRID}, ticks:{ color:'#f59e0b', font:MONO, callback:v=>v.toFixed(1)+'\u00b0' } },
y1:{ position:'right', grid:{drawOnChartArea:false}, ticks:{ color:'#06b6d4', font:MONO, callback:v=>v.toFixed(0)+'%' } } },
plugins:{ legend:{display:false},
tooltip:{ backgroundColor:'rgba(15,23,42,.95)', borderColor:'rgba(255,255,255,.1)', borderWidth:1,
titleFont:MONO, bodyFont:{family:'Plus Jakarta Sans',size:11},
callbacks:{ title:i=>new Date(i[0].parsed.x*1000).toLocaleTimeString() } } } }
});
let liveMin = 60;
el('live-span').addEventListener('click', e => { el('live-span').addEventListener('click', e => {
const b = e.target.closest('button'); if (!b) return; const b = e.target.closest('button'); if (!b) return;
liveMin = Number(b.dataset.min); liveMin = Number(b.dataset.min);
[...el('live-span').children].forEach(x => x.className = [...el('live-span').children].forEach(x => x.className =
'px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 ' + 'px-2 py-1 rounded-lg text-[10px] font-mono border border-slate-800 ' +
(x===b ? 'bg-indigo-600/20 text-indigo-300' : 'text-slate-400 hover:text-slate-200')); (x===b ? 'bg-indigo-600/20 text-indigo-300' : 'text-slate-400 hover:text-slate-200'));
loadLiveChart(); loadForecast();
}); });
async function loadLiveChart() { loaders.live = loadForecast;
const r = await fetch('/api/history/range?hours='+(liveMin/60)).then(r=>r.json());
const s = r.series||{};
charts.live.data.datasets[0].data = (s.ts||[]).map((t,i)=>({x:t,y:s.temp[i]})).filter(p=>p.y!=null);
charts.live.data.datasets[1].data = (s.ts||[]).map((t,i)=>({x:t,y:s.hum[i]})).filter(p=>p.y!=null);
charts.live.update('none');
}
loaders.live = loadLiveChart;
function setFlash(id,val) { function setFlash(id,val) {
const node = el(id); if (!node || node.innerText===val) return; const node = el(id); if (!node || node.innerText===val) return;
@@ -616,7 +569,7 @@ async function loadForecast() {
const target = el('fc-target').value; const target = el('fc-target').value;
const key = {temperature:'temp', humidity:'hum', pressure:'press'}[target]; const key = {temperature:'temp', humidity:'hum', pressure:'press'}[target];
const res = await Promise.all([ const res = await Promise.all([
fetch('/api/history/range?hours=24').then(r=>r.json()), fetch('/api/history/range?hours='+(liveMin/60)).then(r=>r.json()),
fetch('/api/forecast').then(r=>r.json()) ]); fetch('/api/forecast').then(r=>r.json()) ]);
const hist = res[0], fc = res[1], s = hist.series||{}; const hist = res[0], fc = res[1], s = hist.series||{};
charts.fan.data.datasets[0].data = (s.ts||[]).map((t,i)=>({x:t,y:s[key][i]})).filter(p=>p.y!=null); charts.fan.data.datasets[0].data = (s.ts||[]).map((t,i)=>({x:t,y:s[key][i]})).filter(p=>p.y!=null);
@@ -655,7 +608,6 @@ async function loadOutlook() {
'<div class="text-[9px] text-indigo-400/80 font-mono">&plusmn;'+sp.toFixed(1)+'</div></div>'; '<div class="text-[9px] text-indigo-400/80 font-mono">&plusmn;'+sp.toFixed(1)+'</div></div>';
}).join(''); }).join('');
} }
loaders.forecast = () => { loadForecast(); loadOutlook(); };
document.querySelectorAll('.rain-label').forEach(b => b.addEventListener('click', async () => { document.querySelectorAll('.rain-label').forEach(b => b.addEventListener('click', async () => {
const res = await fetch('/api/label', { method:'POST', headers:{'Content-Type':'application/json'}, const res = await fetch('/api/label', { method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({kind:'rain', value:Number(b.dataset.label)}) }).then(r=>r.json()); body: JSON.stringify({kind:'rain', value:Number(b.dataset.label)}) }).then(r=>r.json());
@@ -802,7 +754,7 @@ function toggleRec(daily) {
} }
el('rec-tab-daily').addEventListener('click', ()=>toggleRec(true)); el('rec-tab-daily').addEventListener('click', ()=>toggleRec(true));
el('rec-tab-all').addEventListener('click', ()=>toggleRec(false)); el('rec-tab-all').addEventListener('click', ()=>toggleRec(false));
loaders.history = () => { if (!histData) loadHistory(); }; loaders.history = () => { if (!histData) loadHistory(); loadOutlook(); };
/* ---------------- MODELS ---------------- */ /* ---------------- MODELS ---------------- */
async function loadModels() { async function loadModels() {
@@ -964,12 +916,11 @@ loaders.methods = loadMethods;
/* ---------------- BOOT ---------------- */ /* ---------------- BOOT ---------------- */
connectStream(); connectStream();
loadLiveChart(); loadForecast();
fetch('/api/status').then(r=>r.json()).then(s => { el('hd-days').innerText = fmt(s.history_days,2); }); fetch('/api/status').then(r=>r.json()).then(s => { el('hd-days').innerText = fmt(s.history_days,2); });
setInterval(() => { if (activeTab==='live') loadLiveChart(); }, 60000); setInterval(() => { if (activeTab==='live') loadForecast(); }, 60000);
setInterval(() => { if (activeTab==='forecast') { loadForecast(); loadOutlook(); } }, 120000);
setInterval(() => { if (activeTab==='models') loadModels(); }, 60000); setInterval(() => { if (activeTab==='models') loadModels(); }, 60000);
setInterval(() => { if (activeTab==='history') loadHistory(); }, 300000); setInterval(() => { if (activeTab==='history') { loadHistory(); loadOutlook(); } }, 300000);
</script> </script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -221,7 +221,7 @@ def pipeline(cfg) -> List[Dict[str, Any]]:
"label abstains in the ambiguous middle rather than " "label abstains in the ambiguous middle rather than "
"guessing, because a poisoned training set costs more than " "guessing, because a poisoned training set costs more than "
"the extra samples buy. Trust grows as n/(n+25) in strong " "the extra samples buy. Trust grows as n/(n+25) in strong "
"labels, so the two buttons on the Forecast tab matter.", "labels, so the two buttons on the Live tab matter.",
"params": {"prior": "Zambretti, three-branch", "params": {"prior": "Zambretti, three-branch",
"learner": "logistic, AdaGrad", "learner": "logistic, AdaGrad",
"strong label weight": "10x proxy"}, "strong label weight": "10x proxy"},