Skip to content

Commit a68f463

Browse files
committed
Added latest updates, make history page more useful
1 parent d60a6b6 commit a68f463

6 files changed

Lines changed: 99 additions & 35 deletions

File tree

ESPGeiger/src/Counter/Counter.cpp

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "../GRNG/GRNG.h"
3333
#include "../WebPortal/WebPortal.h"
3434
#include "../UdpBlip/UdpBlip.h"
35+
#include "HIST_JS.gz.h"
3536
#ifdef ESPG_HV_ADC
3637
#include "../HV/HV.h"
3738
#endif
@@ -232,6 +233,15 @@ void Counter::save_lifetime() {
232233
rtc_life_stash(0, 0);
233234
}
234235

236+
unsigned long Counter::get_lifetime_seconds() const {
237+
unsigned long live = _lifetime_seconds_saved;
238+
if (_last_save_time_t > 0 && ntpclient.synced) {
239+
time_t now = time(nullptr);
240+
if ((uint32_t)now >= _last_save_time_t) live += (uint32_t)now - _last_save_time_t;
241+
}
242+
return live;
243+
}
244+
235245
void Counter::reset_lifetime() {
236246
total_clicks_lifetime = 0;
237247
total_clicks_lifetime_rollover = 0;
@@ -583,48 +593,73 @@ static void hJson(EGHttpRequest& req, EGHttpResponse& res, void*) {
583593
res.endChunked();
584594
}
585595

586-
// /hist is a pure-JS render: lifetime block, 24h bar chart, hour table.
587-
// All values come from /clicks (single source of truth).
596+
// /hist body + /histjs. All values come from /clicks.
588597
static const char HISTORY_BODY[] PROGMEM = R"HTML(
589598
<div id=lf style=display:none><h2>Lifetime</h2><div class=card><div id=g2>
590599
<div><span class=muted>Clicks </span><b id=lfc>&mdash;</b></div>
591-
<div><span class=muted>&micro;Sv </span><b id=lfs>&mdash;</b></div>
600+
<div><span class=muted><span class=usvL data-on=µR data-off=µSv>&micro;Sv</span> </span><b id=lfs class=usv>&mdash;</b></div>
592601
<div><span class=muted>Tracked </span><b id=lfd>&mdash;</b></div>
593602
<div><span class=muted>Install age </span><b id=lfi>&mdash;</b></div>
594603
<div><span class=muted>Avg CPM </span><b id=lfa>&mdash;</b></div>
595604
</div><p style="margin:.8em 0 0"><button class=danger onclick="if(confirm('Reset lifetime counters?'))fetch('/life/reset',{method:'POST'}).then(()=>location.reload())">Reset lifetime</button></p></div></div>
596605
<h2>Last 24 hours</h2>
597-
<div id=bc style="display:flex;align-items:flex-end;gap:1px;height:80px;margin:.4em 0 1em;border-bottom:1px solid var(--border)"></div>
606+
<div class=card style="margin:.4em 0"><div id=g2>
607+
<div><span class=muted>Today </span><b id=hsT>&mdash;</b></div>
608+
<div><span class=muted>Yesterday </span><b id=hsY>&mdash;</b></div>
609+
<div><span class=muted id=hsAL>24h avg CPM </span><b id=hsA>&mdash;</b></div>
610+
<div><span class=muted>24h peak CPM </span><b id=hsP>&mdash;</b></div>
611+
<div><span class=muted>Today vs yesterday </span><b id=hsD>&mdash;</b></div>
612+
</div></div>
613+
<div id=bc style="display:flex;align-items:flex-end;gap:1px;height:80px;max-height:80px;width:100%;overflow:hidden;margin:.4em 0 1em;border-bottom:1px solid var(--border)"></div>
598614
<table>
599-
<thead><tr><th>Date</th><th>Clicks</th><th>Avg CPM</th><th>&micro;Sv</th></tr></thead>
615+
<thead><tr><th>Date</th><th>Clicks</th><th>Avg CPM</th><th><span class=usvL>&micro;Sv</span></th></tr></thead>
600616
<tbody id=tb></tbody>
601617
</table>
602-
<script>
618+
<script src=/histjs)HTML" EG_CACHE_BUST R"HTML(></script>
619+
)HTML";
620+
621+
static const char HIST_JS[] PROGMEM = R"JS(
603622
fetch('/clicks').then(r=>r.json()).then(o=>{
604-
var tb=byID('tb'),
623+
var $=byID,N=Date.now()/1000|0,T=o.today||0,Y=o.yesterday||0,
624+
tb=$('tb'),
605625
start='start' in o?new Date(o.start*1000):new Date(Date.now()-o.uptime*1000),
606626
rlv='roll' in o?o.roll*1000:3600000,
607-
rows='',mx=0;
627+
rows='',mx=0,sum=0,
628+
F=s=>s>=86400?(s/86400).toFixed(1)+'d':s>=3600?(s/3600).toFixed(1)+'h':s>=60?(s/60|0)+'m':s+'s';
608629
if('life' in o){
609-
var L=o.life;
610-
var fmtDur=function(s){return s>=86400?(s/86400).toFixed(1)+'d':s>=3600?(s/3600).toFixed(1)+'h':s>=60?Math.floor(s/60)+'m':s+'s'};
611-
byID('lfc').textContent=L.clk.toLocaleString();
612-
byID('lfs').textContent=(L.clk/60/o.ratio).toFixed(3);
613-
// Fallback to wall-clock-since-fbt for firmware predating the secs field.
614-
var ts=L.secs>0?L.secs:(L.fbt>0?Math.floor(Date.now()/1000)-L.fbt:0);
615-
if(ts>0){
616-
byID('lfd').textContent=fmtDur(ts);
617-
byID('lfa').textContent=(L.clk*60/ts).toFixed(1);
618-
}
619-
if(L.fbt>0) byID('lfi').textContent=fmtDur(Math.floor(Date.now()/1000)-L.fbt);
620-
byID('lf').style.display='';
630+
var L=o.life,ts=L.secs>0?L.secs:(L.fbt>0?N-L.fbt:0);
631+
$('lfc').textContent=L.clk.toLocaleString();
632+
setUsv($('lfs'),L.clk/60/o.ratio);
633+
if(ts>0){$('lfd').textContent=F(ts);$('lfa').textContent=(L.clk*60/ts).toFixed(1)}
634+
if(L.fbt>0)$('lfi').textContent=F(N-L.fbt);
635+
$('lf').style.display='';
621636
}
622-
o.last_day.forEach(function(n){if(n>mx)mx=n});
637+
var nowMs=Date.now(),curB=Math.floor(nowMs/rlv)*rlv,pk=0,bs=o.start*1000;
638+
o.last_day.forEach((n,i)=>{
639+
sum+=n;if(n>mx)mx=n;
640+
var sd=curB-i*rlv,ed=i?sd+rlv:nowMs;
641+
if(i===o.last_day.length-1&&sd<bs)sd=bs;
642+
var ms=ed-sd;
643+
if(ms>0){var r=n*60000/ms;if(r>pk)pk=r}
644+
});
645+
$('hsT').textContent=T.toLocaleString();
646+
$('hsY').textContent=Y.toLocaleString();
647+
var es='start' in o?Math.max(0,N-o.start):o.uptime||0,
648+
h60=rlv>=3600000&&es>=3600,
649+
cpmA=h60?((o.last_day[1]||0)*(60-new Date().getMinutes())/60+(o.last_day[0]||0))/60
650+
:es>0?sum*60/Math.min(es,86400):0,
651+
p=Y>0?(T-Y)*100/Y:0;
652+
$('hsA').textContent=Math.round(cpmA);
653+
$('hsAL').textContent=(h60?'1h ':'')+'avg CPM ';
654+
$('hsP').textContent=Math.round(pk);
655+
$('hsD').textContent=Y>0?(p>=0?'+':'')+p.toFixed(1)+'%':'—';
623656
if(mx<1)mx=1;
624657
var bars='';
625-
for(var i=o.last_day.length-1;i>=0;i--){
626-
var v=o.last_day[i],h=(v/mx*100).toFixed(1);
627-
bars+='<div style="flex:1;background:var(--accent);height:'+h+'%" title="'+v+'"></div>';
658+
for(var i=23;i>=0;i--){
659+
var v=o.last_day[i]||0,
660+
h=v?(v/mx*100).toFixed(1)+'%':'3px',
661+
bg=v?'var(--accent)':'var(--border)';
662+
bars+='<div style="flex:1;min-width:0;background:'+bg+';height:'+h+';min-height:3px;max-height:100%" title="'+v+'"></div>';
628663
}
629664
byID('bc').innerHTML=bars;
630665
o.last_day.forEach(function(n,idx){
@@ -634,12 +669,13 @@ fetch('/clicks').then(r=>r.json()).then(o=>{
634669
if(idx==0)ed=new Date();
635670
if(idx==o.last_day.length-1&&sd<start)sd=start;
636671
var mins=(ed-sd)/60000;
637-
rows+='<tr><td>'+sd.toLocaleString()+'</td><td>'+n+'</td><td>'+Math.ceil(n/mins)+'</td><td>'+(n/mins/o.ratio).toFixed(4)+'</td></tr>';
672+
var uv=n/mins/o.ratio;
673+
rows+='<tr><td>'+sd.toLocaleString()+'</td><td>'+n+'</td><td>'+Math.ceil(n/mins)+'</td><td class=usv data-uv="'+uv+'">'+uv.toFixed(4)+'</td></tr>';
638674
});
639675
tb.innerHTML=rows;
676+
if(window.applyRad)applyRad();
640677
});
641-
</script>
642-
)HTML";
678+
)JS";
643679

644680
static void hHistory(EGHttpRequest& req, EGHttpResponse& res, void*) {
645681
res.beginChunked(200, "text/html");
@@ -649,6 +685,15 @@ static void hHistory(EGHttpRequest& req, EGHttpResponse& res, void*) {
649685
res.endChunked();
650686
}
651687

688+
static void hHistJs(EGHttpRequest& req, EGHttpResponse& res, void*) {
689+
res.addHeader("Cache-Control", "public, max-age=31536000, immutable");
690+
#if EG_GZ_HIST_JS
691+
res.sendGzipP(200, "application/javascript", HIST_JS_GZ, HIST_JS_GZ_LEN);
692+
#else
693+
res.send(200, "application/javascript", FPSTR(HIST_JS));
694+
#endif
695+
}
696+
652697
static void hLifeReset(EGHttpRequest& req, EGHttpResponse& res, void*) {
653698
gcounter.reset_lifetime();
654699
res.send(200, "text/plain", "OK");
@@ -659,6 +704,7 @@ void CounterRoutes::registerRoutes(EGHttpServer& http) {
659704
http.on("/lastdata", EGHttpRequest::GET, hLastData);
660705
http.on("/json", EGHttpRequest::GET, hJson);
661706
http.on("/hist", EGHttpRequest::GET, hHistory);
707+
http.on("/histjs", EGHttpRequest::GET, hHistJs);
662708
http.on("/life/reset", EGHttpRequest::POST, hLifeReset);
663709
#if GEIGER_IS_TEST(GEIGER_TYPE)
664710
http.on("/cpm", EGHttpRequest::GET, hSetCPM);

ESPGeiger/src/Counter/Counter.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ class Counter {
187187
void set_first_boot_ts(uint32_t v) { _first_boot_ts = v; }
188188
uint32_t get_first_boot_ts() const { return _first_boot_ts; }
189189
void set_lifetime_seconds(unsigned long v) { _lifetime_seconds_saved = v; }
190-
unsigned long get_lifetime_seconds() const { return _lifetime_seconds_saved; }
190+
unsigned long get_lifetime_seconds() const; // live: saved + uncommitted delta
191191
void save_lifetime();
192192
void reset_lifetime();
193193
CircularBuffer<int,45> cpm_history;

ESPGeiger/src/WebPortal/WebPortal.cpp

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -393,17 +393,34 @@ void WebPortal::hFavicon(EGHttpRequest& req, EGHttpResponse& res, void*) {
393393
static const char THEME_JS[] PROGMEM = R"JS(var byID=t=>document.getElementById(t);
394394
!function(){
395395
var d=document.documentElement,L=addEventListener,
396-
TE=()=>dispatchEvent(new Event('themechange'));
396+
TE=()=>dispatchEvent(new Event('themechange')),
397+
AR=el=>{
398+
var u=d.classList.contains('crt'),
399+
list=el&&el.dataset?[el]:document.querySelectorAll('.usv');
400+
list.forEach(e=>{
401+
var v=parseFloat(e.dataset.uv);
402+
if(isNaN(v))return;
403+
e.textContent=u?(v*114).toFixed(v*114>=10?1:2):v.toFixed(3);
404+
e.title=u?v.toFixed(3)+' µSv/h':'';
405+
});
406+
document.querySelectorAll('.usvL').forEach(e=>{e.textContent=u?(e.dataset.on||'µR/h'):(e.dataset.off||'µSv/h')});
407+
};
408+
window.setUsv=(el,v)=>{el.dataset.uv=v;AR(el)};
409+
window.applyRad=AR;
397410
function C(){var s=localStorage.crt,a=new Date();
398411
if(s==='1'||(s==null&&a.getMonth()===3&&a.getDate()===1))d.classList.add('crt');
399-
else if(s==='0')d.classList.remove('crt')}
412+
else if(s==='0')d.classList.remove('crt');
413+
AR();}
400414
window.theme=()=>{var t=d.dataset.theme=='dark'?'light':'dark';
401415
d.dataset.theme=localStorage.theme=t;TE()};
402416
d.dataset.theme=localStorage.theme||(matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light');
403417
C();L('pageshow',C);
404418
var k=[38,38,40,40,37,39,37,39,66,65],i=0;
405419
L('keydown',e=>{i=e.keyCode===k[i]?i+1:0;
406-
if(i===k.length){localStorage.crt=d.classList.toggle('crt')?'1':'0';TE();i=0}})
420+
if(i===k.length){
421+
localStorage.crt=d.classList.toggle('crt')?'1':'0';
422+
AR();TE();i=0;
423+
}})
407424
}();)JS";
408425
#endif
409426

@@ -1892,7 +1909,7 @@ static const char STATUS_BODY[] PROGMEM = R"HTML(
18921909
<div id=g2></div>
18931910
<table>
18941911
<tr><th>CPM</th><td><span id=blip></span><span id=cpm>-</span></td><th>CPS</th><td><span id=cs>-</span></td></tr>
1895-
<tr><th>&micro;Sv/h</th><td><span id=usv>-</span></td><th>Total clicks</th><td><span id=tc>-</span></td></tr>
1912+
<tr><th><span class=usvL>&micro;Sv/h</span></th><td><span id=usv class=usv>-</span></td><th>Total clicks</th><td><span id=tc>-</span></td></tr>
18961913
<tr><th>Uptime</th><td><span id=upt>-</span></td><th>Signal</th><td><span id=rssi>-</span></td></tr>
18971914
</table>
18981915
<h2>Console</h2>
@@ -2035,7 +2052,7 @@ setInterval(function(){A+=100/I;if(A>=1){F();if((A-=1)>3)A=3}},100);
20352052
var Q=function(){if(!window._csk){window._csk=1;O(f,100)}},t=function(){var n=new X;n.open("GET","/json",!0);
20362053
n.onload=function(){if(n.status>=200&&n.status<400){var o=JSON.parse(n.responseText),u=o.ut;
20372054
U.textContent=(u/86400|0)+"T"+P((u/3600|0)%24)+":"+P((u/60|0)%60)+":"+P(u%60);
2038-
C.textContent=o.c.toFixed(2);T.textContent=o.tc;V.textContent=(o.c/o.r).toFixed(4);S.textContent=o.cs.toFixed(2);
2055+
C.textContent=o.c.toFixed(2);T.textContent=o.tc;setUsv(V,o.c/o.r);S.textContent=o.cs.toFixed(2);
20392056
var v=o.rssi,p=v<=-100?0:v>=-50?100:2*(v+100);R.textContent=v+' dBm ('+p+'%)';
20402057
e.update([o.c,o.c5,o.c15]);var r=o.c5>0&&o.c>0?o.c/o.c5:1;
20412058
I=Math.max(100,Math.min(4e3,2e3/r));

docs/configuration/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ Settings changed on the Config page take effect immediately (or on next submissi
2626
| Warning CPM | Int 0-9999 | `50` | CPM threshold for warning state |
2727
| Alert CPM | Int 0-9999 | `100` | CPM threshold for alert state |
2828
| Web password | String (32) | `(empty)` | Optional HTTP Basic Auth password (user is `admin`). Blank disables auth. Sensitive. |
29-
| Track lifetime | Boolean | `true` | Persist total clicks + first-boot timestamp across reboots. Toggling off pauses the counter without losing the saved value. View totals and reset from `/hist`. |
29+
| Track lifetime | Boolean | `true` | Persist total clicks + first-boot timestamp + tracked seconds across reboots. Tracked seconds advance only over spans where clicks were persisted, so the lifetime CPM on `/hist` stays accurate across crashes. Toggling off pauses the counter without losing the saved value. View totals and reset from `/hist`. |
3030

3131
### Config import / export
3232

docs/outputs/webportal.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The ESPGeiger web portal exposes a number of HTTP endpoints that are useful for
3838
| `/` | Main home page |
3939
| `/status` | Detailed device status page (live readings, uptime, build info) |
4040
| `/hist` | Rolling 24 h CPM history view with bar chart, hour-by-hour table, and (when enabled) a lifetime stats card with a **Reset lifetime** button |
41-
| `/clicks` | JSON feed of the 24 h hourly bucket array plus today/yesterday totals and (when enabled) the lifetime block - powers `/hist` |
41+
| `/clicks` | JSON feed of the 24 h hourly bucket array plus today/yesterday totals and (when enabled) the lifetime block `{clk, fbt, secs}` - powers `/hist`. `secs` = tracked seconds (CPM denominator, advances only over persisted spans); `fbt` = first-boot epoch (install age). |
4242
| `/json` | Machine-readable status snapshot - see [JSON Endpoint](/output/integrations#json-endpoint) |
4343
| `/lastdata` | GeigerLog-compatible CSV line - see [GeigerLog](/output/integrations#geigerlog) |
4444
| `/info` | Human-readable HTML page with full device + network identity - see [/info page](#info-page) |

scripts/gzip_assets.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
("ESPGeiger/src/NTP/NTP.cpp", "NTP_PAGE_JS", "application/javascript"),
2626
("ESPGeiger/src/WebPortal/WebPortal.cpp", "STYLE_CSS", "text/css"),
2727
("ESPGeiger/src/WebPortal/WebPortal.cpp", "THEME_JS", "application/javascript"),
28+
("ESPGeiger/src/Counter/Counter.cpp", "HIST_JS", "application/javascript"),
2829
]
2930

3031
ASSETS_COMBINED = [

0 commit comments

Comments
 (0)