1515 - ' TankAlarm-112025-Server-BluesOpta/*.ino'
1616 - ' TankAlarm-112025-Viewer-BluesOpta/*.ino'
1717 - ' .github/workflows/html-render.yml'
18+ types : [opened, synchronize, reopened, review_requested]
1819 workflow_dispatch :
1920
2021jobs :
2122 render-html :
2223 runs-on : ubuntu-latest
2324 permissions :
2425 contents : write
26+ pull-requests : write
2527 steps :
2628 - name : Checkout repository
2729 uses : actions/checkout@v4
@@ -197,127 +199,96 @@ jobs:
197199 with open(ino_file, 'r', encoding='utf-8', errors='ignore') as f:
198200 content = f.read()
199201
200- # Find the DASHBOARD_HTML constant
201- html_pattern = r'static const char DASHBOARD_HTML\[\] PROGMEM = R"HTML\((.*?)\)HTML"'
202- match = re.search(html_pattern, content, re.DOTALL)
202+ current_time = int(time.time())
203203
204- if match:
205- html_content = match.group(1)
204+ # Sample data for dashboard and client console
205+ sample_clients_data = {
206+ "si": "dev:server001",
207+ "srv": {
208+ "n": "Demo Tank Alarm Server",
209+ "cf": "demo-tankalarm-clients",
210+ "ps": False,
211+ "pc": True
212+ },
213+ "nde": current_time + 43200,
214+ "lse": current_time - 180,
215+ "cs": [
216+ {"c": "dev:client001", "s": "North Facility", "n": "Primary", "k": "1", "l": 78.5, "p": 85.3, "a": False, "u": current_time - 180, "v": 12.4},
217+ {"c": "dev:client001", "s": "North Facility", "n": "Secondary", "k": "2", "l": 45.2, "p": 52.1, "a": False, "u": current_time - 180, "v": 12.4},
218+ {"c": "dev:client002", "s": "South Facility", "n": "Storage", "k": "1", "l": 18.3, "p": 22.5, "a": True, "aType": "LOW", "u": current_time - 240, "v": 11.8},
219+ {"c": "dev:client003", "s": "East Facility", "n": "Main", "k": "1", "l": 92.1, "p": 95.8, "a": False, "u": current_time - 150, "v": 12.6},
220+ {"c": "dev:client004", "s": "West Facility", "n": "Backup", "k": "1", "l": 12.7, "p": 15.2, "a": True, "aType": "CRITICAL", "u": current_time - 90, "v": 10.2}
221+ ]
222+ }
223+
224+ # Extract all HTML pages
225+ html_pages = {
226+ 'DASHBOARD_HTML': 'dashboard',
227+ 'CONFIG_GENERATOR_HTML': 'config-generator',
228+ 'SERIAL_MONITOR_HTML': 'serial-monitor',
229+ 'CALIBRATION_HTML': 'calibration',
230+ 'CONTACTS_MANAGER_HTML': 'contacts',
231+ 'CLIENT_CONSOLE_HTML': 'client-console'
232+ }
233+
234+ for const_name, file_name in html_pages.items():
235+ pattern = rf'static const char {const_name}\[\] PROGMEM = R"HTML\((.*?)\)HTML"'
236+ match = re.search(pattern, content, re.DOTALL)
206237
207- # Create sample data for the server dashboard
208- current_time = int(time.time())
209- sample_data = {
210- "serverUid": "dev:server001",
211- "server": {
212- "name": "Demo Tank Alarm Server",
213- "clientFleet": "demo-tankalarm-clients",
214- "webRefreshSeconds": 60,
215- "pinConfigured": True
216- },
217- "nextDailyEmailEpoch": current_time + 43200, # 12 hours from now
218- "lastSyncEpoch": current_time - 180, # 3 minutes ago
219- "clients": [
220- {
221- "uid": "dev:client001",
222- "site": "North Facility",
223- "lastUpdate": current_time - 180,
224- "tanks": [
225- {
226- "tank": 1,
227- "label": "Primary",
228- "levelInches": 78.5,
229- "percent": 85.3,
230- "alarm": False,
231- "lastUpdate": current_time - 180
232- },
233- {
234- "tank": 2,
235- "label": "Secondary",
236- "levelInches": 45.2,
237- "percent": 52.1,
238- "alarm": False,
239- "lastUpdate": current_time - 180
240- }
241- ]
242- },
243- {
244- "uid": "dev:client002",
245- "site": "South Facility",
246- "lastUpdate": current_time - 240,
247- "tanks": [
248- {
249- "tank": 1,
250- "label": "Storage",
251- "levelInches": 18.3,
252- "percent": 22.5,
253- "alarm": True,
254- "alarmType": "LOW",
255- "lastUpdate": current_time - 240
256- }
257- ]
258- },
259- {
260- "uid": "dev:client003",
261- "site": "East Facility",
262- "lastUpdate": current_time - 150,
263- "tanks": [
264- {
265- "tank": 1,
266- "label": "Main",
267- "levelInches": 92.1,
268- "percent": 95.8,
269- "alarm": False,
270- "lastUpdate": current_time - 150
271- }
238+ if match:
239+ html_content = match.group(1)
240+
241+ # For dashboard, inject sample data
242+ if const_name == 'DASHBOARD_HTML':
243+ sample_json = json.dumps(sample_clients_data)
244+ html_content = html_content.replace('refreshData();', f'applyServerData({sample_json});')
245+
246+ # For client console, inject sample data
247+ elif const_name == 'CLIENT_CONSOLE_HTML':
248+ sample_json = json.dumps(sample_clients_data)
249+ html_content = html_content.replace('refreshData();', f'applyServerData({sample_json}, "dev:client001");')
250+
251+ # For serial monitor, inject sample log data
252+ elif const_name == 'SERIAL_MONITOR_HTML':
253+ sample_logs = {
254+ "logs": [
255+ {"timestamp": current_time - 60, "level": "INFO", "source": "server", "message": "Server initialized successfully"},
256+ {"timestamp": current_time - 45, "level": "INFO", "source": "server", "message": "Connected to Notehub"},
257+ {"timestamp": current_time - 30, "level": "DEBUG", "source": "server", "message": "Polling for client updates..."},
258+ {"timestamp": current_time - 15, "level": "INFO", "source": "server", "message": "Received telemetry from dev:client001"}
272259 ]
273- },
274- {
275- "uid": "dev:client004",
276- "site": "West Facility",
277- "lastUpdate": current_time - 90,
260+ }
261+ sample_json = json.dumps(sample_logs)
262+ html_content = html_content.replace('refreshServerLogs();', f'renderLogs(els.serverLogs, {sample_json}.logs);')
263+
264+ # For calibration, inject sample calibration data
265+ elif const_name == 'CALIBRATION_HTML':
266+ sample_cal = {
267+ "calibrations": [
268+ {"clientUid": "dev:client001", "tankNumber": 1, "entryCount": 5, "hasLearnedCalibration": True, "rSquared": 0.987, "learnedSlope": 7.42, "originalMaxValue": 120, "lastCalibrationEpoch": current_time - 86400},
269+ {"clientUid": "dev:client002", "tankNumber": 1, "entryCount": 2, "hasLearnedCalibration": False, "rSquared": 0, "learnedSlope": 0, "originalMaxValue": 96, "lastCalibrationEpoch": current_time - 172800}
270+ ],
271+ "logs": []
272+ }
273+ sample_tanks = {
278274 "tanks": [
279- {
280- "tank": 1,
281- "label": "Backup",
282- "levelInches": 12.7,
283- "percent": 15.2,
284- "alarm": True,
285- "alarmType": "CRITICAL",
286- "lastUpdate": current_time - 90
287- }
275+ {"client": "dev:client001", "tank": 1, "site": "North Facility", "label": "Primary", "heightInches": 120, "levelInches": 78.5, "sensorMa": 14.2},
276+ {"client": "dev:client002", "tank": 1, "site": "South Facility", "label": "Storage", "heightInches": 96, "levelInches": 18.3, "sensorMa": 6.8}
288277 ]
289278 }
290- ]
291- }
292-
293- # Inject the sample data
294- sample_data_json = json.dumps(sample_data)
295-
296- # Replace the refreshData() call with data injection
297- html_content = html_content.replace(
298- 'refreshData();',
299- f'applyServerData({sample_data_json}, null);'
300- )
301-
302- output_file = output_dir + '/dashboard.html'
303- with open(output_file, 'w', encoding='utf-8') as out:
304- out.write(html_content)
305-
306- print(f'Extracted dashboard HTML: {len(html_content)} bytes')
307- print(f'Injected sample data with {len(sample_data["clients"])} clients')
308-
309- # Find the CONFIG_GENERATOR_HTML constant
310- config_pattern = r'static const char CONFIG_GENERATOR_HTML\[\] PROGMEM = R"HTML\((.*?)\)HTML"'
311- config_match = re.search(config_pattern, content, re.DOTALL)
312-
313- if config_match:
314- config_html = config_match.group(1)
315-
316- config_file = output_dir + '/config-generator.html'
317- with open(config_file, 'w', encoding='utf-8') as out:
318- out.write(config_html)
319-
320- print(f'Extracted config generator HTML: {len(config_html)} bytes')
279+ html_content = html_content.replace('loadTanks();', f'tanks = {json.dumps(sample_tanks["tanks"])}; populateTankDropdowns();')
280+ html_content = html_content.replace('loadCalibrationData();', f'calibrations = {json.dumps(sample_cal["calibrations"])}; calibrationLogs = []; updateCalibrationStats(); updateCalibrationTable(); updateCalibrationLog();')
281+
282+ # For contacts, inject sample contact data
283+ elif const_name == 'CONTACTS_MANAGER_HTML':
284+ # Just render the page without API calls
285+ html_content = html_content.replace('loadData();', '// Sample data mode - loadData() disabled for static preview')
286+
287+ output_file = f'{output_dir}/{file_name}.html'
288+ with open(output_file, 'w', encoding='utf-8') as out:
289+ out.write(html_content)
290+
291+ print(f'Extracted {file_name} HTML: {len(html_content)} bytes')
321292 EOF
322293
323294 - name : Extract and render HTML for 112025 Viewer
@@ -602,11 +573,35 @@ jobs:
602573 This document contains screenshots of the web interface served by the TankAlarm-112025-Server-BluesOpta.
603574
604575 ## Dashboard
576+ Main fleet telemetry dashboard showing server metadata, statistics, and fleet telemetry table with relay controls.
577+
605578 
606579
580+ ## Client Console
581+ Configuration management interface for remote clients with PIN-protected controls.
582+
583+ 
584+
607585 ## Config Generator
586+ Create new client configurations with sensor definitions and alarm thresholds.
587+
608588 
609589
590+ ## Serial Monitor
591+ Debug log viewer with server and client serial output.
592+
593+ 
594+
595+ ## Calibration
596+ Calibration learning system with manual level reading submission and drift analysis.
597+
598+ 
599+
600+ ## Contacts Manager
601+ Contact and notification management with alarm associations.
602+
603+ 
604+
610605 ---
611606 *Screenshots automatically generated by GitHub Actions*
612607 *Last updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*
@@ -620,13 +615,94 @@ jobs:
620615 This document contains screenshots of the web interface served by the TankAlarm-112025-Viewer-BluesOpta.
621616
622617 ## Dashboard
618+ Read-only fleet telemetry dashboard showing server metadata and fleet snapshot with tank levels.
619+
623620 
624621
625622 ---
626623 *Screenshots automatically generated by GitHub Actions*
627624 *Last updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*
628625 EOF
629626
627+ - name : Upload screenshots as artifacts
628+ if : github.event_name == 'pull_request'
629+ uses : actions/upload-artifact@v4
630+ with :
631+ name : website-screenshots
632+ path : |
633+ TankAlarm-092025-Server-Hologram/screenshots/
634+ TankAlarm-112025-Server-BluesOpta/screenshots/
635+ TankAlarm-112025-Viewer-BluesOpta/screenshots/
636+ retention-days : 7
637+
638+ - name : Post preview comment on PR
639+ if : github.event_name == 'pull_request'
640+ uses : actions/github-script@v7
641+ with :
642+ script : |
643+ const fs = require('fs');
644+ const path = require('path');
645+
646+ // Check which screenshots were generated
647+ const screenshotDirs = [
648+ { dir: 'TankAlarm-112025-Server-BluesOpta/screenshots', name: '112025 Server' },
649+ { dir: 'TankAlarm-112025-Viewer-BluesOpta/screenshots', name: '112025 Viewer' },
650+ { dir: 'TankAlarm-092025-Server-Hologram/screenshots', name: '092025 Server' }
651+ ];
652+
653+ let commentBody = '## 🖼️ Website Preview\n\n';
654+ commentBody += 'Screenshots have been generated for the web pages modified in this PR.\n\n';
655+ commentBody += '> **Note:** Download the `website-screenshots` artifact to view the full-resolution images.\n\n';
656+
657+ for (const { dir, name } of screenshotDirs) {
658+ if (fs.existsSync(dir)) {
659+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.png'));
660+ if (files.length > 0) {
661+ commentBody += `### ${name}\n\n`;
662+ commentBody += '| Page | Screenshot |\n';
663+ commentBody += '|------|------------|\n';
664+ for (const file of files.sort()) {
665+ const pageName = file.replace('.png', '').replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
666+ commentBody += `| ${pageName} | ✅ Generated |\n`;
667+ }
668+ commentBody += '\n';
669+ }
670+ }
671+ }
672+
673+ commentBody += '---\n';
674+ commentBody += `*Generated by GitHub Actions on ${new Date().toISOString()}*`;
675+
676+ // Find existing comment to update or create new one
677+ const { data: comments } = await github.rest.issues.listComments({
678+ owner: context.repo.owner,
679+ repo: context.repo.repo,
680+ issue_number: context.issue.number
681+ });
682+
683+ const botComment = comments.find(comment =>
684+ comment.user.type === 'Bot' &&
685+ comment.body.includes('## 🖼️ Website Preview')
686+ );
687+
688+ if (botComment) {
689+ await github.rest.issues.updateComment({
690+ owner: context.repo.owner,
691+ repo: context.repo.repo,
692+ comment_id: botComment.id,
693+ body: commentBody
694+ });
695+ console.log('Updated existing preview comment');
696+ } else {
697+ await github.rest.issues.createComment({
698+ owner: context.repo.owner,
699+ repo: context.repo.repo,
700+ issue_number: context.issue.number,
701+ body: commentBody
702+ });
703+ console.log('Created new preview comment');
704+ }
705+
630706 - name : Commit and push changes
631707 if : github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
632708 run : |
0 commit comments