8080#define WEB_REFRESH_SECONDS 21600
8181#endif
8282
83+ #ifndef WEB_REFRESH_MINUTES
84+ #define WEB_REFRESH_MINUTES (WEB_REFRESH_SECONDS / 60 )
85+ #endif
86+
8387#ifndef SUMMARY_FETCH_INTERVAL_SECONDS
8488#define SUMMARY_FETCH_INTERVAL_SECONDS 21600UL
8589#endif
@@ -158,7 +162,6 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
158162 --header-bg: #ffffff;
159163 --meta-color: #475569;
160164 --card-bg: #ffffff;
161- --filter-bg: #ffffff;
162165 --table-border: rgba(15,23,42,0.08);
163166 }
164167 body[data-theme="dark"] {
@@ -167,7 +170,6 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
167170 --header-bg: #1e293b;
168171 --meta-color: #94a3b8;
169172 --card-bg: #1e293b;
170- --filter-bg: #1e293b;
171173 --table-border: rgba(255,255,255,0.08);
172174 }
173175 body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); transition: background 0.3s, color 0.3s; }
@@ -179,13 +181,6 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
179181 .icon-button { width: 40px; height: 40px; border-radius: 50%; border: 1px solid rgba(148,163,184,0.4); background: var(--card-bg); color: var(--text); font-size: 1.1rem; cursor: pointer; }
180182 main { padding: 24px; max-width: 1400px; margin: 0 auto; }
181183 .card { background: var(--card-bg); border-radius: 16px; padding: 20px; box-shadow: 0 25px 60px rgba(15,23,42,0.15); border: 1px solid rgba(15,23,42,0.08); }
182- .filter-bar { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-end; margin-bottom: 20px; background: var(--filter-bg); padding: 12px 16px; border-radius: 12px; box-shadow: 0 10px 30px rgba(15,23,42,0.15); border: 1px solid rgba(15,23,42,0.08); }
183- .filter-bar label { display: flex; flex-direction: column; font-size: 0.9rem; color: var(--meta-color); }
184- .filter-bar select { margin-top: 6px; padding: 8px 10px; border-radius: 6px; border: 1px solid rgba(148,163,184,0.4); background: var(--bg); color: var(--text); min-width: 220px; }
185- .filter-actions { display: flex; gap: 12px; flex-wrap: wrap; }
186- .btn { padding: 10px 16px; border-radius: 999px; border: none; background: linear-gradient(135deg,#22d3ee,#818cf8); color: #0f172a; font-weight: 600; cursor: pointer; box-shadow: 0 15px 30px rgba(14,165,233,0.25); transition: transform 0.15s ease, box-shadow 0.15s ease; }
187- .btn:disabled { opacity: 0.55; cursor: not-allowed; box-shadow: none; }
188- .btn:not(:disabled):hover { transform: translateY(-1px); box-shadow: 0 20px 40px rgba(14,165,233,0.45); }
189184 table { width: 100%; border-collapse: collapse; margin-top: 12px; }
190185 th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--table-border); }
191186 th { text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.75rem; color: var(--meta-color); }
@@ -219,30 +214,17 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
219214 </div>
220215 </header>
221216 <main>
222- <div class="filter-bar">
223- <label>
224- Site Filter
225- <select id="siteFilter">
226- <option value="">All Sites</option>
227- </select>
228- </label>
229- <div class="filter-actions">
230- <button class="btn" id="refreshSiteBtn">Refresh Selected Site</button>
231- <button class="btn" id="refreshAllBtn">Refresh All Sites</button>
232- </div>
233- </div>
234217 <section class="card">
235218 <div style="display:flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap;">
236219 <h2 style="margin:0; font-size:1.2rem;">Fleet Snapshot</h2>
237- <span class="timestamp">Dashboard auto-refresh: )HTML" STR(WEB_REFRESH_SECONDS ) R"HTML( s </span>
220+ <span class="timestamp">Dashboard auto-refresh: )HTML" STR(WEB_REFRESH_MINUTES ) R"HTML( min </span>
238221 </div>
239222 <table>
240223 <thead>
241224 <tr>
242225 <th>Site</th>
243226 <th>Tank</th>
244227 <th>Level (ft/in)</th>
245- <th>VIN Voltage</th>
246228 <th>24hr Change</th>
247229 <th>Updated</th>
248230 </tr>
@@ -281,19 +263,14 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
281263 lastFetch: document.getElementById('lastFetch'),
282264 nextFetch: document.getElementById('nextFetch'),
283265 refreshHint: document.getElementById('refreshHint'),
284- tankBody: document.getElementById('tankBody'),
285- siteFilter: document.getElementById('siteFilter'),
286- refreshSiteBtn: document.getElementById('refreshSiteBtn'),
287- refreshAllBtn: document.getElementById('refreshAllBtn')
266+ tankBody: document.getElementById('tankBody')
288267 };
289268
290269 const state = {
291- tanks: [],
292- selected: '',
293- refreshing: false
270+ tanks: []
294271 };
295272
296- function applyTankData(data, preferredUid ) {
273+ function applyTankData(data) {
297274 els.viewerName.textContent = data.viewerName || 'Tank Alarm Viewer';
298275 els.viewerUid.textContent = data.viewerUid || '--';
299276 els.sourceServer.textContent = data.sourceServerName || 'Server';
@@ -303,81 +280,27 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
303280 els.nextFetch.textContent = formatEpoch(data.nextFetchEpoch);
304281 els.refreshHint.textContent = describeCadence(data.refreshSeconds, data.baseHour);
305282 state.tanks = data.tanks || [];
306- const desired = preferredUid || state.selected;
307- populateSiteFilter(desired);
308283 renderTankRows();
309284 }
310285
311- async function fetchTanks(preferredUid ) {
286+ async function fetchTanks() {
312287 try {
313288 const res = await fetch('/api/tanks');
314289 if (!res.ok) throw new Error('HTTP ' + res.status);
315290 const data = await res.json();
316- applyTankData(data, preferredUid );
291+ applyTankData(data);
317292 } catch (err) {
318293 console.error('Viewer refresh failed', err);
319294 }
320295 }
321296
322- async function triggerManualRefresh(targetUid) {
323- const payload = targetUid ? { client: targetUid } : {};
324- setRefreshBusy(true);
325- try {
326- const res = await fetch('/api/refresh', {
327- method: 'POST',
328- headers: { 'Content-Type': 'application/json' },
329- body: JSON.stringify(payload)
330- });
331- if (!res.ok) {
332- const text = await res.text();
333- throw new Error(text || 'Refresh failed');
334- }
335- const data = await res.json();
336- applyTankData(data, targetUid || state.selected);
337- } catch (err) {
338- console.error('Manual refresh failed', err);
339- } finally {
340- setRefreshBusy(false);
341- }
342- }
343-
344- function populateSiteFilter(preferredUid) {
345- const uniqueClients = new Map();
346- state.tanks.forEach(tank => {
347- if (!tank.client) return;
348- if (!uniqueClients.has(tank.client)) {
349- const label = tank.site || `Client ${tank.client.slice(-4)}`;
350- uniqueClients.set(tank.client, label);
351- }
352- });
353- const select = els.siteFilter;
354- select.innerHTML = '<option value="">All Sites</option>';
355- uniqueClients.forEach((label, uid) => {
356- const option = document.createElement('option');
357- const suffix = uid.length > 6 ? uid.slice(-6) : uid;
358- option.value = uid;
359- option.textContent = `${label} (${suffix})`;
360- select.appendChild(option);
361- });
362- if (preferredUid && uniqueClients.has(preferredUid)) {
363- select.value = preferredUid;
364- state.selected = preferredUid;
365- } else if (!uniqueClients.has(state.selected)) {
366- select.value = '';
367- state.selected = '';
368- } else {
369- select.value = state.selected;
370- }
371- updateButtonState();
372- }
373-
374297 function renderTankRows() {
375298 const tbody = els.tankBody;
376299 tbody.innerHTML = '';
377- const rows = state.selected ? state.tanks.filter(t => t.client === state.selected) : state. tanks;
300+ const rows = state.tanks;
378301 if (!rows.length) {
379302 const tr = document.createElement('tr');
380- tr.innerHTML = '<td colspan="6 ">No tank data available</td>';
303+ tr.innerHTML = '<td colspan="5 ">No tank data available</td>';
381304 tbody.appendChild(tr);
382305 return;
383306 }
@@ -392,7 +315,6 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
392315 <td>${escapeHtml(tank.site, '--')}</td>
393316 <td>${escapeHtml(tank.label || 'Tank')} #${escapeHtml((tank.tank ?? '?'))}</td>
394317 <td>${formatFeetInches(tank.levelInches)}</td>
395- <td>${formatVoltage(tank.vinVoltage)}</td>
396318 <td>--</td>
397319 <td>${formatEpoch(tank.lastUpdate)}${staleWarning}</td>`;
398320 if (isStale) {
@@ -403,13 +325,6 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
403325 });
404326 }
405327
406- function formatVoltage(voltage) {
407- if (typeof voltage !== 'number' || !isFinite(voltage) || voltage <= 0) {
408- return '--';
409- }
410- return voltage.toFixed(2) + ' V';
411- }
412-
413328 function statusBadge(tank) {
414329 if (!tank.alarm) {
415330 return '<span class="status-pill ok">Normal</span>';
@@ -452,36 +367,8 @@ static const char VIEWER_DASHBOARD_HTML[] PROGMEM = R"HTML(
452367 return String(value).replace(/[&<>"']/g, c => entityMap[c] || c);
453368 }
454369
455- function setRefreshBusy(busy) {
456- state.refreshing = busy;
457- updateButtonState();
458- }
459-
460- function updateButtonState() {
461- els.refreshAllBtn.disabled = state.refreshing;
462- els.refreshSiteBtn.disabled = state.refreshing || !state.selected;
463- }
464-
465- els.siteFilter.addEventListener('change', event => {
466- state.selected = event.target.value;
467- renderTankRows();
468- updateButtonState();
469- });
470-
471- els.refreshSiteBtn.addEventListener('click', () => {
472- if (!state.selected) {
473- alert('Select a site to refresh.');
474- return;
475- }
476- triggerManualRefresh(state.selected);
477- });
478-
479- els.refreshAllBtn.addEventListener('click', () => {
480- triggerManualRefresh(null);
481- });
482-
483370 fetchTanks();
484- setInterval(() => fetchTanks(state.selected ), REFRESH_SECONDS * 1000);
371+ setInterval(() => fetchTanks(), REFRESH_SECONDS * 1000);
485372 })();
486373 </script>
487374</body>
@@ -503,7 +390,6 @@ static double computeNextAlignedEpoch(double epoch, uint8_t baseHour, uint32_t i
503390static void scheduleNextSummaryFetch ();
504391static void fetchViewerSummary ();
505392static void handleViewerSummary (JsonDocument &doc, double epoch);
506- static void handleRefreshPost (EthernetClient &client, const String &body);
507393
508394void setup () {
509395 Serial.begin (115200 );
@@ -688,8 +574,6 @@ static void handleWebRequests() {
688574 sendDashboard (client);
689575 } else if (method == " GET" && path == " /api/tanks" ) {
690576 sendTankJson (client);
691- } else if (method == " POST" && path == " /api/refresh" ) {
692- handleRefreshPost (client, body);
693577 } else {
694578 respondStatus (client, 404 , " Not Found" );
695579 }
@@ -948,27 +832,3 @@ static void handleViewerSummary(JsonDocument &doc, double epoch) {
948832 Serial.print (gTankRecordCount );
949833 Serial.println (F (" tanks)" ));
950834}
951-
952- static void handleRefreshPost (EthernetClient &client, const String &body) {
953- char clientUid[48 ] = {0 };
954- if (body.length () > 0 ) {
955- DynamicJsonDocument doc (128 );
956- if (deserializeJson (doc, body) == DeserializationError::Ok) {
957- const char *uid = doc[" client" ] | " " ;
958- if (uid && *uid) {
959- strlcpy (clientUid, uid, sizeof (clientUid));
960- }
961- }
962- }
963-
964- if (clientUid[0 ]) {
965- Serial.print (F (" Viewer manual refresh requested for client " ));
966- Serial.println (clientUid);
967- } else {
968- Serial.println (F (" Viewer manual refresh requested for all sites" ));
969- }
970-
971- fetchViewerSummary ();
972- scheduleNextSummaryFetch ();
973- sendTankJson (client);
974- }
0 commit comments