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:
2026-07-17 21:42:34 +07:00
co-authored by Claude Opus 4.8
parent 74121a2ef2
commit 26a5071e86
+344 -84
View File
@@ -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&nbsp;Predictor</h1>
<p class="lede">Reading the sky by pressure alone, the <b>Zambretti</b> way station at
<span id="coords">54.99&deg;N, 82.87&deg;E</span>.</p>
</header>
<div class="card">
<!-- 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&nbsp;·&nbsp;sea level</span>
<span class="chip" id="chip" style="color:var(--muted)">&mdash;</span>
</div>
<p class="verdict" id="verdict">Reading the sky&hellip;</p>
<p class="verdict-sub" id="verdict-sub">The forecaster needs about three hours of pressure history before its first call.</p>
</section>
<!-- 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">&mdash;</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">&mdash;</div><div class="s" id="deltas">&mdash;</div></div>
</section>
<!-- Barograph -->
<section class="panel panel-pad">
<div class="head">
<h2>Barograph</h2>
<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>
<span><i style="background:var(--change)"></i>Pressure</span>
<span><i style="background:var(--fair)"></i>Temperature</span>
</div>
<svg id="chart" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
<div class="muted" id="hint"></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&hellip;</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>
<!-- 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 &middot; metres</label><input id="s-alt" type="number" step="1"></div>
<div><label>Time zone &middot; 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 &middot; 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);
}
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';
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"}));
}
// 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)+"&deg;N, "+Number(s.lon).toFixed(2)+"&deg;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>