Skip to content

Restructure file write logic to avoid unnecessary writes #291

Restructure file write logic to avoid unnecessary writes

Restructure file write logic to avoid unnecessary writes #291

Workflow file for this run

name: HTML Render and Screenshot
on:
push:
branches: [main, master]
paths:
- 'TankAlarm-092025-Server-Hologram/*.ino'
- 'TankAlarm-112025-Server-BluesOpta/*.ino'
- 'TankAlarm-112025-Viewer-BluesOpta/*.ino'
- '.github/workflows/html-render.yml'
pull_request:
branches: [main, master]
paths:
- 'TankAlarm-092025-Server-Hologram/*.ino'
- 'TankAlarm-112025-Server-BluesOpta/*.ino'
- 'TankAlarm-112025-Viewer-BluesOpta/*.ino'
- '.github/workflows/html-render.yml'
types: [opened, synchronize, reopened, review_requested]
workflow_dispatch:
jobs:
render-html:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ github.head_ref || github.ref_name }}
fetch-depth: 1
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Playwright
run: |
npm install playwright
npx playwright install chromium
- name: Extract and render HTML for 092025 Server
run: |
mkdir -p /tmp/html-092025
python3 << 'EOF'
import re
import os
ino_file = 'TankAlarm-092025-Server-Hologram/TankAlarm-092025-Server-Hologram.ino'
output_dir = '/tmp/html-092025'
with open(ino_file, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Find functions that generate HTML pages
pages = {
'dashboard': ('sendWebPage', 'Dashboard'),
'email-management': ('sendEmailManagementPage', 'Email Management'),
'tank-management': ('sendTankManagementPage', 'Tank Management'),
'calibration': ('sendCalibrationPage', 'Calibration'),
'404': ('send404Page', '404 Page')
}
for page_name, (func_name, title) in pages.items():
# Find the function more carefully
func_pattern = rf'void {func_name}\([^)]*\)\s*\{{'
match = re.search(func_pattern, content)
if match:
start_pos = match.end()
# Find the matching closing brace
brace_count = 1
pos = start_pos
while pos < len(content) and brace_count > 0:
if content[pos] == '{':
brace_count += 1
elif content[pos] == '}':
brace_count -= 1
pos += 1
func_body = content[start_pos:pos-1]
# Extract HTML from client.println statements
html_lines = []
for line in func_body.split('\n'):
line = line.strip()
# Match client.println("...") or client.print("...")
println_match = re.search(r'client\.print(?:ln)?\s*\(\s*[fF]?\s*\(\s*"([^"]*)"\s*\)\s*\)', line)
if not println_match:
println_match = re.search(r'client\.print(?:ln)?\s*\(\s*"([^"]*)"\s*\)', line)
if println_match:
html_content = println_match.group(1)
# Unescape the string
html_content = html_content.replace(r'\"', '"')
html_content = html_content.replace(r"\'", "'")
html_content = html_content.replace(r'\n', '\n')
html_lines.append(html_content)
# Handle String concatenation with variables
elif 'client.print' in line:
# Check for meta refresh
if 'webPageRefreshSeconds' in line:
html_lines.append("<meta http-equiv='refresh' content='30'>")
# For other dynamic content, extract what we can
elif '"' in line:
parts = re.findall(r'"([^"]*)"', line)
for part in parts:
html_lines.append(part)
# Join HTML lines
html_content = '\n'.join(html_lines)
# Add sample data for dynamic content
if page_name == 'dashboard':
# Add sample tank data grouped by sites
sample_data = '''
<h2>North Facility</h2>
<div class="tank-container">
<div class="tank-card">
<div class="tank-header">Tank #1</div>
<div class="tank-level">Level: 85.3%</div>
<div class="tank-change positive">24hr Change: +2.1%</div>
<div class="status status-normal">Status: Normal</div>
<div style="font-size: 12px; color: #666; margin-top: 10px;">Updated: 11/22/2025, 4:55:56 AM</div>
</div>
<div class="tank-card">
<div class="tank-header">Tank #2</div>
<div class="tank-level">Level: 52.1%</div>
<div class="tank-change negative">24hr Change: -3.2%</div>
<div class="status status-normal">Status: Normal</div>
<div style="font-size: 12px; color: #666; margin-top: 10px;">Updated: 11/22/2025, 4:55:56 AM</div>
</div>
</div>
<h2>South Facility</h2>
<div class="tank-container">
<div class="tank-card">
<div class="tank-header">Tank #1</div>
<div class="tank-level">Level: 22.5%</div>
<div class="tank-change negative">24hr Change: -8.5%</div>
<div class="status status-alarm">Status: LOW</div>
<div style="font-size: 12px; color: #666; margin-top: 10px;">Updated: 11/22/2025, 4:54:56 AM</div>
</div>
</div>
<h2>East Facility</h2>
<div class="tank-container">
<div class="tank-card">
<div class="tank-header">Tank #1</div>
<div class="tank-level">Level: 95.8%</div>
<div class="tank-change positive">24hr Change: +1.2%</div>
<div class="status status-normal">Status: Normal</div>
<div style="font-size: 12px; color: #666; margin-top: 10px;">Updated: 11/22/2025, 4:56:26 AM</div>
</div>
</div>
<h2>West Facility</h2>
<div class="tank-container">
<div class="tank-card">
<div class="tank-header">Tank #1</div>
<div class="tank-level">Level: 15.2%</div>
<div class="tank-change negative">24hr Change: -12.8%</div>
<div class="status status-alarm">Status: CRITICAL</div>
<div style="font-size: 12px; color: #666; margin-top: 10px;">Updated: 11/22/2025, 4:57:26 AM</div>
</div>
</div>
<div class="footer">Last updated: 2025-11-22 18:00:00 UTC</div>
'''
# Replace the "No tank reports received yet" message with sample data
html_content = html_content.replace(
"<p style='text-align: center;'>No tank reports received yet.</p>",
sample_data
)
# Make sure we have closing tags
if '</html>' not in html_content:
if '</body>' not in html_content:
html_content += '\n</body>'
html_content += '\n</html>'
# Write to file
output_file = os.path.join(output_dir, f'{page_name}.html')
with open(output_file, 'w', encoding='utf-8') as out:
out.write(html_content)
print(f'Extracted {page_name}: {len(html_lines)} lines')
EOF
- name: Extract and render HTML for 112025 Server
run: |
mkdir -p /tmp/html-112025
python3 << 'EOF'
import re
import json
import time
ino_file = 'TankAlarm-112025-Server-BluesOpta/TankAlarm-112025-Server-BluesOpta.ino'
output_dir = '/tmp/html-112025'
with open(ino_file, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
current_time = int(time.time())
# Sample data for dashboard and client console
sample_clients_data = {
"si": "dev:server001",
"srv": {
"n": "Demo Tank Alarm Server",
"cf": "demo-tankalarm-clients",
"ps": False,
"pc": True
},
"nde": current_time + 43200,
"lse": current_time - 180,
"cs": [
{"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},
{"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},
{"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},
{"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},
{"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}
]
}
# Extract all HTML pages
html_pages = {
'DASHBOARD_HTML': 'dashboard',
'CONFIG_GENERATOR_HTML': 'config-generator',
'SERIAL_MONITOR_HTML': 'serial-monitor',
'CALIBRATION_HTML': 'calibration',
'CONTACTS_MANAGER_HTML': 'contacts',
'CLIENT_CONSOLE_HTML': 'client-console'
}
for const_name, file_name in html_pages.items():
pattern = rf'static const char {const_name}\[\] PROGMEM = R"HTML\((.*?)\)HTML"'
match = re.search(pattern, content, re.DOTALL)
if match:
html_content = match.group(1)
# For dashboard, inject sample data
if const_name == 'DASHBOARD_HTML':
sample_json = json.dumps(sample_clients_data)
html_content = html_content.replace('refreshData();', f'applyServerData({sample_json});')
# For client console, inject sample data and create TWO versions
elif const_name == 'CLIENT_CONSOLE_HTML':
sample_json = json.dumps(sample_clients_data)
# Version 1: With PIN modal showing (PIN setup view)
html_with_pin = html_content.replace('refreshData();', f'''
// Inject sample data

Check failure on line 252 in .github/workflows/html-render.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/html-render.yml

Invalid workflow file

You have an error in your yaml syntax on line 252
applyServerData({sample_json}, "dev:client001");
// Show PIN setup modal immediately for preview
setTimeout(() => {{
pinState.configured = false;
pinState.value = '';
showPinModal('setup');
}}, 100);
''')
# Version 2: Without PIN modal (unlocked view)
html_unlocked = html_content.replace('refreshData();', f'''
// Inject sample data and simulate unlocked state
pinState.configured = true;
pinState.value = 'PREVIEW'; // Simulated unlocked state for preview only
applyServerData({sample_json}, "dev:client001");
// Force hide the PIN modal for the unlocked preview
setTimeout(() => {{
const modal = document.getElementById('pinModal');
if (modal) {{
modal.classList.add('hidden');
}}
}}, 200);
''')
# For serial monitor, inject sample log data
elif const_name == 'SERIAL_MONITOR_HTML':
sample_logs = {
"logs": [
{"timestamp": current_time - 60, "level": "INFO", "source": "server", "message": "Server initialized successfully"},
{"timestamp": current_time - 45, "level": "INFO", "source": "server", "message": "Connected to Notehub"},
{"timestamp": current_time - 30, "level": "DEBUG", "source": "server", "message": "Polling for client updates..."},
{"timestamp": current_time - 15, "level": "INFO", "source": "server", "message": "Received telemetry from dev:client001"}
]
}
sample_json = json.dumps(sample_logs)
html_content = html_content.replace('refreshServerLogs();', f'renderLogs(els.serverLogs, {sample_json}.logs);')
# For calibration, inject sample calibration data
elif const_name == 'CALIBRATION_HTML':
sample_cal = {
"calibrations": [
{"clientUid": "dev:client001", "tankNumber": 1, "entryCount": 5, "hasLearnedCalibration": True, "rSquared": 0.987, "learnedSlope": 7.42, "originalMaxValue": 120, "lastCalibrationEpoch": current_time - 86400},
{"clientUid": "dev:client002", "tankNumber": 1, "entryCount": 2, "hasLearnedCalibration": False, "rSquared": 0, "learnedSlope": 0, "originalMaxValue": 96, "lastCalibrationEpoch": current_time - 172800}
],
"logs": []
}
sample_tanks = {
"tanks": [
{"client": "dev:client001", "tank": 1, "site": "North Facility", "label": "Primary", "heightInches": 120, "levelInches": 78.5, "sensorMa": 14.2},
{"client": "dev:client002", "tank": 1, "site": "South Facility", "label": "Storage", "heightInches": 96, "levelInches": 18.3, "sensorMa": 6.8}
]
}
html_content = html_content.replace('loadTanks();', f'tanks = {json.dumps(sample_tanks["tanks"])}; populateTankDropdowns();')
html_content = html_content.replace('loadCalibrationData();', f'calibrations = {json.dumps(sample_cal["calibrations"])}; calibrationLogs = []; updateCalibrationStats(); updateCalibrationTable(); updateCalibrationLog();')
# For contacts, inject sample contact data
elif const_name == 'CONTACTS_MANAGER_HTML':
# Just render the page without API calls
html_content = html_content.replace('loadData();', '// Sample data mode - loadData() disabled for static preview')
# For CLIENT_CONSOLE_HTML, write the special versions
if const_name == 'CLIENT_CONSOLE_HTML':
# Write the PIN setup version
output_file_pin = f'{output_dir}/client-console-pin-setup.html'
with open(output_file_pin, 'w', encoding='utf-8') as out:
out.write(html_with_pin)
print(f'Extracted client-console-pin-setup HTML: {len(html_with_pin)} bytes')
# Write the unlocked version as the primary file
output_file = f'{output_dir}/{file_name}.html'
with open(output_file, 'w', encoding='utf-8') as out:
out.write(html_unlocked)
print(f'Extracted client-console (unlocked) HTML: {len(html_unlocked)} bytes')
else:
# Write the primary version for all other pages
output_file = f'{output_dir}/{file_name}.html'
with open(output_file, 'w', encoding='utf-8') as out:
out.write(html_content)
print(f'Extracted {file_name} HTML: {len(html_content)} bytes')
EOF
- name: Extract and render HTML for 112025 Viewer
run: |
mkdir -p /tmp/html-112025-viewer
python3 << 'EOF'
import re
import json
import time
ino_file = 'TankAlarm-112025-Viewer-BluesOpta/TankAlarm-112025-Viewer-BluesOpta.ino'
output_dir = '/tmp/html-112025-viewer'
with open(ino_file, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Find the VIEWER_DASHBOARD_HTML constant - need to handle macro splits
# The HTML is split into multiple R"HTML()HTML" sections with STR() macros in between
start_pattern = r'static const char VIEWER_DASHBOARD_HTML\[\] PROGMEM = R"HTML\('
start_match = re.search(start_pattern, content)
if start_match:
# Find the end - look for the final )HTML"; that ends the declaration
start_pos = start_match.end()
# Find the position after the HTML definition ends
end_pattern = r'\)HTML";'
remaining = content[start_pos:]
# Collect all parts, handling the macro splits
html_parts = []
current_pos = 0
while True:
# Find next )HTML"
end_match = re.search(r'\)HTML"', remaining[current_pos:])
if not end_match:
break
# Extract the HTML part
html_parts.append(remaining[current_pos:current_pos + end_match.start()])
# Check if this is the final terminator
check_pos = current_pos + end_match.end()
if check_pos < len(remaining) and remaining[check_pos:check_pos+1] == ';':
# This is the final terminator
break
# Look for the next R"HTML(
next_start = re.search(r'R"HTML\(', remaining[check_pos:])
if next_start:
# Extract the macro value in between
macro_section = remaining[check_pos:check_pos + next_start.start()]
# Replace STR(WEB_REFRESH_SECONDS) with 30
if 'STR(WEB_REFRESH_SECONDS)' in macro_section:
html_parts.append('30')
current_pos = check_pos + next_start.end()
else:
break
html_content = ''.join(html_parts)
# Create sample data for the viewer
current_time = int(time.time())
sample_data = {
"viewerName": "Demo Tank Alarm Viewer",
"viewerUid": "dev:viewer001",
"sourceServerName": "Demo Server",
"sourceServerUid": "dev:server001",
"generatedEpoch": current_time - 300, # 5 minutes ago
"lastFetchEpoch": current_time - 120, # 2 minutes ago
"nextFetchEpoch": current_time + 21480, # ~6 hours from now
"refreshSeconds": 21600, # 6 hours
"baseHour": 6,
"tanks": [
{
"client": "dev:client001",
"site": "North Facility",
"label": "Primary",
"tank": 1,
"levelInches": 78.5,
"percent": 85.3,
"alarm": False,
"lastUpdate": current_time - 180
},
{
"client": "dev:client001",
"site": "North Facility",
"label": "Secondary",
"tank": 2,
"levelInches": 45.2,
"percent": 52.1,
"alarm": False,
"lastUpdate": current_time - 180
},
{
"client": "dev:client002",
"site": "South Facility",
"label": "Storage",
"tank": 1,
"levelInches": 18.3,
"percent": 22.5,
"alarm": True,
"alarmType": "LOW",
"lastUpdate": current_time - 240
},
{
"client": "dev:client003",
"site": "East Facility",
"label": "Main",
"tank": 1,
"levelInches": 92.1,
"percent": 95.8,
"alarm": False,
"lastUpdate": current_time - 150
},
{
"client": "dev:client004",
"site": "West Facility",
"label": "Backup",
"tank": 1,
"levelInches": 12.7,
"percent": 15.2,
"alarm": True,
"alarmType": "CRITICAL",
"lastUpdate": current_time - 90
}
]
}
# Inject the sample data into the HTML by replacing the fetchTanks call
# Find the fetchTanks() call at the end of the script and replace it with data injection
sample_data_json = json.dumps(sample_data)
# Replace the async fetchTanks call with synchronous data application
html_content = html_content.replace(
'fetchTanks();',
f'applyTankData({sample_data_json}, null);'
)
# Also remove the setInterval call to prevent attempts to fetch from non-existent API
html_content = re.sub(
r'setInterval\(\(\) => fetchTanks\(state\.selected\), REFRESH_SECONDS \* 1000\);',
'// Auto-refresh disabled for static preview',
html_content
)
output_file = output_dir + '/dashboard.html'
with open(output_file, 'w', encoding='utf-8') as out:
out.write(html_content)
print(f'Extracted viewer dashboard HTML: {len(html_content)} bytes')
print(f'Injected sample data with {len(sample_data["tanks"])} tanks')
EOF
- name: Take screenshots with Playwright
run: |
node << 'EOF'
const playwright = require('playwright');
const fs = require('fs');
const path = require('path');
async function takeScreenshots() {
const browser = await playwright.chromium.launch();
const context = await browser.newContext({
viewport: { width: 1280, height: 720 }
});
const page = await context.newPage();
// Screenshot 092025 pages
const dir092025 = '/tmp/html-092025';
const output092025 = 'TankAlarm-092025-Server-Hologram/screenshots';
if (fs.existsSync(dir092025)) {
fs.mkdirSync(output092025, { recursive: true });
const files = fs.readdirSync(dir092025);
for (const file of files) {
if (file.endsWith('.html')) {
const htmlPath = path.join(dir092025, file);
const screenshotName = file.replace('.html', '.png');
const screenshotPath = path.join(output092025, screenshotName);
try {
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`Screenshot saved: ${screenshotPath}`);
} catch (error) {
console.error(`Error capturing ${file}:`, error.message);
}
}
}
}
// Screenshot 112025 pages
const dir112025 = '/tmp/html-112025';
const output112025 = 'TankAlarm-112025-Server-BluesOpta/screenshots';
if (fs.existsSync(dir112025)) {
fs.mkdirSync(output112025, { recursive: true });
const files = fs.readdirSync(dir112025);
for (const file of files) {
if (file.endsWith('.html')) {
const htmlPath = path.join(dir112025, file);
const screenshotName = file.replace('.html', '.png');
const screenshotPath = path.join(output112025, screenshotName);
try {
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`Screenshot saved: ${screenshotPath}`);
} catch (error) {
console.error(`Error capturing ${file}:`, error.message);
}
}
}
}
// Screenshot 112025 Viewer pages
const dir112025Viewer = '/tmp/html-112025-viewer';
const output112025Viewer = 'TankAlarm-112025-Viewer-BluesOpta/screenshots';
if (fs.existsSync(dir112025Viewer)) {
fs.mkdirSync(output112025Viewer, { recursive: true });
const files = fs.readdirSync(dir112025Viewer);
for (const file of files) {
if (file.endsWith('.html')) {
const htmlPath = path.join(dir112025Viewer, file);
const screenshotName = file.replace('.html', '.png');
const screenshotPath = path.join(output112025Viewer, screenshotName);
try {
await page.goto('file://' + htmlPath, { waitUntil: 'networkidle' });
await page.screenshot({ path: screenshotPath, fullPage: true });
console.log(`Screenshot saved: ${screenshotPath}`);
} catch (error) {
console.error(`Error capturing ${file}:`, error.message);
}
}
}
}
await browser.close();
}
takeScreenshots().catch(console.error);
EOF
- name: Generate WEBSITE_PREVIEW.md for 092025
run: |
cat > TankAlarm-092025-Server-Hologram/WEBSITE_PREVIEW.md << 'EOF'
# Website Preview - TankAlarm 092025 Server
This document contains screenshots of the web interface served by the TankAlarm-092025-Server-Hologram.
## Dashboard
![Dashboard](screenshots/dashboard.png)
## Email Management
![Email Management](screenshots/email-management.png)
## Tank Management
![Tank Management](screenshots/tank-management.png)
## Calibration
![Calibration](screenshots/calibration.png)
## 404 Page
![404 Page](screenshots/404.png)
---
*Screenshots automatically generated by GitHub Actions*
*Last updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*
EOF
- name: Generate WEBSITE_PREVIEW.md for 112025
run: |
cat > TankAlarm-112025-Server-BluesOpta/WEBSITE_PREVIEW.md << 'EOF'
# Website Preview - TankAlarm 112025 Server
This document contains screenshots of the web interface served by the TankAlarm-112025-Server-BluesOpta.
## Dashboard
Main fleet telemetry dashboard showing server metadata, statistics, and fleet telemetry table with relay controls.
![Dashboard](screenshots/dashboard.png)
## Client Console - PIN Setup
PIN setup modal shown on first access to secure the configuration interface.
![Client Console - PIN Setup](screenshots/client-console-pin-setup.png)
## Client Console
Configuration management interface for remote clients with PIN-protected controls (unlocked view).
![Client Console](screenshots/client-console.png)
## Config Generator
Create new client configurations with sensor definitions and alarm thresholds.
![Config Generator](screenshots/config-generator.png)
## Serial Monitor
Debug log viewer with server and client serial output.
![Serial Monitor](screenshots/serial-monitor.png)
## Calibration
Calibration learning system with manual level reading submission and drift analysis.
![Calibration](screenshots/calibration.png)
## Contacts Manager
Contact and notification management with alarm associations.
![Contacts Manager](screenshots/contacts.png)
---
*Screenshots automatically generated by GitHub Actions*
*Last updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*
EOF
- name: Generate WEBSITE_PREVIEW.md for 112025 Viewer
run: |
cat > TankAlarm-112025-Viewer-BluesOpta/WEBSITE_PREVIEW.md << 'EOF'
# Website Preview - TankAlarm 112025 Viewer
This document contains screenshots of the web interface served by the TankAlarm-112025-Viewer-BluesOpta.
## Dashboard
Read-only fleet telemetry dashboard showing server metadata and fleet snapshot with tank levels.
![Dashboard](screenshots/dashboard.png)
---
*Screenshots automatically generated by GitHub Actions*
*Last updated: $(date -u +"%Y-%m-%d %H:%M:%S UTC")*
EOF
- name: Upload screenshots as artifacts
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: website-screenshots
path: |
TankAlarm-092025-Server-Hologram/screenshots/
TankAlarm-112025-Server-BluesOpta/screenshots/
TankAlarm-112025-Viewer-BluesOpta/screenshots/
retention-days: 7
- name: Post preview comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
// Check which screenshots were generated
const screenshotDirs = [
{ dir: 'TankAlarm-112025-Server-BluesOpta/screenshots', name: '112025 Server' },
{ dir: 'TankAlarm-112025-Viewer-BluesOpta/screenshots', name: '112025 Viewer' },
{ dir: 'TankAlarm-092025-Server-Hologram/screenshots', name: '092025 Server' }
];
let commentBody = '## 🖼️ Website Preview\n\n';
commentBody += 'Screenshots have been generated for the web pages modified in this PR.\n\n';
commentBody += '> **Note:** Download the `website-screenshots` artifact to view the full-resolution images.\n\n';
for (const { dir, name } of screenshotDirs) {
if (fs.existsSync(dir)) {
const files = fs.readdirSync(dir).filter(f => f.endsWith('.png'));
if (files.length > 0) {
commentBody += `### ${name}\n\n`;
commentBody += '| Page | Screenshot |\n';
commentBody += '|------|------------|\n';
for (const file of files.sort()) {
const pageName = file.replace('.png', '').replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
commentBody += `| ${pageName} | ✅ Generated |\n`;
}
commentBody += '\n';
}
}
}
commentBody += '---\n';
commentBody += `*Generated by GitHub Actions on ${new Date().toISOString()}*`;
// Find existing comment to update or create new one
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('## 🖼️ Website Preview')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
console.log('Updated existing preview comment');
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
console.log('Created new preview comment');
}
- name: Commit and push changes
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add TankAlarm-092025-Server-Hologram/WEBSITE_PREVIEW.md TankAlarm-092025-Server-Hologram/screenshots/
git add TankAlarm-112025-Server-BluesOpta/WEBSITE_PREVIEW.md TankAlarm-112025-Server-BluesOpta/screenshots/
git add TankAlarm-112025-Viewer-BluesOpta/WEBSITE_PREVIEW.md TankAlarm-112025-Viewer-BluesOpta/screenshots/
git diff --staged --quiet || git commit -m "Update website preview screenshots [skip ci]"
git push origin HEAD:${{ github.head_ref || github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}