Removes the SoC bias. Set it from the reading below.
-
Station log
-
+
+
Estimator
+
+
novelty d²--
+
+
drift pressure--
+
+
+
+
+
+
+
Storage and weights
+
+
+
+
@@ -453,7 +465,7 @@ el('live-span').addEventListener('click', e => {
(x===b ? 'bg-indigo-600/20 text-indigo-300' : 'text-slate-400 hover:text-slate-200'));
loadForecast();
});
-loaders.live = loadForecast;
+loaders.live = () => { loadForecast(); loadOutlook(); };
function setFlash(id,val) {
const node = el(id); if (!node || node.innerText===val) return;
@@ -491,6 +503,8 @@ function applyTelemetry(d) {
el('d-az').innerText = fmt(d.accel && d.accel.z,2);
el('e-k').innerText = fmt(d.compensator_k,4);
+ el('e-hoff').innerText = (d.hum_offset===undefined||d.hum_offset===null) ? '--' : (d.hum_offset>=0?'+':'')+fmt(d.hum_offset,2)+'%';
+ el('e-hpsy').innerText = (d.hum_psychrometric===undefined||d.hum_psychrometric===null) ? '--' : (d.hum_psychrometric>=0?'+':'')+fmt(d.hum_psychrometric,2)+'%';
el('e-off').innerText = fmt(d.cpu_offset,1)+' K';
el('e-nov').innerText = fmt(d.novelty_d2,1);
el('e-nov-bar').style.width = Math.min((d.novelty_d2||0)/24,1)*100+'%';
@@ -754,7 +768,7 @@ function toggleRec(daily) {
}
el('rec-tab-daily').addEventListener('click', ()=>toggleRec(true));
el('rec-tab-all').addEventListener('click', ()=>toggleRec(false));
-loaders.history = () => { if (!histData) loadHistory(); loadOutlook(); };
+loaders.history = () => { if (!histData) loadHistory(); };
/* ---------------- MODELS ---------------- */
async function loadModels() {
@@ -768,19 +782,31 @@ async function loadModels() {
(md.nowcast||[]).forEach(h => { wmap[h.target+'@'+h.horizon_s] = h.weights; });
const rows = sc.rows||[];
el('m-empty').style.display = rows.length ? 'none' : 'block';
- el('m-score').innerHTML = rows.map(r=>{
- const sk = r.skill||0;
- const cls = sk>0.05?'text-emerald-300':sk<-0.05?'text-rose-300':'text-slate-400';
- const lead = r.horizon_s<3600 ? (r.horizon_s/60)+'m' : r.horizon_s<86400 ? (r.horizon_s/3600)+'h' : (r.horizon_s/86400)+'d';
- const w = wmap[r.target+'@'+r.horizon_s];
- const ws = w ? Math.round(w.persistence*100)+'/'+Math.round(w.climatology*100)+'/'+Math.round(w.learned*100) : '-';
- return '
| '+r.target+' | '+
- ''+lead+' | '+fmt(r.mae,3)+' | '+
- ''+fmt(r.mae_persistence,3)+' | '+
- ''+Math.round(sk*100)+'% | '+
- ''+Math.round((r.coverage||0)*100)+'% | '+
- ''+r.n+' | '+
- ''+ws+' |
';
+ // One column per target rather than one 18-row table: every head stays
+ // visible instead of hiding behind a scrollbar.
+ const byTarget = {};
+ rows.forEach(r => { (byTarget[r.target] = byTarget[r.target] || []).push(r); });
+ el('m-score').innerHTML = Object.keys(byTarget).map(target => {
+ const body = byTarget[target].map(r => {
+ const sk = r.skill||0;
+ const cls = sk>0.05?'text-emerald-300':sk<-0.05?'text-rose-300':'text-slate-400';
+ const lead = r.horizon_s<3600 ? (r.horizon_s/60)+'m' : r.horizon_s<86400 ? (r.horizon_s/3600)+'h' : (r.horizon_s/86400)+'d';
+ const w = wmap[r.target+'@'+r.horizon_s];
+ const ws = w ? Math.round(w.persistence*100)+'/'+Math.round(w.climatology*100)+'/'+Math.round(w.learned*100) : '-';
+ return '
'+
+ '| '+lead+' | '+
+ ''+fmt(r.mae,2)+' | '+
+ ''+fmt(r.mae_persistence,2)+' | '+
+ ''+Math.round(sk*100)+'% | '+
+ ''+Math.round((r.coverage||0)*100)+'% | '+
+ ''+ws+' |
';
+ }).join('');
+ return '
'+
+ '
'+target+'
'+
+ '
'+
+ '| lead | MAE | '+
+ 'pers | skill | cov | '+
+ 'p/c/l |
'+body+'
';
}).join('');
el('m-storage').innerHTML = (sg.tiers||[]).map(t=>
@@ -837,6 +863,20 @@ el('m-calrst').addEventListener('click', async () => {
body: JSON.stringify({reset:true})}).then(r=>r.json());
el('m-calstat').innerHTML = 'Reset to prior k =
'+r.k+'';
});
+el('m-hcalgo').addEventListener('click', async () => {
+ const v = parseFloat(el('m-hcalin').value);
+ if (Number.isNaN(v) || v < 0 || v > 100) { el('m-hcalstat').innerText = 'Enter a relative humidity between 0 and 100%.'; return; }
+ const r = await fetch('/api/calibrate/humidity', {method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({reference_pct:v})}).then(r=>r.json());
+ el('m-hcalstat').innerHTML = r.offset!==undefined
+ ? 'offset is now
'+r.offset.toFixed(2)+'%, residual '+r.residual.toFixed(2)+'%'
+ : 'Rejected: no live reading yet.';
+});
+el('m-hcalrst').addEventListener('click', async () => {
+ const r = await fetch('/api/calibrate/humidity', {method:'POST', headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({reset:true})}).then(r=>r.json());
+ el('m-hcalstat').innerHTML = 'Reset to prior offset =
'+r.offset+'%';
+});
/* ---------------- METHODS ---------------- */
let methodsDoc = null, methodSel = 'acquire';
@@ -917,10 +957,11 @@ loaders.methods = loadMethods;
/* ---------------- BOOT ---------------- */
connectStream();
loadForecast();
+loadOutlook();
fetch('/api/status').then(r=>r.json()).then(s => { el('hd-days').innerText = fmt(s.history_days,2); });
-setInterval(() => { if (activeTab==='live') loadForecast(); }, 60000);
+setInterval(() => { if (activeTab==='live') { loadForecast(); loadOutlook(); } }, 60000);
setInterval(() => { if (activeTab==='models') loadModels(); }, 60000);
-setInterval(() => { if (activeTab==='history') { loadHistory(); loadOutlook(); } }, 300000);
+setInterval(() => { if (activeTab==='history') loadHistory(); }, 300000);