Add WLED health monitoring, calibration test mode, and UI improvements
Some checks failed
Validate / validate (push) Failing after 8s
Some checks failed
Validate / validate (push) Failing after 8s
- Add background health checks (GET /json/info) with configurable interval per device - Auto-detect LED count from WLED device on add (remove led_count from create API) - Add calibration test mode: toggle edges on/off with colored LEDs via PUT endpoint - Show WLED firmware version badge and LED count badge on device cards - Add modal dirty tracking with discard confirmation on close/backdrop click - Fix layout jump when modals open by compensating for scrollbar width - Add state_check_interval to settings API and UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,10 +10,12 @@
|
||||
<body style="visibility: hidden;">
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1 data-i18n="app.title">WLED Screen Controller</h1>
|
||||
<div class="server-info">
|
||||
<span id="server-version"><span data-i18n="app.version">Version:</span> <span id="version-number">Loading...</span></span>
|
||||
<div class="header-title">
|
||||
<span id="server-status" class="status-badge">●</span>
|
||||
<h1 data-i18n="app.title">WLED Screen Controller</h1>
|
||||
<span id="server-version"><span id="version-number"></span></span>
|
||||
</div>
|
||||
<div class="server-info">
|
||||
<button class="theme-toggle" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
|
||||
<span id="theme-icon">🌙</span>
|
||||
</button>
|
||||
@@ -21,9 +23,6 @@
|
||||
<option value="en">English</option>
|
||||
<option value="ru">Русский</option>
|
||||
</select>
|
||||
<span id="auth-status" style="margin-left: 10px; display: none; white-space: nowrap;">
|
||||
<span id="logged-in-user" style="color: #4CAF50;" data-i18n="auth.authenticated">Authenticated</span>
|
||||
</span>
|
||||
<button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
|
||||
🔑 <span data-i18n="auth.login">Login</span>
|
||||
</button>
|
||||
@@ -34,25 +33,15 @@
|
||||
</header>
|
||||
|
||||
<section class="displays-section">
|
||||
<h2 data-i18n="displays.title">Available Displays</h2>
|
||||
<h2 data-i18n="displays.layout">Display Layout</h2>
|
||||
|
||||
<!-- Visual Layout Preview -->
|
||||
<div class="display-layout-preview">
|
||||
<h3 data-i18n="displays.layout">Display Layout</h3>
|
||||
<div id="display-layout-canvas" class="display-layout-canvas">
|
||||
<div class="loading" data-i18n="displays.loading">Loading layout...</div>
|
||||
</div>
|
||||
<div class="layout-legend">
|
||||
<span class="legend-item"><span class="legend-dot primary"></span> <span data-i18n="displays.legend.primary">Primary Display</span></span>
|
||||
<span class="legend-item"><span class="legend-dot secondary"></span> <span data-i18n="displays.legend.secondary">Secondary Display</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display Cards -->
|
||||
<h3 style="margin-top: 30px;" data-i18n="displays.information">Display Information</h3>
|
||||
<div id="displays-list" class="displays-grid">
|
||||
<div class="loading" data-i18n="displays.loading">Loading displays...</div>
|
||||
</div>
|
||||
<div id="displays-list" style="display: none;"></div>
|
||||
</section>
|
||||
|
||||
<section class="devices-section">
|
||||
@@ -78,11 +67,6 @@
|
||||
<label for="device-url" data-i18n="device.url">WLED URL:</label>
|
||||
<input type="url" id="device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="device-led-count" data-i18n="device.led_count">LED Count:</label>
|
||||
<input type="number" id="device-led-count" value="150" min="1" required>
|
||||
<small class="input-hint" data-i18n="device.led_count.hint">Number of LEDs configured in your WLED device</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" data-i18n="device.button.add">Add Device</button>
|
||||
</form>
|
||||
</section>
|
||||
@@ -108,115 +92,81 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="calibration-device-id">
|
||||
<p style="margin-bottom: 20px; color: var(--text-secondary);" data-i18n="calibration.description">
|
||||
Configure how your LED strip is mapped to screen edges. Use test buttons to verify each edge lights up correctly.
|
||||
<p style="margin-bottom: 12px; color: var(--text-secondary);" data-i18n="calibration.description">
|
||||
Configure how your LED strip is mapped to screen edges. Click an edge to toggle test mode.
|
||||
</p>
|
||||
|
||||
<!-- Visual Preview -->
|
||||
<div style="margin-bottom: 25px;">
|
||||
<div style="position: relative; width: 400px; height: 250px; margin: 0 auto; background: var(--card-bg); border: 2px solid var(--border-color); border-radius: 8px;">
|
||||
<!-- Screen representation -->
|
||||
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; height: 180px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 4px; display: flex; align-items: center; justify-content: center; color: white; font-size: 14px;" data-i18n="calibration.preview.screen">
|
||||
Screen
|
||||
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
||||
<div style="margin-bottom: 12px;">
|
||||
<div class="calibration-preview">
|
||||
<!-- Screen with direction toggle -->
|
||||
<div class="preview-screen">
|
||||
<span data-i18n="calibration.preview.screen">Screen</span>
|
||||
<button type="button" class="direction-toggle" onclick="toggleDirection()" title="Toggle direction">
|
||||
<span id="direction-icon">↻</span> <span id="direction-label">CW</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Edge labels -->
|
||||
<div style="position: absolute; top: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);">
|
||||
<span data-i18n="calibration.preview.top">Top:</span> <span id="preview-top-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
|
||||
<!-- Clickable edge bars with LED count inputs -->
|
||||
<div class="preview-edge edge-top" onclick="toggleTestEdge('top')">
|
||||
<span>T</span>
|
||||
<input type="number" id="cal-top-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
<div style="position: absolute; right: 5px; top: 50%; transform: translateY(-50%) rotate(90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;">
|
||||
<span data-i18n="calibration.preview.right">Right:</span> <span id="preview-right-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
|
||||
<div class="preview-edge edge-right" onclick="toggleTestEdge('right')">
|
||||
<span>R</span>
|
||||
<input type="number" id="cal-right-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
<div style="position: absolute; bottom: 5px; left: 50%; transform: translateX(-50%); font-size: 12px; color: var(--text-secondary);">
|
||||
<span data-i18n="calibration.preview.bottom">Bottom:</span> <span id="preview-bottom-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
|
||||
<div class="preview-edge edge-bottom" onclick="toggleTestEdge('bottom')">
|
||||
<span>B</span>
|
||||
<input type="number" id="cal-bottom-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
<div style="position: absolute; left: 5px; top: 50%; transform: translateY(-50%) rotate(-90deg); font-size: 12px; color: var(--text-secondary); white-space: nowrap;">
|
||||
<span data-i18n="calibration.preview.left">Left:</span> <span id="preview-left-count">0</span> <span data-i18n="calibration.preview.leds">LEDs</span>
|
||||
<div class="preview-edge edge-left" onclick="toggleTestEdge('left')">
|
||||
<span>L</span>
|
||||
<input type="number" id="cal-left-leds" class="edge-led-input" min="0" value="0"
|
||||
oninput="updateCalibrationPreview()" onclick="event.stopPropagation()">
|
||||
</div>
|
||||
|
||||
<!-- Starting position indicator -->
|
||||
<div id="start-indicator" style="position: absolute; bottom: 10px; left: 10px; width: 12px; height: 12px; background: #4CAF50; border-radius: 50%; border: 2px solid white;"></div>
|
||||
<!-- Corner start position buttons -->
|
||||
<div class="preview-corner corner-top-left" onclick="setStartPosition('top_left')">●</div>
|
||||
<div class="preview-corner corner-top-right" onclick="setStartPosition('top_right')">●</div>
|
||||
<div class="preview-corner corner-bottom-left" onclick="setStartPosition('bottom_left')">●</div>
|
||||
<div class="preview-corner corner-bottom-right" onclick="setStartPosition('bottom_right')">●</div>
|
||||
</div>
|
||||
<p class="preview-hint" data-i18n="calibration.preview.click_hint">Click an edge to toggle test LEDs on/off</p>
|
||||
</div>
|
||||
|
||||
<!-- Layout Configuration -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
|
||||
<div class="form-group">
|
||||
<label for="cal-start-position" data-i18n="calibration.start_position">Starting Position:</label>
|
||||
<select id="cal-start-position" onchange="updateCalibrationPreview()">
|
||||
<option value="bottom_left" data-i18n="calibration.position.bottom_left">Bottom Left</option>
|
||||
<option value="bottom_right" data-i18n="calibration.position.bottom_right">Bottom Right</option>
|
||||
<option value="top_left" data-i18n="calibration.position.top_left">Top Left</option>
|
||||
<option value="top_right" data-i18n="calibration.position.top_right">Top Right</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cal-layout" data-i18n="calibration.direction">Direction:</label>
|
||||
<select id="cal-layout" onchange="updateCalibrationPreview()">
|
||||
<option value="clockwise" data-i18n="calibration.direction.clockwise">Clockwise</option>
|
||||
<option value="counterclockwise" data-i18n="calibration.direction.counterclockwise">Counterclockwise</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cal-offset" data-i18n="calibration.offset">LED Offset:</label>
|
||||
<input type="number" id="cal-offset" min="0" value="0" oninput="updateCalibrationPreview()">
|
||||
<small style="color: #aaa; display: block; margin-top: 4px;" data-i18n="calibration.offset_hint">LEDs from LED 0 to start corner (along strip)</small>
|
||||
</div>
|
||||
<!-- Hidden selects (used by saveCalibration) -->
|
||||
<div style="display: none;">
|
||||
<select id="cal-start-position">
|
||||
<option value="bottom_left">Bottom Left</option>
|
||||
<option value="bottom_right">Bottom Right</option>
|
||||
<option value="top_left">Top Left</option>
|
||||
<option value="top_right">Top Right</option>
|
||||
</select>
|
||||
<select id="cal-layout">
|
||||
<option value="clockwise">Clockwise</option>
|
||||
<option value="counterclockwise">Counterclockwise</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- LED Counts per Edge -->
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px;">
|
||||
<div class="form-group">
|
||||
<label for="cal-top-leds" data-i18n="calibration.leds.top">Top LEDs:</label>
|
||||
<input type="number" id="cal-top-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cal-right-leds" data-i18n="calibration.leds.right">Right LEDs:</label>
|
||||
<input type="number" id="cal-right-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cal-bottom-leds" data-i18n="calibration.leds.bottom">Bottom LEDs:</label>
|
||||
<input type="number" id="cal-bottom-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cal-left-leds" data-i18n="calibration.leds.left">Left LEDs:</label>
|
||||
<input type="number" id="cal-left-leds" min="0" value="0" oninput="updateCalibrationPreview()">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 12px;">
|
||||
<label for="cal-offset" data-i18n="calibration.offset">LED Offset:</label>
|
||||
<input type="number" id="cal-offset" min="0" value="0" oninput="updateCalibrationPreview()">
|
||||
<small style="color: #aaa; display: block; margin-top: 4px;" data-i18n="calibration.offset_hint">LEDs from LED 0 to start corner (along strip)</small>
|
||||
</div>
|
||||
|
||||
<div style="padding: 10px; background: rgba(255, 193, 7, 0.1); border-left: 4px solid #FFC107; border-radius: 4px; margin-bottom: 20px;">
|
||||
<div style="padding: 8px 10px; background: rgba(255, 193, 7, 0.1); border-left: 4px solid #FFC107; border-radius: 4px; margin-bottom: 12px;">
|
||||
<strong data-i18n="calibration.total">Total LEDs:</strong> <span id="cal-total-leds">0</span> / <span id="cal-device-led-count">0</span>
|
||||
</div>
|
||||
|
||||
<!-- Test Buttons -->
|
||||
<div style="margin-bottom: 15px;">
|
||||
<p style="font-weight: 600; margin-bottom: 10px;" data-i18n="calibration.test">Test Edges (lights up each edge):</p>
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;">
|
||||
<button class="btn btn-secondary" onclick="testCalibrationEdge('top')" style="font-size: 0.9rem; padding: 8px;">
|
||||
⬆️ <span data-i18n="calibration.test.top">Top</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="testCalibrationEdge('right')" style="font-size: 0.9rem; padding: 8px;">
|
||||
➡️ <span data-i18n="calibration.test.right">Right</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="testCalibrationEdge('bottom')" style="font-size: 0.9rem; padding: 8px;">
|
||||
⬇️ <span data-i18n="calibration.test.bottom">Bottom</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="testCalibrationEdge('left')" style="font-size: 0.9rem; padding: 8px;">
|
||||
⬅️ <span data-i18n="calibration.test.left">Left</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="calibration-error" class="error-message" style="display: none;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeCalibrationModal()" data-i18n="calibration.button.cancel">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="saveCalibration()" data-i18n="calibration.button.save">Save Calibration</button>
|
||||
<button class="btn btn-primary" onclick="saveCalibration()" data-i18n="calibration.button.save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -242,18 +192,11 @@
|
||||
<small class="input-hint" data-i18n="settings.url.hint">IP address or hostname of your WLED device</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-device-led-count" data-i18n="device.led_count">LED Count:</label>
|
||||
<input type="number" id="settings-device-led-count" min="1" required>
|
||||
<small class="input-hint" data-i18n="device.led_count.hint">Number of LEDs configured in your WLED device</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="settings-device-brightness"><span data-i18n="settings.brightness">Brightness:</span> <span id="brightness-value">100%</span></label>
|
||||
<input type="range" id="settings-device-brightness" min="0" max="100" value="100"
|
||||
oninput="document.getElementById('brightness-value').textContent = this.value + '%'"
|
||||
style="width: 100%;">
|
||||
<small class="input-hint" data-i18n="settings.brightness.hint">Global brightness for this WLED device (0-100%)</small>
|
||||
<label for="settings-health-interval" data-i18n="settings.health_interval">Health Check Interval (s):</label>
|
||||
<input type="number" id="settings-health-interval" min="5" max="600" value="30">
|
||||
<small class="input-hint" data-i18n="settings.health_interval.hint">How often to check the WLED device status (5-600 seconds)</small>
|
||||
</div>
|
||||
|
||||
<div id="settings-error" class="error-message" style="display: none;"></div>
|
||||
@@ -346,24 +289,13 @@
|
||||
const apiKey = localStorage.getItem('wled_api_key');
|
||||
const loginBtn = document.getElementById('login-btn');
|
||||
const logoutBtn = document.getElementById('logout-btn');
|
||||
const authStatus = document.getElementById('auth-status');
|
||||
const loggedInUser = document.getElementById('logged-in-user');
|
||||
|
||||
if (apiKey) {
|
||||
// Logged in
|
||||
loginBtn.style.display = 'none';
|
||||
logoutBtn.style.display = 'inline-block';
|
||||
authStatus.style.display = 'inline';
|
||||
|
||||
// Show masked key
|
||||
const masked = apiKey.substring(0, 8) + '...';
|
||||
loggedInUser.textContent = `● Authenticated`;
|
||||
loggedInUser.title = `API Key: ${masked}`;
|
||||
} else {
|
||||
// Logged out
|
||||
loginBtn.style.display = 'inline-block';
|
||||
logoutBtn.style.display = 'none';
|
||||
authStatus.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -419,7 +351,7 @@
|
||||
input.placeholder = 'Enter your API key...';
|
||||
error.style.display = 'none';
|
||||
modal.style.display = 'flex';
|
||||
document.body.classList.add('modal-open');
|
||||
lockBody();
|
||||
|
||||
// Hide cancel button if this is required login (no existing session)
|
||||
cancelBtn.style.display = hideCancel ? 'none' : 'inline-block';
|
||||
@@ -430,7 +362,7 @@
|
||||
function closeApiKeyModal() {
|
||||
const modal = document.getElementById('api-key-modal');
|
||||
modal.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
unlockBody();
|
||||
}
|
||||
|
||||
function submitApiKey(event) {
|
||||
|
||||
Reference in New Issue
Block a user