feat(web): redesign UI as a barometer instrument
Dark dusk-indigo panel with brass/enamel accents, serif almanac headings + monospace instrument readouts. Signature element: an antique barometer dial with a live needle and a ghost 'set hand' at the pressure 3h ago -- the trend made physical. Barograph, stat tiles and settings kept quiet. Fully self-contained (no external fonts/CDNs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+347
-87
@@ -9,120 +9,380 @@ static const char INDEX_HTML[] PROGMEM = R"HTML(
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Weather Predictor</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; max-width: 720px; margin-inline: auto; }
|
||||
h1 { font-size: 1.3rem; }
|
||||
.card { border: 1px solid #8884; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
|
||||
.big { font-size: 2rem; font-weight: 700; }
|
||||
.row { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
|
||||
.muted { opacity: .7; font-size: .9rem; }
|
||||
label { display: block; margin: 8px 0 2px; font-size: .9rem; }
|
||||
input { width: 100%; padding: 8px; box-sizing: border-box; border-radius: 8px; border: 1px solid #8886; }
|
||||
button { margin-top: 12px; padding: 10px 16px; border-radius: 8px; border: 0; background: #2b7; color: #fff; font-size: 1rem; }
|
||||
svg { width: 100%; height: 180px; }
|
||||
.axis { stroke: #8886; stroke-width: 1; }
|
||||
.p-line { fill: none; stroke: #2b7; stroke-width: 2; }
|
||||
.t-line { fill: none; stroke: #e83; stroke-width: 2; }
|
||||
.legend span { display: inline-block; margin-right: 16px; font-size: .85rem; }
|
||||
.sw { display: inline-block; width: 12px; height: 12px; border-radius: 3px; vertical-align: middle; margin-right: 4px; }
|
||||
:root{
|
||||
--ink:#0F1A2E; --ink2:#0B1322; --surface:#182642; --line:#2A3A5A;
|
||||
--enamel:#F1ECE0; --brass:#C6A15B; --brass-dim:#8C7538;
|
||||
--text:#EAF0F8; --muted:#93A3BE;
|
||||
--storm:#E4573B; --rain:#5C86D6; --change:#79C2D0; --fair:#E4B658; --fine:#8FBE8A;
|
||||
--serif:"Palatino Linotype",Palatino,"Book Antiqua",Georgia,"Times New Roman",serif;
|
||||
--sans:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
|
||||
--mono:ui-monospace,"Cascadia Mono","Segoe UI Mono",Consolas,Menlo,monospace;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{
|
||||
margin:0; background:
|
||||
radial-gradient(120% 80% at 50% -10%, #1B2B49 0%, var(--ink) 55%, var(--ink2) 100%) fixed;
|
||||
color:var(--text); font-family:var(--sans); line-height:1.5;
|
||||
-webkit-font-smoothing:antialiased;
|
||||
}
|
||||
.wrap{max-width:760px; margin:0 auto; padding:28px 20px 48px}
|
||||
|
||||
/* Masthead */
|
||||
.eyebrow{
|
||||
font-size:.72rem; letter-spacing:.28em; text-transform:uppercase;
|
||||
color:var(--brass); display:flex; align-items:center; gap:10px; margin:0 0 10px;
|
||||
}
|
||||
.eyebrow::before{content:""; width:26px; height:1px; background:var(--brass-dim)}
|
||||
h1{
|
||||
font-family:var(--serif); font-weight:600; font-size:clamp(2.1rem,7vw,3.1rem);
|
||||
letter-spacing:.01em; margin:0; line-height:1.02;
|
||||
}
|
||||
.lede{color:var(--muted); font-size:.92rem; margin:8px 0 0}
|
||||
.lede b{color:var(--text); font-weight:600}
|
||||
|
||||
.panel{
|
||||
background:linear-gradient(180deg, var(--surface), #14203a);
|
||||
border:1px solid var(--line); border-radius:16px; margin-top:22px;
|
||||
}
|
||||
.panel-pad{padding:22px}
|
||||
.rule{height:1px; background:var(--line); border:0; margin:0}
|
||||
|
||||
/* Dial hero */
|
||||
.dial{padding:20px 16px 6px; text-align:center}
|
||||
svg{display:block; width:100%; height:auto}
|
||||
#gauge{max-width:420px; margin:0 auto}
|
||||
.needle{transition:transform .9s cubic-bezier(.2,.8,.2,1)}
|
||||
.sethand{transition:transform .9s cubic-bezier(.2,.8,.2,1)}
|
||||
|
||||
.readout{
|
||||
display:flex; align-items:baseline; justify-content:center; gap:12px;
|
||||
margin-top:2px; flex-wrap:wrap;
|
||||
}
|
||||
.pressure{
|
||||
font-family:var(--mono); font-variant-numeric:tabular-nums;
|
||||
font-size:clamp(2.6rem,11vw,3.7rem); font-weight:600; letter-spacing:-.02em; color:var(--enamel);
|
||||
}
|
||||
.unit{font-family:var(--sans); font-size:.9rem; letter-spacing:.22em; text-transform:uppercase; color:var(--muted)}
|
||||
.chip{
|
||||
font-size:.74rem; letter-spacing:.14em; text-transform:uppercase; font-weight:600;
|
||||
padding:5px 11px; border-radius:999px; border:1px solid currentColor; white-space:nowrap;
|
||||
}
|
||||
.verdict{
|
||||
font-family:var(--serif); font-size:clamp(1.35rem,4.6vw,1.7rem); font-weight:600;
|
||||
margin:16px 0 4px; padding:0 12px; color:var(--enamel);
|
||||
}
|
||||
.verdict-sub{color:var(--muted); font-size:.86rem; margin:0 0 20px}
|
||||
|
||||
/* Stats */
|
||||
.stats{display:grid; grid-template-columns:repeat(3,1fr)}
|
||||
.stat{padding:18px; text-align:left; border-left:1px solid var(--line)}
|
||||
.stat:first-child{border-left:0}
|
||||
.stat .k{font-size:.68rem; letter-spacing:.2em; text-transform:uppercase; color:var(--muted)}
|
||||
.stat .v{font-family:var(--mono); font-variant-numeric:tabular-nums; font-size:1.7rem; font-weight:600; color:var(--enamel); margin-top:6px; line-height:1}
|
||||
.stat .s{font-size:.78rem; color:var(--muted); margin-top:5px}
|
||||
@media(max-width:520px){
|
||||
.stats{grid-template-columns:1fr 1fr}
|
||||
.stat:nth-child(3){grid-column:1 / -1; border-left:0; border-top:1px solid var(--line)}
|
||||
}
|
||||
|
||||
/* Barograph */
|
||||
.head{display:flex; align-items:baseline; justify-content:space-between; gap:12px; margin-bottom:14px}
|
||||
.head h2{font-family:var(--serif); font-weight:600; font-size:1.15rem; margin:0}
|
||||
.legend{display:flex; gap:16px; font-size:.76rem; color:var(--muted)}
|
||||
.legend i{display:inline-block; width:20px; height:2px; vertical-align:middle; margin-right:6px}
|
||||
#chart{width:100%; height:200px}
|
||||
.hint{color:var(--muted); font-size:.78rem; margin:10px 0 0}
|
||||
|
||||
/* Settings */
|
||||
details.set summary{
|
||||
cursor:pointer; list-style:none; padding:18px 22px; display:flex; align-items:center;
|
||||
justify-content:space-between; font-family:var(--serif); font-size:1.1rem; font-weight:600;
|
||||
}
|
||||
details.set summary::-webkit-details-marker{display:none}
|
||||
details.set summary .caret{color:var(--brass); font-size:.8rem; letter-spacing:.15em; text-transform:uppercase}
|
||||
.form{padding:4px 22px 22px}
|
||||
.grid2{display:grid; grid-template-columns:1fr 1fr; gap:14px}
|
||||
@media(max-width:520px){.grid2{grid-template-columns:1fr}}
|
||||
label{display:block; font-size:.72rem; letter-spacing:.14em; text-transform:uppercase; color:var(--muted); margin:0 0 6px}
|
||||
input{
|
||||
width:100%; padding:11px 12px; background:var(--ink2); color:var(--text);
|
||||
border:1px solid var(--line); border-radius:10px; font-family:var(--mono); font-size:1rem;
|
||||
}
|
||||
input:focus{outline:none; border-color:var(--brass); box-shadow:0 0 0 3px rgba(198,161,91,.18)}
|
||||
.actions{display:flex; align-items:center; gap:16px; margin-top:18px}
|
||||
button{
|
||||
padding:11px 22px; border:0; border-radius:10px; cursor:pointer;
|
||||
background:var(--brass); color:#221a06; font-weight:700; font-size:.95rem; letter-spacing:.02em;
|
||||
font-family:var(--sans);
|
||||
}
|
||||
button:hover{filter:brightness(1.06)}
|
||||
button:focus-visible{outline:2px solid var(--enamel); outline-offset:2px}
|
||||
.saved{font-size:.82rem; color:var(--fine)}
|
||||
|
||||
footer{color:var(--muted); font-size:.74rem; text-align:center; margin-top:26px; letter-spacing:.04em}
|
||||
|
||||
@media(prefers-reduced-motion:reduce){.needle,.sethand{transition:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Weather Predictor</h1>
|
||||
<div class="wrap">
|
||||
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div><div class="muted">Time</div><div class="big" id="time">--:--</div></div>
|
||||
<div><div class="muted">Temperature</div><div class="big" id="temp">-- C</div></div>
|
||||
</div>
|
||||
<div class="row" style="margin-top:12px">
|
||||
<div><div class="muted">Pressure (sea level)</div><div class="big"><span id="msl">----</span> <span id="trend"></span></div></div>
|
||||
</div>
|
||||
<div style="margin-top:12px"><div class="muted">Forecast</div><div id="forecast" style="font-size:1.2rem">...</div></div>
|
||||
<div class="muted" id="abs" style="margin-top:8px"></div>
|
||||
</div>
|
||||
<header>
|
||||
<p class="eyebrow">Barometric Forecaster</p>
|
||||
<h1>Weather Predictor</h1>
|
||||
<p class="lede">Reading the sky by pressure alone, the <b>Zambretti</b> way — station at
|
||||
<span id="coords">54.99°N, 82.87°E</span>.</p>
|
||||
</header>
|
||||
|
||||
<div class="card">
|
||||
<div class="legend">
|
||||
<span><span class="sw" style="background:#2b7"></span>Pressure (hPa)</span>
|
||||
<span><span class="sw" style="background:#e83"></span>Temperature (C)</span>
|
||||
<!-- Barometer dial -->
|
||||
<section class="panel dial">
|
||||
<svg id="gauge" viewBox="0 0 400 232" role="img" aria-label="Barometer dial"></svg>
|
||||
<div class="readout">
|
||||
<span class="pressure"><span id="msl">----</span></span>
|
||||
<span class="unit">hPa · sea level</span>
|
||||
<span class="chip" id="chip" style="color:var(--muted)">—</span>
|
||||
</div>
|
||||
<svg id="chart" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
|
||||
<div class="muted" id="hint"></div>
|
||||
</div>
|
||||
<p class="verdict" id="verdict">Reading the sky…</p>
|
||||
<p class="verdict-sub" id="verdict-sub">The forecaster needs about three hours of pressure history before its first call.</p>
|
||||
</section>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="font-size:1.05rem;margin-top:0">Settings</h2>
|
||||
<label>Altitude (m)</label><input id="s-alt" type="number" step="1">
|
||||
<label>Timezone offset (minutes from UTC)</label><input id="s-tz" type="number" step="15">
|
||||
<label>Latitude</label><input id="s-lat" type="number" step="0.0001">
|
||||
<label>Longitude</label><input id="s-lon" type="number" step="0.0001">
|
||||
<button id="save">Save</button>
|
||||
<div class="muted" id="saved"></div>
|
||||
</div>
|
||||
<!-- Stats -->
|
||||
<section class="panel stats">
|
||||
<div class="stat"><div class="k">Local time</div><div class="v" id="time">--:--</div><div class="s" id="date">—</div></div>
|
||||
<div class="stat"><div class="k">Temperature</div><div class="v" id="temp">--</div><div class="s">degrees celsius</div></div>
|
||||
<div class="stat"><div class="k">3-hour move</div><div class="v" id="delta">—</div><div class="s" id="deltas">—</div></div>
|
||||
</section>
|
||||
|
||||
<!-- Barograph -->
|
||||
<section class="panel panel-pad">
|
||||
<div class="head">
|
||||
<h2>Barograph</h2>
|
||||
<div class="legend">
|
||||
<span><i style="background:var(--change)"></i>Pressure</span>
|
||||
<span><i style="background:var(--fair)"></i>Temperature</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg id="chart" viewBox="0 0 640 200" preserveAspectRatio="none" aria-label="Pressure and temperature history"></svg>
|
||||
<p class="hint" id="hint">Collecting data…</p>
|
||||
</section>
|
||||
|
||||
<!-- Settings -->
|
||||
<details class="panel set">
|
||||
<summary>Station settings <span class="caret">Adjust</span></summary>
|
||||
<hr class="rule">
|
||||
<div class="form">
|
||||
<div class="grid2" style="margin-top:18px">
|
||||
<div><label>Altitude · metres</label><input id="s-alt" type="number" step="1"></div>
|
||||
<div><label>Time zone · minutes from UTC</label><input id="s-tz" type="number" step="15"></div>
|
||||
<div><label>Latitude</label><input id="s-lat" type="number" step="0.0001"></div>
|
||||
<div><label>Longitude</label><input id="s-lon" type="number" step="0.0001"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button id="save">Save settings</button>
|
||||
<span class="saved" id="saved"></span>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<footer>Served from the station · works without internet</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function arrow(t){ return t>0 ? "↑" : (t<0 ? "↓" : "→"); }
|
||||
"use strict";
|
||||
var CX=200, CY=200, R=150, PMIN=960, PMAX=1060;
|
||||
var ZONES=[
|
||||
{from:960, to:985, label:"STORMY", color:"#E4573B"},
|
||||
{from:985, to:1000, label:"RAIN", color:"#5C86D6"},
|
||||
{from:1000,to:1015, label:"CHANGE", color:"#79C2D0"},
|
||||
{from:1015,to:1035, label:"FAIR", color:"#E4B658"},
|
||||
{from:1035,to:1060, label:"VERY DRY", color:"#8FBE8A"}
|
||||
];
|
||||
var CATCOLOR={Fine:"#8FBE8A",Fair:"#E4B658",Changeable:"#79C2D0",Rain:"#5C86D6",Storm:"#E4573B"};
|
||||
|
||||
function clamp(v,a,b){return v<a?a:(v>b?b:v);}
|
||||
// pressure -> screen angle (deg): 960=180 (left), 1060=0 (right)
|
||||
function angleOf(p){return 180 - (clamp(p,PMIN,PMAX)-PMIN)/(PMAX-PMIN)*180;}
|
||||
function pol(r,deg){var a=deg*Math.PI/180; return [CX+r*Math.cos(a), CY-r*Math.sin(a)];}
|
||||
function arcPts(r,pFrom,pTo){
|
||||
var a0=angleOf(pFrom), a1=angleOf(pTo), d=(a1<a0?-1:1), s="";
|
||||
for(var a=a0; (d<0? a>=a1 : a<=a1); a+=d*2){ var q=pol(r,a); s+=(s?"L":"M")+q[0].toFixed(1)+" "+q[1].toFixed(1); }
|
||||
var e=pol(r,a1); s+="L"+e[0].toFixed(1)+" "+e[1].toFixed(1);
|
||||
return s;
|
||||
}
|
||||
function el(tag,attrs,txt){
|
||||
var n=document.createElementNS("http://www.w3.org/2000/svg",tag);
|
||||
for(var k in attrs) n.setAttribute(k,attrs[k]);
|
||||
if(txt!=null) n.textContent=txt;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Build the static dial once.
|
||||
function buildGauge(){
|
||||
var g=document.getElementById("gauge");
|
||||
// track
|
||||
g.appendChild(el("path",{d:arcPts(R,PMIN,PMAX),fill:"none",stroke:"#2A3A5A","stroke-width":14,"stroke-linecap":"round"}));
|
||||
// zone arcs (dim) + boundary ticks + labels
|
||||
ZONES.forEach(function(z){
|
||||
g.appendChild(el("path",{id:"zone-"+z.label,d:arcPts(R,z.from,z.to),fill:"none",stroke:z.color,"stroke-width":14,opacity:.28}));
|
||||
var mid=(z.from+z.to)/2, lp=pol(R+21,angleOf(mid));
|
||||
var t=el("text",{id:"lab-"+z.label,x:lp[0].toFixed(1),y:lp[1].toFixed(1),fill:"#93A3BE","font-size":11,"font-family":"var(--sans)","letter-spacing":"1.5","text-anchor":"middle","dominant-baseline":"middle"},z.label);
|
||||
g.appendChild(t);
|
||||
});
|
||||
// numeric ticks
|
||||
[960,980,1000,1020,1040,1060].forEach(function(p){
|
||||
var o=pol(R-8,angleOf(p)), i=pol(R-18,angleOf(p));
|
||||
g.appendChild(el("line",{x1:o[0].toFixed(1),y1:o[1].toFixed(1),x2:i[0].toFixed(1),y2:i[1].toFixed(1),stroke:"#4A5B7C","stroke-width":1.5}));
|
||||
var np=pol(R-30,angleOf(p));
|
||||
g.appendChild(el("text",{x:np[0].toFixed(1),y:np[1].toFixed(1),fill:"#6B7C9C","font-size":9,"font-family":"var(--mono)","text-anchor":"middle","dominant-baseline":"middle"},p));
|
||||
});
|
||||
// set-hand (ghost, hidden until data) — points up at rot 0
|
||||
var sh=el("g",{id:"sethand",class:"sethand",transform:"rotate(0 "+CX+" "+CY+")",opacity:0});
|
||||
sh.appendChild(el("line",{x1:CX,y1:CY,x2:CX,y2:CY-(R-22),stroke:"#93A3BE","stroke-width":2,"stroke-dasharray":"3 4"}));
|
||||
sh.appendChild(el("circle",{cx:CX,cy:CY-(R-22),r:3.5,fill:"none",stroke:"#93A3BE","stroke-width":1.5}));
|
||||
g.appendChild(sh);
|
||||
// live needle — points up at rot 0
|
||||
var nd=el("g",{id:"needle",class:"needle",transform:"rotate(0 "+CX+" "+CY+")"});
|
||||
nd.appendChild(el("line",{x1:CX,y1:CY+14,x2:CX,y2:CY-(R-12),stroke:"#F1ECE0","stroke-width":3,"stroke-linecap":"round"}));
|
||||
nd.appendChild(el("polygon",{points:CX+","+(CY-(R-2))+" "+(CX-5)+","+(CY-(R-16))+" "+(CX+5)+","+(CY-(R-16)),fill:"#F1ECE0"}));
|
||||
g.appendChild(nd);
|
||||
// hub
|
||||
g.appendChild(el("circle",{cx:CX,cy:CY,r:10,fill:"#182642",stroke:"#C6A15B","stroke-width":2}));
|
||||
g.appendChild(el("circle",{cx:CX,cy:CY,r:3.5,fill:"#C6A15B"}));
|
||||
}
|
||||
|
||||
function zoneOf(p){for(var i=0;i<ZONES.length;i++){if(p>=ZONES[i].from&&p<ZONES[i].to)return ZONES[i];}return ZONES[p>=PMAX?ZONES.length-1:0];}
|
||||
function setNeedle(id,p){ // rotate so up(rot0)=1010
|
||||
var rot=90-angleOf(p);
|
||||
document.getElementById(id).setAttribute("transform","rotate("+rot.toFixed(1)+" "+CX+" "+CY+")");
|
||||
}
|
||||
function highlightZone(p){
|
||||
ZONES.forEach(function(z){
|
||||
var on=(p>=z.from&&p<z.to);
|
||||
document.getElementById("zone-"+z.label).setAttribute("opacity",on?1:.28);
|
||||
var lab=document.getElementById("lab-"+z.label);
|
||||
lab.setAttribute("fill",on?"#F1ECE0":"#93A3BE");
|
||||
lab.setAttribute("font-weight",on?"700":"400");
|
||||
});
|
||||
}
|
||||
|
||||
function fmtSigned(x,digits){return (x>=0?"+":"")+x.toFixed(digits==null?1:digits);}
|
||||
|
||||
var lastHistory=[];
|
||||
|
||||
async function refresh(){
|
||||
try{
|
||||
const c = await (await fetch('/api/current')).json();
|
||||
document.getElementById('time').textContent = c.time.split(' ')[1].slice(0,5);
|
||||
document.getElementById('temp').textContent = c.temp.toFixed(1)+' C';
|
||||
document.getElementById('msl').textContent = c.msl.toFixed(0);
|
||||
document.getElementById('trend').textContent = arrow(c.trend);
|
||||
document.getElementById('forecast').textContent = c.forecast;
|
||||
document.getElementById('abs').textContent = 'Absolute: '+c.abs.toFixed(1)+' hPa | '+c.time;
|
||||
var c=await (await fetch("/api/current")).json();
|
||||
// pressure + needle + zone
|
||||
var msl=c.msl;
|
||||
document.getElementById("msl").textContent=msl.toFixed(0);
|
||||
setNeedle("needle",msl);
|
||||
highlightZone(msl);
|
||||
// time
|
||||
var parts=(c.time||"").split(" ");
|
||||
document.getElementById("date").textContent=parts[0]||"";
|
||||
document.getElementById("time").textContent=(parts[1]||"").slice(0,5);
|
||||
// temp
|
||||
document.getElementById("temp").textContent=c.temp.toFixed(1);
|
||||
// verdict
|
||||
var v=document.getElementById("verdict"), vs=document.getElementById("verdict-sub"), chip=document.getElementById("chip");
|
||||
if(c.haveTrend){
|
||||
v.textContent=c.forecast;
|
||||
var col=CATCOLOR[c.category]||"#93A3BE";
|
||||
chip.textContent=c.category; chip.style.color=col;
|
||||
var word=c.trend>0?"pressure rising":(c.trend<0?"pressure falling":"pressure steady");
|
||||
vs.textContent="Zambretti reading — "+word+" over the last three hours.";
|
||||
}else{
|
||||
v.textContent="Reading the sky…";
|
||||
chip.textContent="warming up"; chip.style.color="#93A3BE";
|
||||
vs.textContent="The forecaster needs about three hours of pressure history before its first call.";
|
||||
}
|
||||
document.getElementById("chip").setAttribute("aria-label",c.category||"");
|
||||
// absolute already implied; keep footer clean
|
||||
}catch(e){}
|
||||
drawChart();
|
||||
await drawChart();
|
||||
}
|
||||
|
||||
async function drawChart(){
|
||||
const svg = document.getElementById('chart');
|
||||
let data;
|
||||
try{ data = await (await fetch('/api/history')).json(); }catch(e){ return; }
|
||||
const W=320,H=180,pad=4;
|
||||
if(!data.length){ document.getElementById('hint').textContent='Collecting data...'; svg.innerHTML=''; return; }
|
||||
const ps=data.map(d=>d.msl), ts=data.map(d=>d.temp);
|
||||
const n=data.length;
|
||||
function path(vals){
|
||||
const mn=Math.min(...vals), mx=Math.max(...vals), rng=(mx-mn)||1;
|
||||
return vals.map((v,i)=>{
|
||||
const x=pad+(W-2*pad)*(n>1?i/(n-1):0);
|
||||
const y=H-pad-(H-2*pad)*((v-mn)/rng);
|
||||
return (i?'L':'M')+x.toFixed(1)+' '+y.toFixed(1);
|
||||
}).join(' ');
|
||||
var data;
|
||||
try{ data=await (await fetch("/api/history")).json(); }catch(e){ return; }
|
||||
lastHistory=data||[];
|
||||
var svg=document.getElementById("chart"), W=640,H=200,pad=6;
|
||||
while(svg.firstChild) svg.removeChild(svg.firstChild);
|
||||
if(!data.length){ document.getElementById("hint").textContent="Collecting data…"; updateSetHand(); return; }
|
||||
var ps=data.map(function(d){return d.msl;}), ts=data.map(function(d){return d.temp;}), n=data.length;
|
||||
function line(vals,color,fillId){
|
||||
var mn=Math.min.apply(null,vals), mx=Math.max.apply(null,vals), rng=(mx-mn)||1, d="";
|
||||
for(var i=0;i<n;i++){
|
||||
var x=pad+(W-2*pad)*(n>1?i/(n-1):0);
|
||||
var y=H-pad-(H-2*pad)*((vals[i]-mn)/rng);
|
||||
d+=(i?"L":"M")+x.toFixed(1)+" "+y.toFixed(1);
|
||||
}
|
||||
if(fillId){
|
||||
var area=d+"L"+(W-pad)+" "+(H-pad)+"L"+pad+" "+(H-pad)+"Z";
|
||||
svg.appendChild(el("path",{d:area,fill:color,opacity:.10}));
|
||||
}
|
||||
svg.appendChild(el("path",{d:d,fill:"none",stroke:color,"stroke-width":2,"stroke-linejoin":"round"}));
|
||||
}
|
||||
svg.innerHTML =
|
||||
'<line class="axis" x1="0" y1="'+(H-1)+'" x2="'+W+'" y2="'+(H-1)+'"/>'+
|
||||
'<path class="p-line" d="'+path(ps)+'"/>'+
|
||||
'<path class="t-line" d="'+path(ts)+'"/>';
|
||||
const hrs=((data[n-1].t-data[0].t)/3600).toFixed(1);
|
||||
document.getElementById('hint').textContent=n+' samples over '+hrs+' h';
|
||||
// baseline
|
||||
svg.appendChild(el("line",{x1:0,y1:H-1,x2:W,y2:H-1,stroke:"#2A3A5A","stroke-width":1}));
|
||||
line(ps,"#79C2D0",true);
|
||||
line(ts,"#E4B658",false);
|
||||
var hrs=((data[n-1].t-data[0].t)/3600);
|
||||
document.getElementById("hint").textContent=n+" readings over "+hrs.toFixed(1)+" h · "+
|
||||
Math.min.apply(null,ps).toFixed(0)+"–"+Math.max.apply(null,ps).toFixed(0)+" hPa";
|
||||
updateSetHand();
|
||||
}
|
||||
|
||||
// Ghost "set hand": pressure ~3 h ago, and the 3-hour move readout.
|
||||
function updateSetHand(){
|
||||
var sh=document.getElementById("sethand");
|
||||
var d=lastHistory;
|
||||
if(d.length<2){ sh.setAttribute("opacity",0); return; }
|
||||
var latest=d[d.length-1], target=latest.t-10800, past=null;
|
||||
for(var i=0;i<d.length;i++){ if(d[i].t<=target) past=d[i]; }
|
||||
if(!past || (latest.t-past.t)<9000){ sh.setAttribute("opacity",0);
|
||||
document.getElementById("delta").textContent="—";
|
||||
document.getElementById("deltas").textContent="need 3 h of data"; return; }
|
||||
setNeedle("sethand",past.msl);
|
||||
sh.setAttribute("opacity",.7);
|
||||
var diff=latest.msl-past.msl;
|
||||
document.getElementById("delta").textContent=fmtSigned(diff,1);
|
||||
var word=diff>1.6?"rising":(diff<-1.6?"falling":"steady");
|
||||
document.getElementById("deltas").textContent="hPa · "+word;
|
||||
}
|
||||
|
||||
async function loadSettings(){
|
||||
const s = await (await fetch('/api/settings')).json();
|
||||
document.getElementById('s-alt').value = s.altitude;
|
||||
document.getElementById('s-tz').value = s.tz;
|
||||
document.getElementById('s-lat').value = s.lat;
|
||||
document.getElementById('s-lon').value = s.lon;
|
||||
try{
|
||||
var s=await (await fetch("/api/settings")).json();
|
||||
document.getElementById("s-alt").value=s.altitude;
|
||||
document.getElementById("s-tz").value=s.tz;
|
||||
document.getElementById("s-lat").value=s.lat;
|
||||
document.getElementById("s-lon").value=s.lon;
|
||||
document.getElementById("coords").innerHTML=Number(s.lat).toFixed(2)+"°N, "+Number(s.lon).toFixed(2)+"°E";
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
document.getElementById('save').addEventListener('click', async ()=>{
|
||||
const body = {
|
||||
altitude: parseFloat(document.getElementById('s-alt').value),
|
||||
tz: parseInt(document.getElementById('s-tz').value),
|
||||
lat: parseFloat(document.getElementById('s-lat').value),
|
||||
lon: parseFloat(document.getElementById('s-lon').value)
|
||||
document.getElementById("save").addEventListener("click", async function(){
|
||||
var body={
|
||||
altitude:parseFloat(document.getElementById("s-alt").value),
|
||||
tz:parseInt(document.getElementById("s-tz").value),
|
||||
lat:parseFloat(document.getElementById("s-lat").value),
|
||||
lon:parseFloat(document.getElementById("s-lon").value)
|
||||
};
|
||||
const r = await (await fetch('/api/settings',{method:'POST',body:JSON.stringify(body)})).json();
|
||||
document.getElementById('saved').textContent = r.ok ? 'Saved.' : 'Error saving.';
|
||||
var msg=document.getElementById("saved");
|
||||
try{
|
||||
var r=await (await fetch("/api/settings",{method:"POST",body:JSON.stringify(body)})).json();
|
||||
msg.textContent=r.ok?"Saved.":"Could not save.";
|
||||
msg.style.color=r.ok?"var(--fine)":"var(--storm)";
|
||||
if(r.ok){ loadSettings(); refresh(); }
|
||||
}catch(e){ msg.textContent="Could not reach the station."; msg.style.color="var(--storm)"; }
|
||||
setTimeout(function(){msg.textContent="";},4000);
|
||||
});
|
||||
|
||||
buildGauge();
|
||||
loadSettings();
|
||||
refresh();
|
||||
setInterval(refresh, 15000);
|
||||
setInterval(refresh,15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user