feat: self-contained web UI with live view, SVG chart, settings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+127
-1
@@ -1,3 +1,129 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
static const char INDEX_HTML[] PROGMEM = "<!doctype html><meta charset=utf-8><title>Weather Predictor</title><h1>Weather Predictor</h1><p>See <a href=/api/current>/api/current</a></p>";
|
||||
|
||||
static const char INDEX_HTML[] PROGMEM = R"HTML(
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Weather Predictor</h1>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<svg id="chart" viewBox="0 0 320 180" preserveAspectRatio="none"></svg>
|
||||
<div class="muted" id="hint"></div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
|
||||
<script>
|
||||
function arrow(t){ return t>0 ? "↑" : (t<0 ? "↓" : "→"); }
|
||||
|
||||
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;
|
||||
}catch(e){}
|
||||
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(' ');
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
const r = await (await fetch('/api/settings',{method:'POST',body:JSON.stringify(body)})).json();
|
||||
document.getElementById('saved').textContent = r.ok ? 'Saved.' : 'Error saving.';
|
||||
});
|
||||
|
||||
loadSettings();
|
||||
refresh();
|
||||
setInterval(refresh, 15000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)HTML";
|
||||
|
||||
Reference in New Issue
Block a user