/
ellersseer
/
MyCheat
Обзор
Документация
Войти
/
ellersseer
/
MyCheat
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
2
CI/CD
Аналитика
Безопасность
master
data/settings.html
887 строк
31 KB
Dmitry Zhiltsov
feat(ota): TSKMCHT-2 Добавить возможность обновления через OTA
17 фев 2026, 16:27
17 фев 2026, 16:27
971fa2a
Код
Авторство
О чём код?
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MyCheat Settings</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; padding: 20px; } .container { max-width: 600px; margin: 0 auto; } h1 { color: #00d4ff; text-align: center; margin-bottom: 30px; font-size: 1.8em; } .card { background: #16213e; border-radius: 12px; padding: 20px; margin-bottom: 20px; border: 1px solid #0f3460; } .card h2 { color: #00d4ff; font-size: 1.2em; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #0f3460; } .form-group { margin-bottom: 15px; } label { display: block; color: #aaa; font-size: 0.9em; margin-bottom: 5px; } input, select { width: 100%; padding: 12px; border: 1px solid #0f3460; border-radius: 8px; background: #1a1a2e; color: #eee; font-size: 1em; } input::placeholder { color: #6b7280; opacity: 1; } input:focus, select:focus { outline: none; border-color: #00d4ff; } .checkbox-group { display: flex; align-items: center; gap: 10px; } .checkbox-group input { width: auto; } .btn { display: inline-block; padding: 12px 24px; border: none; border-radius: 8px; font-size: 1em; cursor: pointer; transition: all 0.2s; } .btn-primary { background: #00d4ff; color: #1a1a2e; } .btn-primary:hover { background: #00b8e6; } .btn-danger { background: #e94560; color: #fff; } .btn-danger:hover { background: #d63350; } .btn-secondary { background: #0f3460; color: #eee; } .btn-secondary:hover { background: #1a4a7a; } .btn.success { background: #00d46a; } .btn.error { background: #e94560; } .btn-block { width: 100%; margin-top: 10px; } .status { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 0.85em; margin-left: 10px; } .status-connected { background: #00d46a; color: #000; } .status-disconnected { background: #e94560; color: #fff; } .status-ap { background: #ffc107; color: #000; } .network-list { max-height: 200px; overflow-y: auto; margin-bottom: 15px; } .network-item { display: flex; justify-content: space-between; align-items: center; padding: 10px; background: #1a1a2e; border-radius: 6px; margin-bottom: 5px; cursor: pointer; transition: background 0.2s; } .network-item:hover { background: #0f3460; } .network-item.selected { border: 1px solid #00d4ff; } .signal-bars { display: flex; gap: 2px; align-items: flex-end; } .signal-bar { width: 4px; background: #0f3460; border-radius: 1px; } .signal-bar.active { background: #00d4ff; } .actions { display: flex; gap: 10px; margin-top: 20px; } .actions .btn { flex: 1; text-align: center; } .device-info { display: flex; flex-direction: column; gap: 12px; } .device-info p { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #0f3460; } .device-info p:last-child { border-bottom: none; } .wifi-icon { width: 18px; height: 18px; margin-right: 8px; vertical-align: middle; } @media (max-width: 480px) { body { padding: 12px; } .card { padding: 15px; } h1 { font-size: 1.5em; } .actions { flex-direction: column; } .actions .btn { width: 100%; } } .back-link { display: block; text-align: center; color: #00d4ff; text-decoration: none; margin-top: 20px; } .back-link:hover { text-decoration: underline; } .message { padding: 12px; border-radius: 8px; margin-bottom: 15px; display: none; } .message.success { background: rgba(0, 212, 106, 0.2); border: 1px solid #00d46a; color: #00d46a; } .message.error { background: rgba(233, 69, 96, 0.2); border: 1px solid #e94560; color: #e94560; } .ip-display { font-family: monospace; color: #00d4ff; } .loading { text-align: center; padding: 20px; color: #aaa; } .footer { text-align: center; margin-top: 20px; padding: 15px 0; color: #666; font-size: 12px; border-top: 1px solid #0f3460; } .progress-bar-bg { width: 100%; height: 24px; background: #1a1a2e; border-radius: 12px; overflow: hidden; margin: 12px 0; border: 1px solid #0f3460; display: none; } .progress-bar-fill { height: 100%; width: 0%; background: #00d4ff; border-radius: 12px; transition: width 0.3s; } .ota-status { font-size: 0.9em; color: #aaa; margin-top: 8px; min-height: 1.2em; } .file-input-wrapper { position: relative; display: flex; gap: 10px; align-items: center; } .file-input-wrapper input[type="file"] { flex: 1; padding: 10px; background: #1a1a2e; border: 1px solid #0f3460; border-radius: 8px; color: #eee; cursor: pointer; } .file-input-wrapper input[type="file"]::file-selector-button { background: #0f3460; color: #eee; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; margin-right: 10px; } </style> </head> <body> <div class="container"> <h1>⚙️ MyCheat Settings</h1> <div id="message" class="message"></div> <div class="card"> <h2>WiFi Configuration <span id="wifi-status" class="status status-disconnected">Checking...</span> </h2> <div id="wifi-scan-area"> <button class="btn btn-secondary btn-block" onclick="scanNetworks()"><svg class="wifi-icon" viewBox="0 0 24 24" fill="currentColor"> <path d="M12 18c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm-4.9-2.3l1.4 1.4C9.4 16.4 10.6 16 12 16s2.6.4 3.5 1.1l1.4-1.4C15.6 14.6 13.9 14 12 14s-3.6.6-4.9 1.7zm-2.8-2.8l1.4 1.4C7.3 13 9.5 12 12 12s4.7 1 6.3 2.3l1.4-1.4C17.5 11.1 14.9 10 12 10s-5.5 1.1-7.7 2.9zM1.5 10l1.4 1.4C5.1 9.5 8.4 8 12 8s6.9 1.5 9.1 3.4l1.4-1.4C19.8 7.5 16.1 6 12 6S4.2 7.5 1.5 10z" /> </svg>Scan Networks</button> <div id="network-list" class="network-list" style="display:none; margin-top:15px;"></div> </div> <form id="wifi-form"> <div class="form-group"> <label>SSID</label> <input type="text" id="wifi-ssid" name="ssid" value="%WIFI_SSID%" required> </div> <div class="form-group"> <label>Password</label> <input type="password" id="wifi-password" name="password" placeholder="Enter new password or leave empty"> </div> <button type="submit" class="btn btn-primary btn-block">Save WiFi Settings</button> </form> </div> <div class="card"> <h2>MQTT Configuration <span id="mqtt-status" class="status status-disconnected">Checking...</span> </h2> <form id="mqtt-form"> <div class="form-group checkbox-group"> <input type="checkbox" id="mqtt-enabled" name="enabled" %MQTT_ENABLED_CHECKED%> <label for="mqtt-enabled" style="margin:0;">Enable MQTT</label> </div> <div class="form-group"> <label>Server</label> <input type="text" id="mqtt-server" name="server" value="%MQTT_SERVER%"> </div> <div class="form-group"> <label>Port</label> <input type="number" id="mqtt-port" name="port" value="%MQTT_PORT%"> </div> <div class="form-group"> <label>Username (optional)</label> <input type="text" id="mqtt-user" name="user" value="%MQTT_USER%"> </div> <div class="form-group"> <label>Password (optional)</label> <input type="password" id="mqtt-password" name="password" placeholder="Enter new password or leave empty"> </div> <div class="form-group"> <label>Client ID</label> <input type="text" id="mqtt-client-id" name="client_id" value="%MQTT_CLIENT_ID%"> </div> <div class="form-group"> <label>Topic Prefix</label> <input type="text" id="mqtt-topic-prefix" name="topic_prefix" value="%MQTT_TOPIC_PREFIX%"> </div> <button type="submit" class="btn btn-primary btn-block">Save MQTT Settings</button> </form> </div> <div class="card"> <h2>Sensor Configuration</h2> <form id="sensor-form"> <div class="form-group"> <label>Stale Timeout (minutes)</label> <input type="number" id="stale-timeout" name="staleTimeoutMinutes" min="0" max="1440" value="%STALE_TIMEOUT%"> <small style="color:#888;display:block;margin-top:4px;">Set to 0 to disable. Sensors without updates for this duration will show 85°C.</small> </div> <button type="submit" class="btn btn-primary btn-block">Save Sensor Settings</button> </form> </div> <div class="card"> <h2>Firmware Update</h2> <p style="color:#aaa; font-size:0.9em; margin-bottom:12px;"> Current version: <strong style="color:#00d4ff;" id="ota-current-version">...</strong> </p> <div class="file-input-wrapper"> <input type="file" id="ota-file" accept=".bin"> </div> <div class="progress-bar-bg" id="ota-progress-bar"> <div class="progress-bar-fill" id="ota-progress-fill"></div> </div> <div class="ota-status" id="ota-status"></div> <button class="btn btn-primary btn-block" id="ota-upload-btn" onclick="uploadFirmware()">Upload Firmware</button> <p style="color:#888; font-size:0.8em; margin-top:10px;"> Automatic rollback: if new firmware fails to start 3 times, the device reverts to the previous version. </p> </div> <div class="card"> <h2>Device</h2> <div class="device-info"> <p><span>IP Address</span><span class="ip-display" id="device-ip">Loading...</span></p> <p><span>Uptime</span><span id="device-uptime">Loading...</span></p> <p><span>Free Heap</span><span id="device-heap">Loading...</span></p> </div> <div class="actions"> <button class="btn btn-secondary" onclick="rebootDevice()">Reboot</button> <button class="btn btn-danger" onclick="factoryReset()">Factory Reset</button> </div> </div> <a href="/" class="back-link">← Back to Dashboard</a> <div class="footer"> MyCheat v<span id="version">...</span> </div> </div> <script> function showMessage(text, isError) { const msg = document.getElementById('message'); msg.textContent = text; msg.className = 'message ' + (isError ? 'error' : 'success'); msg.style.display = 'block'; setTimeout(() => msg.style.display = 'none', 5000); } function updateStatus() { fetch('/api/config') .then(r => r.json()) .then(data => { // WiFi status const wifiStatus = document.getElementById('wifi-status'); if (data.wifi.mode === 'Connected (STA)') { wifiStatus.textContent = 'Connected'; wifiStatus.className = 'status status-connected'; } else if (data.wifi.mode === 'Access Point') { wifiStatus.textContent = 'AP Mode'; wifiStatus.className = 'status status-ap'; } else { wifiStatus.textContent = 'Disconnected'; wifiStatus.className = 'status status-disconnected'; } // MQTT status (basic enabled/disabled) const mqttStatus = document.getElementById('mqtt-status'); if (data.mqtt.enabled) { mqttStatus.textContent = 'Enabled'; mqttStatus.className = 'status status-ap'; } else { mqttStatus.textContent = 'Disabled'; mqttStatus.className = 'status status-disconnected'; } // Device info document.getElementById('device-ip').textContent = data.wifi.ip; document.getElementById('device-uptime').textContent = formatUptime(data.device.uptime); document.getElementById('device-heap').textContent = formatBytes(data.device.free_heap); }) .catch(err => console.error('Status update failed:', err)); // Also fetch real-time MQTT connection status fetch('/api/status') .then(r => r.json()) .then(data => { const mqttStatus = document.getElementById('mqtt-status'); if (data.mqtt_connected) { mqttStatus.textContent = 'Connected'; mqttStatus.className = 'status status-connected'; } else if (data.mqtt_enabled) { mqttStatus.textContent = 'Disconnected'; mqttStatus.className = 'status status-disconnected'; } if (data.firmware_version) { document.getElementById('version').textContent = data.firmware_version; document.getElementById('ota-current-version').textContent = data.firmware_version; } }) .catch(err => { }); } function formatUptime(seconds) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; return `${h}h ${m}m ${s}s`; } function formatBytes(bytes) { return (bytes / 1024).toFixed(1) + ' KB'; } function scanNetworks() { const list = document.getElementById('network-list'); list.innerHTML = '<div class="loading">Scanning...</div>'; list.style.display = 'block'; console.log('Starting WiFi scan...'); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout fetch('/api/wifi/scan', { signal: controller.signal }) .then(r => { clearTimeout(timeoutId); console.log('Response status:', r.status); return r.json(); }) .then(data => { console.log('Scan result:', data); list.innerHTML = ''; if (!data.networks || data.networks.length === 0) { list.innerHTML = '<div class="loading">No networks found</div>'; return; } data.networks.forEach(net => { const item = document.createElement('div'); item.className = 'network-item'; item.onclick = () => selectNetwork(net.ssid); const bars = getSignalBars(net.quality); item.innerHTML = ` <span>${net.ssid} ${net.open ? '(Open)' : ''}</span> <div class="signal-bars">${bars}</div> `; list.appendChild(item); }); }) .catch(err => { console.error('Scan error:', err); list.innerHTML = '<div class="loading">Scan failed: ' + err.message + '</div>'; }); } function getSignalBars(quality) { let bars = ''; const heights = [6, 10, 14, 18]; for (let i = 0; i < 4; i++) { const active = quality >= (i + 1) * 25; bars += `<div class="signal-bar ${active ? 'active' : ''}" style="height:${heights[i]}px"></div>`; } return bars; } function selectNetwork(ssid) { document.getElementById('wifi-ssid').value = ssid; document.querySelectorAll('.network-item').forEach(item => { item.classList.remove('selected'); if (item.querySelector('span').textContent.startsWith(ssid)) { item.classList.add('selected'); } }); } async function submitBtn(btn, originalText, fetchFn) { btn.textContent = '...'; btn.disabled = true; try { const result = await fetchFn(); btn.textContent = '\u2713'; btn.classList.add('success'); btn.classList.remove('error'); setTimeout(() => { btn.textContent = originalText; btn.classList.remove('success'); btn.disabled = false; }, 1500); return result; } catch (err) { btn.textContent = '\u2717'; btn.classList.add('error'); btn.classList.remove('success'); setTimeout(() => { btn.textContent = originalText; btn.classList.remove('error'); btn.disabled = false; }, 1500); throw err; } } document.getElementById('wifi-form').onsubmit = async function (e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const data = { ssid: document.getElementById('wifi-ssid').value, password: document.getElementById('wifi-password').value }; try { await submitBtn(btn, 'Save WiFi Settings', async () => { const r = await fetch('/api/config/wifi', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await r.json(); if (!result.success) throw new Error(result.message); showMessage(result.message, false); if (result.restart) { showMessage('Device restarting... Please reconnect.', false); } return result; }); } catch (err) { showMessage(err.message || 'Failed to save WiFi settings', true); } }; document.getElementById('mqtt-form').onsubmit = async function (e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const data = { server: document.getElementById('mqtt-server').value, port: parseInt(document.getElementById('mqtt-port').value), user: document.getElementById('mqtt-user').value, password: document.getElementById('mqtt-password').value, client_id: document.getElementById('mqtt-client-id').value, topic_prefix: document.getElementById('mqtt-topic-prefix').value, enabled: document.getElementById('mqtt-enabled').checked }; try { await submitBtn(btn, 'Save MQTT Settings', async () => { const r = await fetch('/api/config/mqtt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await r.json(); if (!result.success) throw new Error(result.message); showMessage(result.message, false); setTimeout(updateStatus, 2000); return result; }); } catch (err) { showMessage(err.message || 'Failed to save MQTT settings', true); } }; document.getElementById('sensor-form').onsubmit = async function (e) { e.preventDefault(); const btn = this.querySelector('button[type="submit"]'); const data = { staleTimeoutMinutes: parseInt(document.getElementById('stale-timeout').value) }; try { await submitBtn(btn, 'Save Sensor Settings', async () => { const r = await fetch('/api/config/sensors', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); const result = await r.json(); if (!result.success) throw new Error(result.message); showMessage(result.message || 'Sensor settings saved', false); return result; }); } catch (err) { showMessage(err.message || 'Failed to save sensor settings', true); } }; async function rebootDevice() { if (!confirm('Are you sure you want to reboot the device?')) return; const btn = event.target; try { await submitBtn(btn, 'Reboot', async () => { await fetch('/api/reboot', { method: 'POST' }); showMessage('Device rebooting...', false); }); } catch (err) { showMessage('Reboot failed', true); } } async function factoryReset() { if (!confirm('WARNING: This will erase all settings!\n\nAre you sure you want to factory reset?')) return; const btn = event.target; try { await submitBtn(btn, 'Factory Reset', async () => { const r = await fetch('/api/factory-reset', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true }) }); const result = await r.json(); if (!result.success) throw new Error(result.message); showMessage(result.message, false); return result; }); } catch (err) { showMessage(err.message || 'Factory reset failed', true); } } function uploadFirmware() { const fileInput = document.getElementById('ota-file'); const file = fileInput.files[0]; if (!file) { showMessage('Please select a firmware file (.bin)', true); return; } if (!file.name.endsWith('.bin')) { showMessage('Please select a .bin firmware file', true); return; } if (!confirm('Upload firmware "' + file.name + '" (' + (file.size / 1024).toFixed(0) + ' KB)?\\n\\nThe device will restart after update.')) { return; } const btn = document.getElementById('ota-upload-btn'); const progressBar = document.getElementById('ota-progress-bar'); const progressFill = document.getElementById('ota-progress-fill'); const statusEl = document.getElementById('ota-status'); btn.disabled = true; btn.textContent = 'Uploading...'; progressBar.style.display = 'block'; progressFill.style.width = '0%'; statusEl.textContent = 'Starting upload...'; statusEl.style.color = '#aaa'; const formData = new FormData(); formData.append('firmware', file); const xhr = new XMLHttpRequest(); xhr.open('POST', '/api/ota/upload'); xhr.upload.onprogress = function (e) { if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); progressFill.style.width = pct + '%'; statusEl.textContent = 'Uploading: ' + pct + '% (' + (e.loaded / 1024).toFixed(0) + ' / ' + (e.total / 1024).toFixed(0) + ' KB)'; } }; xhr.onload = function () { try { const result = JSON.parse(xhr.responseText); if (result.success) { progressFill.style.width = '100%'; progressFill.style.background = '#00d46a'; statusEl.textContent = 'Firmware updated! Restarting device...'; statusEl.style.color = '#00d46a'; btn.textContent = 'Restarting...'; waitForReboot(); } else { otaFailed(result.error || 'Upload failed'); } } catch (e) { otaFailed('Invalid response from device'); } }; xhr.onerror = function () { // Connection lost likely means device is restarting (success case) // or genuine network error statusEl.textContent = 'Connection lost. Device may be restarting...'; statusEl.style.color = '#ffc107'; btn.textContent = 'Waiting...'; waitForReboot(); }; xhr.send(formData); } function otaFailed(errorMsg) { const progressFill = document.getElementById('ota-progress-fill'); const statusEl = document.getElementById('ota-status'); const btn = document.getElementById('ota-upload-btn'); progressFill.style.background = '#e94560'; statusEl.textContent = 'Error: ' + errorMsg; statusEl.style.color = '#e94560'; btn.textContent = 'Upload Firmware'; btn.disabled = false; } function waitForReboot() { let attempts = 0; const maxAttempts = 30; const checkInterval = setInterval(function () { attempts++; fetch('/api/status', { signal: AbortSignal.timeout(2000) }) .then(r => r.json()) .then(data => { clearInterval(checkInterval); document.getElementById('ota-status').textContent = 'Device is back online! Reloading...'; document.getElementById('ota-status').style.color = '#00d46a'; setTimeout(function () { location.reload(); }, 1000); }) .catch(function () { if (attempts >= maxAttempts) { clearInterval(checkInterval); document.getElementById('ota-status').textContent = 'Device did not respond. Please refresh manually.'; document.getElementById('ota-status').style.color = '#ffc107'; document.getElementById('ota-upload-btn').textContent = 'Upload Firmware'; document.getElementById('ota-upload-btn').disabled = false; } }); }, 2000); } // Initial status update updateStatus(); setInterval(updateStatus, 10000); </script> </body> </html>