Simplify status interface
Deploy Solar Relay / deploy (push) Successful in 1m2s

This commit is contained in:
franzz committed 2026-11-02 23:14:13 +01:00
1 parent cd7ddd3cf6
commit 89621bc7c3
15 files changed
+289 -74

No files matched your search

+11 -12
View File
@@ -24,19 +24,9 @@
<p id="notice" class="notice" role="alert" hidden></p>
<section class="metrics" aria-label="Current collection status">
<article class="metric"><p class="metric-label">Last extraction</p><strong id="last-extraction">—</strong><p id="last-extraction-date">No observation received yet</p><small id="last-success">Last successful extraction: —</small></article>
<article class="metric"><p class="metric-label">Yesterday’s coverage</p><strong id="yesterday">— <span>/ —</span></strong><p id="yesterday-detail">Five-minute intervals</p><small>Energy, meter and boiler status</small></article>
<article class="metric"><p class="metric-label">Yesterday’s coverage</p><strong id="yesterday">— <span>/ —</span></strong><p id="yesterday-detail">Five-minute intervals</p><small>Energy and boiler status</small></article>
<article class="metric"><p class="metric-label">Waiting to upload</p><strong id="pending">—</strong><p id="upload-detail">Checking delivery</p><small id="last-upload">Last acknowledgement: —</small></article>
<article class="metric"><p class="metric-label">Local archive</p><strong id="samples">—</strong><p>Observations retained in Parquet</p><small id="retention">Acknowledged readings kept for 30 days</small></article>
</section>
<section class="panel">
<div class="panel-heading"><div><p class="eyebrow">DAILY COVERAGE</p><h2>Five-minute completeness</h2></div><label class="range">History <select id="history"><option value="7">7 days</option><option value="14" selected>14 days</option><option value="30">30 days</option><option value="90">90 days</option></select></label></div>
<p class="panel-note">Each count shows five-minute intervals with valid data out of the elapsed intervals for that day. Complete intervals have all three sources. Polling does not guarantee continuous observation. Solar.web recovery fills Energy only.</p>
<dl class="coverage-definitions">
<div><dt>Energy</dt><dd id="energy-description">Solar production, household consumption and electricity imported from or exported to the grid. Available from live readings or recovered Solar.web history.</dd></div>
<div><dt>Meter</dt><dd id="meter-description">Running totals of electricity bought from and sold to the grid. Requires live meter readings; Solar.web history cannot fill these counters.</dd></div>
<div><dt>Boiler</dt><dd id="boiler-description">Boiler control output: enabled or disabled. Both states count as valid data. Heating energy is not measured, and Solar.web cannot recover past switch states.</dd></div>
</dl>
<div class="table-scroll"><table><thead><tr><th scope="col">Day</th><th scope="col">Complete intervals</th><th scope="col" class="source-column" aria-describedby="energy-description">Energy</th><th scope="col" class="source-column" aria-describedby="meter-description">Meter</th><th scope="col" class="source-column" aria-describedby="boiler-description">Boiler</th><th scope="col">Status</th></tr></thead><tbody id="days"><tr><td colspan="6" class="empty">Waiting for the local archive…</td></tr></tbody></table></div>
<article class="metric"><p class="metric-label">Local archive</p><strong id="samples">—</strong><p>Observations retained in Parquet</p><small id="retention">Retention: 30 full calendar days</small></article>
</section>
<section class="panel detail" aria-label="Selected day interval details">
<div class="panel-heading"><div><p class="eyebrow">FIVE-MINUTE INTERVALS</p><h2 id="selected-date">Select a day</h2></div><div class="legend"><span><i class="complete"></i>Complete</span><span><i class="partial"></i>Partial</span><span><i class="missing"></i>Missing</span><span><i class="future"></i>Upcoming</span></div></div>
@@ -44,6 +34,15 @@
<div id="slots" class="slots" aria-label="Five-minute coverage"></div>
<p id="missing-ranges" class="gap-summary"></p>
</section>
<section class="panel">
<div class="panel-heading"><div><p class="eyebrow">DAILY COVERAGE</p><h2>Five-minute completeness</h2></div><label class="range">History <select id="history"><option value="7">7 days</option><option value="14" selected>14 days</option><option value="30">30 days</option><option value="90">90 days</option></select></label></div>
<p class="panel-note">Each count shows five-minute intervals with valid data out of the intervals assessed so far. Saved extractions appear immediately, including the current interval. Complete intervals have both Energy and Boiler data. Polling does not guarantee continuous observation. Solar.web recovery fills Energy only.</p>
<dl class="coverage-definitions">
<div><dt>Energy</dt><dd id="energy-description">Solar production, household consumption and electricity imported from or exported to the grid. Available from live readings or recovered Solar.web history.</dd></div>
<div><dt>Boiler</dt><dd id="boiler-description">Boiler control output: enabled or disabled. Both states count as valid data. Heating energy is not measured, and Solar.web cannot recover past switch states.</dd></div>
</dl>
<div class="table-scroll"><table><thead><tr><th scope="col">Day</th><th scope="col">Complete intervals</th><th scope="col" class="source-column" aria-describedby="energy-description">Energy</th><th scope="col" class="source-column" aria-describedby="boiler-description">Boiler</th><th scope="col">Status</th></tr></thead><tbody id="days"><tr><td colspan="5" class="empty">Waiting for the local archive…</td></tr></tbody></table></div>
</section>
<footer><span>Read-only status · refreshes every 15 seconds</span><span id="version">solar-relay</span></footer>
<noscript><p class="notice">Enable JavaScript to load extraction and coverage status.</p></noscript>
</main>
+1 -1
View File
@@ -1,6 +1,6 @@
:root{color-scheme:dark;--bg:#101518;--panel:#171e22;--line:#2b353a;--ink:#eef1ed;--muted:#9ba9ac;--amber:#e8ba70;--green:#7ad6ac;--red:#e68d86;--radius:12px;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;font-synthesis:none}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink)}button,select{font:inherit}button{cursor:pointer}button:focus-visible,select:focus-visible{outline:2px solid var(--amber);outline-offset:4px}main{max-width:1216px;margin:auto;padding:0 32px}.masthead{height:100px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line)}.brand{display:flex;align-items:center;gap:12px;font-size:13px;letter-spacing:.14em;font-weight:750}.brand small{display:block;color:var(--muted);font-size:9px;letter-spacing:.22em;margin-top:5px}.sun{font-size:36px;color:var(--amber);line-height:1}.header-actions{display:flex;align-items:center;gap:16px}.badge{display:inline-flex;align-items:center;gap:7px;white-space:nowrap;border-radius:20px;padding:6px 11px;font-size:11px;font-weight:650;line-height:1.3;background:#252e32;color:var(--muted)}.badge::before{content:"";width:5px;height:5px;border-radius:50%;background:currentColor}.badge.ok,.badge.complete{color:var(--green);background:#193a2e}.badge.partial,.badge.in_progress{color:var(--amber);background:#3a3121}.badge.stale{color:var(--red);background:#3d2829}.badge.missing,.badge.waiting,.badge.neutral{color:var(--muted);background:#252e32}.refresh{border:1px solid var(--line);background:transparent;color:var(--ink);border-radius:7px;padding:8px 12px;font-size:13px}.refresh:hover{border-color:var(--muted)}.refresh:disabled{opacity:.45;cursor:wait}.intro{padding:42px 0 28px}.eyebrow{color:var(--amber);font-size:10px;font-weight:700;letter-spacing:.17em;margin:0 0 13px}h1{font-size:clamp(27px,3.4vw,42px);letter-spacing:-.045em;font-weight:550;line-height:1.14;margin:0 0 13px}.description{color:var(--muted);font-size:14px;line-height:1.6;margin:0}.context{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--muted);margin-top:20px;flex-wrap:wrap}.separator{color:#48565c}.metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px;margin:0 0 28px}.metric{border:1px solid var(--line);border-radius:var(--radius);padding:23px 20px;background:var(--panel)}.metric-label{font-size:11px;font-weight:550;color:var(--muted);margin:0 0 18px}.metric strong{display:block;font-size:30px;font-weight:550;font-variant-numeric:tabular-nums;letter-spacing:-.04em;line-height:1.1;min-height:34px}.metric strong span{color:var(--muted);font-size:17px;font-weight:400;letter-spacing:normal}.metric p:not(.metric-label){font-size:11px;margin:12px 0 7px;color:#d1d9d7}.metric small{font-size:10px;color:var(--muted);line-height:1.6;display:block}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);margin-bottom:22px;overflow:hidden}.panel-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:24px 24px 0}.panel-heading .eyebrow{margin-bottom:7px}h2{font-size:19px;font-weight:550;letter-spacing:-.025em;margin:0}.panel-note{font-size:11px;line-height:1.7;color:var(--muted);padding:0 24px;margin:12px 0 22px}.range{display:flex;align-items:center;gap:9px;color:var(--muted);font-size:11px;white-space:nowrap}select{background:#20292e;border:1px solid #354149;border-radius:6px;color:var(--ink);padding:7px 27px 7px 9px;font-size:11px}.table-scroll{overflow:auto}table{width:100%;border-collapse:collapse;text-align:left;font-size:12px;font-variant-numeric:tabular-nums}th{background:#1c252a;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:11px 18px;font-size:9px;font-weight:650;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}th:first-child,td:first-child{padding-left:24px}td{padding:13px 18px;border-bottom:1px solid #283137;white-space:nowrap}tbody tr:last-child td{border-bottom:0}tr.selected{background:#222d32}tr:hover{background:#202a2f}.day-select{display:block;text-align:left;color:var(--ink);background:transparent;border:0;padding:0;line-height:1.55;font-size:12px}.day-select small{display:block;color:var(--muted);font-size:10px}.coverage-number{display:flex;align-items:baseline;gap:5px;font-size:12px}.coverage-number small{font-size:10px;color:var(--muted)}.progress{height:3px;width:126px;border-radius:2px;background:#303c42;margin-top:7px;overflow:hidden}.progress span{display:block;height:100%;background:var(--green)}.source-count{color:#c1ceca;font-size:11px}.source-count.incomplete{color:var(--amber)}.empty{padding:40px!important;text-align:center;color:var(--muted)}.legend{display:flex;gap:14px;flex-wrap:wrap;font-size:10px;color:var(--muted)}.legend span{display:flex;align-items:center;gap:6px}.legend i{display:block;width:7px;height:7px;border-radius:2px}.slots{display:flex;gap:5px;padding:0 24px;overflow-x:auto}.hour{display:grid;grid-template-rows:16px repeat(12,5px);gap:3px;flex:1;min-width:14px}.hour-label{font-size:9px;color:var(--muted);font-variant-numeric:tabular-nums;text-align:center}.slot{display:block;border-radius:1px;min-height:5px}.complete{background:#76c9a4}.partial{background:#bd9558}.missing{bacLine truncated
.coverage-definitions{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;margin:0;padding:0 24px 24px}.coverage-definitions dt{font-size:11px;font-weight:650;margin-bottom:6px}.coverage-definitions dd{margin:0;color:var(--muted);font-size:11px;line-height:1.7}
.coverage-definitions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin:0;padding:0 24px 24px}.coverage-definitions dt{font-size:11px;font-weight:650;margin-bottom:6px}.coverage-definitions dd{margin:0;color:var(--muted);font-size:11px;line-height:1.7}
@media(max-width:900px){.metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.metric{padding:20px}.source-column{display:none}.legend{max-width:215px;gap:10px}.slots{gap:4px}.progress{width:115px}}
@media(max-width:560px){.coverage-definitions{grid-template-columns:1fr;gap:14px;padding:0 16px 20px}.coverage-definitions dt,.coverage-definitions dd{font-size:10px}}
@media(max-width:560px){main{padding:0 16px}.masthead{height:82px}.brand{font-size:11px;gap:8px}.sun{font-size:30px}.header-actions{gap:8px}.refresh span{display:none}.refresh{font-size:17px;padding:4px 9px}.badge{font-size:10px;padding:6px 8px}.intro{padding-top:30px}.metrics{gap:10px;margin-bottom:20px}.metric{padding:17px 14px}.metric strong{font-size:26px}.metric strong span{font-size:14px}.metric-label{margin-bottom:14px}.metric small{font-size:9px}.panel-heading{padding:20px 16px 0;align-items:flex-start}h2{font-size:16px}.panel-note{padding:0 16px;font-size:10px}th,td{padding-left:11px;padding-right:11px}th:first-child,td:first-child{padding-left:16px}.progress{width:88px}.range{flex-direction:column;align-items:flex-end;gap:5px}.legend{font-size:9px;gap:7px;max-width:150px}.slots{padding:0 16px;gap:3px}.hour{min-width:10px;grid-template-rows:16px repeat(12,5px);gap:3px}.hour-label{font-size:8px}.gap-summary{padding:16px;font-size:10px}.day-select{font-size:11px}footer{font-size:9px;flex-wrap:wrap}.detail .panel-heading{gap:8px}}
+10 -9
View File
@@ -48,7 +48,7 @@
const fill = addText(progress,'span','');
fill.style.width = (day.elapsed ? day.complete/day.elapsed*100:0)+'%';
row.append(amount);
for(const field of ['energy_complete','meter_complete','boiler_complete']) {
for(const field of ['energy_complete','boiler_complete']) {
const cell = td('source-column');
addText(cell,'span',number(day[field])+'/'+number(day.elapsed),'source-count'+(day[field]<day.elapsed?' incomplete':''));
row.append(cell);
@@ -63,8 +63,9 @@
const day = current.days.find(item => item.date===selected);
if(!day) return;
$('selected-date').textContent = format(day.date+'T12:00:00Z',{weekday:'long',day:'numeric',month:'long',year:'numeric'});
$('selected-summary').textContent = `${number(day.complete)} complete of ${number(day.elapsed)} elapsed intervals · ${number(day.expected)} intervals in this calendar day. Each cell is five minutes.`;
$('selected-summary').textContent = `${number(day.complete)} complete of ${number(day.elapsed)} assessed intervals · ${number(day.expected)} intervals in this calendar day. Each cell is five minutes.`;
const start = Date.parse(day.day_start);
const currentIndex = day.status==='in_progress' ? Math.floor((Date.parse(current.generated_at)-start)/300000) : -1;
const stamp = index => format(new Date(start+index*300000),{hour:'2-digit',minute:'2-digit',timeZoneName:'shortOffset',hour12:false});
$('slots').replaceChildren();
let hour;
@@ -74,9 +75,9 @@
addText(hour,'span',format(new Date(start+index*300000),{hour:'2-digit',hour12:false}),'hour-label');
}
const mask = day.slots[index];
const state = mask===7?'complete':mask===-1?'future':mask>0?'partial':'missing';
const missing = ['Energy','Meter','Boiler'].filter((_,bit) => !(mask & (1<<bit)));
const text = `${stamp(index)}–${stamp(index+1)} · ${mask===7?'Complete':mask===-1?'Upcoming':'Incomplete: '+missing.join(', ')}`;
const state = mask===3?'complete':mask===-1?'future':mask>0?'partial':'missing';
const missing = ['Energy','Boiler'].filter((_,bit) => !(mask & (1<<bit)));
const text = `${stamp(index)}–${stamp(index+1)} · ${mask===3?'Complete':mask===-1?(index===currentIndex?'Awaiting extraction':'Upcoming'):'Incomplete: '+missing.join(', ')}`;
const cell = addText(hour,'span','','slot '+state);
cell.title = text;
cell.setAttribute('role','img');
@@ -84,12 +85,12 @@
}
const ranges = [];
for(let index=0;index<day.elapsed;) {
if(day.slots[index]===7) {index++;continue;}
if(day.slots[index]===3) {index++;continue;}
const from = index;
while(index<day.elapsed && day.slots[index]!==7) index++;
while(index<day.elapsed && day.slots[index]!==3) index++;
ranges.push(`${stamp(from)}–${stamp(index)}`);
}
$('missing-ranges').textContent = ranges.length ? `${number(day.missing)} incomplete intervals. Gaps: ${ranges.slice(0,6).join('; ')}${ranges.length>6?`; plus ${number(ranges.length-6)} more ranges.`:'.'}` : day.status==='in_progress'?'All elapsed intervals are complete. The current interval is still being recorded.':'All five-minute intervals have complete data.';
$('missing-ranges').textContent = ranges.length ? `${number(day.missing)} incomplete intervals. Gaps: ${ranges.slice(0,6).join('; ')}${ranges.length>6?`; plus ${number(ranges.length-6)} more ranges.`:'.'}` : day.status==='in_progress'?(day.slots[currentIndex]===-1?'All previous intervals are complete. Waiting for the current extraction.':'All intervals checked so far have complete data.'):'All five-minute intervals have complete data.';
}
function render(data) {
current = data;
@@ -112,7 +113,7 @@
$('upload-detail').textContent = !data.archive.upload_enabled?'Uploads not configured':data.archive.pending?'Saved locally, awaiting delivery':'All observations acknowledged';
$('last-upload').textContent = 'Last acknowledgement: '+(data.archive.last_upload?date(data.archive.last_upload)+' '+time(data.archive.last_upload):'none yet');
$('samples').textContent = number(data.archive.samples);
$('retention').textContent = `Acknowledged readings kept for ${number(data.archive.retention_days)} days · pending readings retained`;
$('retention').textContent = `Retention: ${number(data.archive.retention_days)} full calendar days · pending days retained`;
$('version').textContent = 'solar-relay '+data.version;
if(!selected || !data.days.some(day=>day.date===selected)) selected=(data.days.find(day=>day.samples>0)||yesterday||data.days[0]).date;
renderDays();