Files
esp32-bmc/main/html/index.html
Valentin Haudiquet e269bf62f3
All checks were successful
Build ESP32 BMC Firmware / build (push) Successful in 55s
fix: tryfix power status
2026-03-18 10:12:37 +01:00

1565 lines
51 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VisionFive2 BMC Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: #eee;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
backdrop-filter: blur(10px);
}
h1 {
font-size: 2.5em;
margin-bottom: 10px;
background: linear-gradient(90deg, #00d4ff, #00ff88);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.status-bar {
display: flex;
justify-content: center;
gap: 30px;
flex-wrap: wrap;
}
.status-item {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #ff4444;
animation: pulse 2s infinite;
}
.status-dot.connected {
background: #00ff88;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.card {
background: rgba(255, 255, 255, 0.05);
border-radius: 15px;
padding: 20px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.card h2 {
font-size: 1.3em;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
gap: 10px;
}
.card h2 .icon {
font-size: 1.5em;
}
/* Power Control */
.power-buttons {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1em;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.btn:active {
transform: translateY(0);
}
.btn-power-on {
background: linear-gradient(135deg, #00c853, #00e676);
color: #000;
}
.btn-power-off {
background: linear-gradient(135deg, #ff1744, #ff5252);
color: #fff;
}
.btn-reset {
background: linear-gradient(135deg, #ff9100, #ffab40);
color: #000;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.power-status {
margin-top: 15px;
padding: 15px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
display: flex;
align-items: center;
gap: 10px;
}
.power-indicator {
width: 20px;
height: 20px;
border-radius: 50%;
background: #ff4444;
}
.power-indicator.on {
background: #00ff88;
box-shadow: 0 0 20px rgba(0, 255, 136, 0.5);
}
/* GPIO Monitor */
.gpio-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.gpio-item {
background: rgba(0, 0, 0, 0.2);
padding: 10px;
border-radius: 8px;
text-align: center;
}
.gpio-name {
font-size: 0.8em;
color: #888;
margin-bottom: 5px;
}
.gpio-pin {
font-size: 0.9em;
color: #00d4ff;
margin-bottom: 5px;
}
.gpio-value {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.gpio-led {
width: 16px;
height: 16px;
border-radius: 50%;
background: #333;
border: 2px solid #555;
}
.gpio-led.high {
background: #00ff88;
border-color: #00ff88;
box-shadow: 0 0 10px rgba(0, 255, 136, 0.5);
}
.gpio-toggle {
padding: 4px 8px;
font-size: 0.8em;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.gpio-toggle:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Serial Console */
.console-container {
display: flex;
flex-direction: column;
height: 400px;
}
.console-output {
flex: 1;
background: #0a0a0a;
border-radius: 8px;
padding: 15px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
overflow-y: auto;
margin-bottom: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
cursor: text;
}
.console-output:focus {
outline: none;
border-color: #00d4ff;
}
.console-output pre {
white-space: pre-wrap;
word-wrap: break-word;
}
.console-output .input-line {
color: #00ff88;
}
.console-output .cursor {
display: inline-block;
width: 8px;
height: 1em;
background: #00ff88;
animation: blink 1s step-end infinite;
vertical-align: middle;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.console-controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.console-controls select {
padding: 8px 12px;
border-radius: 6px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
}
/* System Info */
.info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.info-item {
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 8px;
}
.info-label {
font-size: 0.8em;
color: #888;
margin-bottom: 3px;
}
.info-value {
font-size: 1em;
color: #00d4ff;
font-family: 'Courier New', monospace;
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.toast {
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 10px;
animation: slideIn 0.3s ease;
max-width: 300px;
}
.toast.success {
background: rgba(0, 255, 136, 0.2);
border: 1px solid #00ff88;
}
.toast.error {
background: rgba(255, 68, 68, 0.2);
border: 1px solid #ff4444;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* OTA Update Styles */
.ota-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.ota-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.ota-form label {
font-weight: 600;
color: #555;
}
.ota-form input {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.ota-form input:focus {
outline: none;
border-color: #2196F3;
}
.ota-progress {
display: flex;
align-items: center;
gap: 10px;
}
.progress-bar {
flex: 1;
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
border-radius: 10px;
transition: width 0.3s ease;
width: 0%;
}
#ota-progress-text {
font-weight: 600;
min-width: 45px;
}
.ota-info {
margin-bottom: 10px;
}
/* OTA Upload Section */
.ota-upload-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.drop-zone {
border: 2px dashed #ccc;
border-radius: 8px;
padding: 30px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
background: #fafafa;
}
.drop-zone:hover, .drop-zone.drag-over {
border-color: #2196F3;
background: #e3f2fd;
}
.drop-zone.drag-over {
transform: scale(1.02);
}
.drop-zone-icon {
font-size: 48px;
display: block;
margin-bottom: 10px;
}
.drop-zone p {
margin: 0;
font-weight: 600;
color: #333;
}
.drop-zone-hint {
font-size: 12px;
color: #666;
margin-top: 5px;
display: block;
}
.file-info {
display: flex;
justify-content: space-between;
padding: 10px;
background: #e8f5e9;
border-radius: 4px;
font-size: 14px;
}
.file-info #file-name {
font-weight: 600;
color: #2e7d32;
}
.file-info #file-size {
color: #666;
}
.ota-divider {
display: flex;
align-items: center;
text-align: center;
margin: 15px 0;
}
.ota-divider::before,
.ota-divider::after {
content: '';
flex: 1;
border-bottom: 1px solid #ddd;
}
.ota-divider span {
padding: 0 15px;
color: #999;
font-size: 12px;
font-weight: 600;
}
/* Responsive */
@media (max-width: 600px) {
h1 {
font-size: 1.8em;
}
.status-bar {
flex-direction: column;
align-items: center;
}
.power-buttons {
flex-direction: column;
}
.btn {
width: 100%;
}
.info-grid {
grid-template-columns: 1fr;
}
}
/* Login Modal Styles */
.login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.login-overlay.hidden {
display: none;
}
.login-modal {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 20px;
padding: 40px;
width: 100%;
max-width: 400px;
text-align: center;
}
.login-modal h2 {
margin-bottom: 10px;
font-size: 1.8em;
}
.login-modal p {
color: #aaa;
margin-bottom: 30px;
}
.login-field {
margin-bottom: 20px;
text-align: left;
}
.login-field label {
display: block;
margin-bottom: 8px;
color: #ccc;
font-size: 0.9em;
}
.login-field input {
width: 100%;
padding: 12px 16px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
color: #fff;
font-size: 1em;
outline: none;
transition: border-color 0.3s;
}
.login-field input:focus {
border-color: #00d4ff;
}
.login-btn {
width: 100%;
margin-top: 10px;
}
.login-error {
color: #ff4444;
margin-top: 15px;
font-size: 0.9em;
min-height: 20px;
}
</style>
</head>
<body>
<!-- Login Modal -->
<div class="login-overlay" id="login-overlay">
<div class="login-modal">
<h2>🔐 BMC Login</h2>
<p>Enter your credentials to access the BMC dashboard</p>
<form id="login-form" onsubmit="return handleLogin(event)">
<div class="login-field">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autocomplete="username">
</div>
<div class="login-field">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<button type="submit" class="btn btn-primary login-btn">Login</button>
<div class="login-error" id="login-error"></div>
</form>
</div>
</div>
<div class="container" id="main-container" style="display: none;">
<header>
<h1>🖥️ VisionFive2 BMC Dashboard</h1>
<div class="status-bar">
<div class="status-item">
<div class="status-dot" id="eth-status"></div>
<span>Ethernet: <span id="eth-text">Disconnected</span></span>
</div>
<div class="status-item">
<span>IP: <span id="ip-address">-</span></span>
</div>
<div class="status-item">
<span>UART: <span id="uart-baud">115200</span> baud</span>
</div>
</div>
</header>
<div class="grid">
<!-- Power Control Card -->
<div class="card">
<h2><span class="icon"></span> Power Control</h2>
<div class="power-buttons">
<button class="btn btn-power-on" onclick="powerOn()">Power On</button>
<button class="btn btn-power-off" onclick="powerOff()">Power Off</button>
<button class="btn btn-reset" onclick="reset()">Reset</button>
</div>
<div class="power-status">
<div class="power-indicator" id="power-indicator"></div>
<span>Power Status: <strong id="power-status">Unknown</strong></span>
</div>
</div>
<!-- System Info Card -->
<div class="card">
<h2><span class="icon"></span> System Info</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">MAC Address</div>
<div class="info-value" id="mac-address">-</div>
</div>
<div class="info-item">
<div class="info-label">Netmask</div>
<div class="info-value" id="netmask">-</div>
</div>
<div class="info-item">
<div class="info-label">Gateway</div>
<div class="info-value" id="gateway">-</div>
</div>
<div class="info-item">
<div class="info-label">Link Speed</div>
<div class="info-value" id="link-speed">-</div>
</div>
</div>
</div>
</div>
<div class="grid">
<!-- OTA Update Card -->
<div class="card">
<h2><span class="icon">🔄</span> OTA Update</h2>
<div class="ota-container">
<div class="ota-info">
<div class="info-item">
<div class="info-label">Running Partition</div>
<div class="info-value" id="ota-partition">-</div>
</div>
</div>
<!-- File Upload Section -->
<div class="ota-upload-section">
<div class="drop-zone" id="drop-zone">
<div class="drop-zone-content">
<span class="drop-zone-icon">📁</span>
<p>Drag & drop firmware file here</p>
<span class="drop-zone-hint">or click to browse</span>
</div>
<input type="file" id="ota-file-input" accept=".bin" style="display: none;" />
</div>
<div class="file-info" id="file-info" style="display: none;">
<span id="file-name">-</span>
<span id="file-size">-</span>
</div>
<button class="btn btn-primary" id="ota-upload-btn" onclick="uploadFirmware()" disabled>Upload Firmware</button>
</div>
<div class="ota-divider">
<span>OR</span>
</div>
<div class="ota-form">
<label for="ota-url">Firmware URL:</label>
<input type="text" id="ota-url" placeholder="http://server/firmware.bin" />
<button class="btn btn-secondary" id="ota-btn" onclick="startOTAUpdate()">Download from URL</button>
</div>
<div class="ota-progress" id="ota-progress-container" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="ota-progress-bar"></div>
</div>
<span id="ota-progress-text">0%</span>
</div>
</div>
</div>
<!-- System Info Card (Additional) -->
<div class="card">
<h2><span class="icon">📊</span> System Status</h2>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Free Heap</div>
<div class="info-value" id="free-heap">-</div>
</div>
<div class="info-item">
<div class="info-label">Uptime</div>
<div class="info-value" id="uptime">-</div>
</div>
<div class="info-item">
<div class="info-label">WiFi Mode</div>
<div class="info-value" id="wifi-mode">Ethernet</div>
</div>
<div class="info-item">
<div class="info-label">IP Address</div>
<div class="info-value" id="ip-address">-</div>
</div>
</div>
</div>
</div>
<div class="grid">
<!-- GPIO Monitor Card -->
<div class="card">
<h2><span class="icon">🔌</span> GPIO Monitor</h2>
<div class="gpio-grid" id="gpio-grid">
<!-- GPIO items will be populated by JavaScript -->
</div>
</div>
<!-- Serial Console Card -->
<div class="card">
<h2><span class="icon">💻</span> Serial Console</h2>
<div class="console-container">
<div class="console-output" id="console-output" tabindex="0">
<pre>Connecting to serial console...</pre>
</div>
<div class="console-controls">
<label>Baud Rate:</label>
<select id="baud-select" onchange="setBaudRate()">
<option value="9600">9600</option>
<option value="19200">19200</option>
<option value="38400">38400</option>
<option value="57600">57600</option>
<option value="115200" selected>115200</option>
<option value="230400">230400</option>
<option value="460800">460800</option>
<option value="921600">921600</option>
</select>
<button class="btn btn-secondary" onclick="clearConsole()">Clear</button>
<button class="btn btn-secondary" onclick="connectWebSocket()">Reconnect</button>
</div>
</div>
</div>
</div>
<div class="grid">
<!-- System Logs Card -->
<div class="card">
<h2><span class="icon">📋</span> System Logs</h2>
<div class="console-container">
<div class="console-output" id="logs-output">
<pre>Loading logs...</pre>
</div>
<div class="console-controls">
<button class="btn btn-secondary" onclick="refreshLogs()">Refresh</button>
<button class="btn btn-secondary" onclick="clearLogs()">Clear Logs</button>
<label><input type="checkbox" id="auto-refresh-logs" onchange="toggleAutoRefreshLogs()" /> Auto-refresh</label>
</div>
</div>
</div>
</div>
</div>
<div class="toast-container" id="toast-container"></div>
<script>
// WebSocket connection
let ws = null;
let wsConnected = false;
// API base URL
const API_BASE = '';
// Authentication
let authCredentials = null;
// Get stored credentials from sessionStorage
function getStoredCredentials() {
const stored = sessionStorage.getItem('bmc_auth');
if (stored) {
return JSON.parse(stored);
}
return null;
}
// Store credentials in sessionStorage
function storeCredentials(username, password) {
sessionStorage.setItem('bmc_auth', JSON.stringify({ username, password }));
}
// Clear stored credentials
function clearCredentials() {
sessionStorage.removeItem('bmc_auth');
authCredentials = null;
}
// Create Basic Auth header value
function getAuthHeader() {
if (!authCredentials) {
const stored = getStoredCredentials();
if (stored) {
authCredentials = stored;
} else {
return null;
}
}
return 'Basic ' + btoa(authCredentials.username + ':' + authCredentials.password);
}
// Handle login form submission
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
const errorDiv = document.getElementById('login-error');
// Store credentials temporarily
authCredentials = { username, password };
// Test credentials by making an API call
try {
const authHeader = 'Basic ' + btoa(username + ':' + password);
const response = await fetch(`${API_BASE}/api/system/info`, {
headers: {
'Authorization': authHeader
}
});
if (response.status === 401) {
errorDiv.textContent = 'Invalid username or password';
authCredentials = null;
return false;
}
if (!response.ok) {
errorDiv.textContent = 'Connection error. Please try again.';
authCredentials = null;
return false;
}
// Success - store credentials and show main content
storeCredentials(username, password);
document.getElementById('login-overlay').classList.add('hidden');
document.getElementById('main-container').style.display = 'block';
// Initialize the dashboard
initialize();
} catch (error) {
errorDiv.textContent = 'Connection error. Please try again.';
authCredentials = null;
return false;
}
return false;
}
// Check if already logged in
function checkAuth() {
const stored = getStoredCredentials();
if (stored) {
authCredentials = stored;
// Verify credentials are still valid
const authHeader = getAuthHeader();
fetch(`${API_BASE}/api/system/info`, {
headers: { 'Authorization': authHeader }
})
.then(response => {
if (response.status === 401) {
clearCredentials();
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-container').style.display = 'none';
} else {
document.getElementById('login-overlay').classList.add('hidden');
document.getElementById('main-container').style.display = 'block';
initialize();
}
})
.catch(() => {
// On error, still show login
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-container').style.display = 'none';
});
} else {
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-container').style.display = 'none';
}
}
// Toast notification
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// API helper with authentication
async function apiCall(endpoint, method = 'GET', data = null) {
const authHeader = getAuthHeader();
const options = {
method: method,
headers: {
'Content-Type': 'application/json'
}
};
if (authHeader) {
options.headers['Authorization'] = authHeader;
}
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(`${API_BASE}${endpoint}`, options);
// Handle 401 - session expired
if (response.status === 401) {
clearCredentials();
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-container').style.display = 'none';
throw new Error('Session expired. Please login again.');
}
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Unknown error');
}
return result;
} catch (error) {
showToast(error.message, 'error');
throw error;
}
}
// Power control functions
async function powerOn() {
try {
await apiCall('/api/power/on', 'POST');
showToast('Power on sequence initiated');
updatePowerStatus();
} catch (error) {
console.error('Power on failed:', error);
}
}
async function powerOff() {
try {
await apiCall('/api/power/off', 'POST');
showToast('Power off sequence initiated');
updatePowerStatus();
} catch (error) {
console.error('Power off failed:', error);
}
}
async function reset() {
try {
await apiCall('/api/power/reset', 'POST');
showToast('Reset sequence initiated');
} catch (error) {
console.error('Reset failed:', error);
}
}
async function updatePowerStatus() {
try {
const result = await apiCall('/api/power/status');
const indicator = document.getElementById('power-indicator');
const status = document.getElementById('power-status');
if (result.data.power_status === 'on') {
indicator.classList.add('on');
status.textContent = 'ON';
} else {
indicator.classList.remove('on');
status.textContent = 'OFF';
}
} catch (error) {
console.error('Failed to get power status:', error);
}
}
// GPIO functions
async function updateGPIOStates() {
try {
const result = await apiCall('/api/gpio');
const grid = document.getElementById('gpio-grid');
grid.innerHTML = '';
result.gpios.forEach(gpio => {
const item = document.createElement('div');
item.className = 'gpio-item';
const isOutput = gpio.mode === 'output';
item.innerHTML = `
<div class="gpio-name">${gpio.name}</div>
<div class="gpio-pin">GPIO ${gpio.pin}</div>
<div class="gpio-value">
<div class="gpio-led ${gpio.value ? 'high' : ''}"></div>
<span>${gpio.value}</span>
${isOutput ? `<button class="gpio-toggle" onclick="toggleGPIO(${gpio.pin})">Toggle</button>` : ''}
</div>
`;
grid.appendChild(item);
});
} catch (error) {
console.error('Failed to get GPIO states:', error);
}
}
async function toggleGPIO(pin) {
try {
// Get current state
const result = await apiCall('/api/gpio');
const gpio = result.gpios.find(g => g.pin === pin);
if (gpio) {
const newValue = gpio.value ? 0 : 1;
await apiCall(`/api/gpio/set?pin=${pin}`, 'POST', { value: newValue });
updateGPIOStates();
}
} catch (error) {
console.error('Failed to toggle GPIO:', error);
}
}
// Serial console functions
let inputBuffer = '';
let cursorVisible = true;
function connectWebSocket() {
const protocol = window.location.protocol === 'https' ? 'wss' : 'ws';
const wsUrl = `${protocol}://${window.location.host}/api/serial/ws`;
ws = new WebSocket(wsUrl);
ws.onopen = () => {
wsConnected = true;
clearConsole();
appendConsole('Connected to serial console\n');
updateCursor();
};
ws.onmessage = (event) => {
appendConsole(event.data);
};
ws.onclose = () => {
wsConnected = false;
appendConsole('\nDisconnected from serial console\n');
// Try to reconnect after 3 seconds
setTimeout(connectWebSocket, 3000);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
appendConsole('WebSocket error\n');
};
}
// Strip ANSI escape sequences from text
function stripAnsi(text) {
// Remove ANSI escape sequences (cursor movement, colors, etc.)
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
.replace(/\x1b\][^\x07]*\x07/g, '')
.replace(/\x1b[()][AB012]/g, '')
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
}
function appendConsole(data) {
const output = document.getElementById('console-output');
const pre = output.querySelector('pre') || document.createElement('pre');
if (!output.contains(pre)) {
output.innerHTML = '';
output.appendChild(pre);
}
// Remove cursor before appending, then re-add
const cursor = pre.querySelector('.cursor');
if (cursor) cursor.remove();
// Strip ANSI escape sequences for cleaner display
const cleanData = stripAnsi(data);
pre.textContent += cleanData;
updateCursor();
output.scrollTop = output.scrollHeight;
}
function updateCursor() {
const output = document.getElementById('console-output');
const pre = output.querySelector('pre');
if (!pre) return;
// Remove existing cursor
const existingCursor = pre.querySelector('.cursor');
if (existingCursor) existingCursor.remove();
// Add cursor at end
if (wsConnected) {
const cursor = document.createElement('span');
cursor.className = 'cursor';
pre.appendChild(cursor);
}
}
function sendChar(char) {
if (wsConnected) {
ws.send(char);
}
}
function sendBuffer() {
if (inputBuffer && wsConnected) {
ws.send(inputBuffer + '\n');
inputBuffer = '';
updateCursor();
}
}
function handleConsoleKeyDown(event) {
if (!wsConnected) return;
const output = document.getElementById('console-output');
// Handle special keys
if (event.key === 'Enter') {
event.preventDefault();
sendChar('\n');
inputBuffer = '';
} else if (event.key === 'Backspace') {
event.preventDefault();
if (inputBuffer.length > 0) {
inputBuffer = inputBuffer.slice(0, -1);
sendChar('\b'); // Send backspace to remote
}
} else if (event.key === 'Tab') {
event.preventDefault();
sendChar('\t');
inputBuffer += '\t';
} else if (event.key === 'Escape') {
event.preventDefault();
sendChar('\x1b');
} else if (event.ctrlKey && event.key.length === 1) {
// Handle Ctrl+C, Ctrl+D, etc.
event.preventDefault();
const ctrlChar = String.fromCharCode(event.key.charCodeAt(0) - 96);
sendChar(ctrlChar);
} else if (event.key.length === 1 && !event.ctrlKey && !event.altKey && !event.metaKey) {
// Regular character
event.preventDefault();
sendChar(event.key);
inputBuffer += event.key;
}
}
function clearConsole() {
const output = document.getElementById('console-output');
output.innerHTML = '<pre></pre>';
inputBuffer = '';
updateCursor();
}
// Initialize console keyboard handling
document.addEventListener('DOMContentLoaded', () => {
const output = document.getElementById('console-output');
output.addEventListener('keydown', handleConsoleKeyDown);
// Focus console on click
output.addEventListener('click', () => {
output.focus();
});
});
async function setBaudRate() {
const baud = document.getElementById('baud-select').value;
try {
await apiCall('/api/serial/config', 'PUT', { baud_rate: parseInt(baud) });
document.getElementById('uart-baud').textContent = baud;
showToast(`Baud rate set to ${baud}`);
} catch (error) {
console.error('Failed to set baud rate:', error);
}
}
// System info functions
async function updateSystemInfo() {
try {
const result = await apiCall('/api/system/info');
document.getElementById('ip-address').textContent = result.data.ip || '-';
document.getElementById('mac-address').textContent = result.data.mac || '-';
document.getElementById('netmask').textContent = result.data.netmask || '-';
document.getElementById('gateway').textContent = result.data.gateway || '-';
document.getElementById('link-speed').textContent = result.data.link_speed ? `${result.data.link_speed} Mbps` : '-';
// Update Ethernet status
const ethStatus = document.getElementById('eth-status');
const ethText = document.getElementById('eth-text');
if (result.data.ethernet_connected) {
ethStatus.classList.add('connected');
ethText.textContent = 'Connected';
} else {
ethStatus.classList.remove('connected');
ethText.textContent = 'Disconnected';
}
document.getElementById('uart-baud').textContent = result.data.uart_baud_rate;
} catch (error) {
console.error('Failed to get system info:', error);
}
}
// OTA Update Functions
let otaPollInterval = null;
async function updateOTAStatus() {
try {
const response = await fetch(`${API_BASE}/api/ota/status`);
const result = await response.json();
if (result.success && result.data) {
document.getElementById('ota-partition').textContent = result.data.partition || '-';
if (result.data.updating) {
document.getElementById('ota-progress-container').style.display = 'flex';
const progress = result.data.progress || 0;
document.getElementById('ota-progress-bar').style.width = `${progress}%`;
document.getElementById('ota-progress-text').textContent = `${progress}%`;
document.getElementById('ota-btn').disabled = true;
document.getElementById('ota-btn').textContent = 'Updating...';
} else {
document.getElementById('ota-progress-container').style.display = 'none';
document.getElementById('ota-btn').disabled = false;
document.getElementById('ota-btn').textContent = 'Start Update';
if (otaPollInterval) {
clearInterval(otaPollInterval);
otaPollInterval = null;
}
}
}
} catch (error) {
console.error('Failed to get OTA status:', error);
}
}
async function startOTAUpdate() {
const url = document.getElementById('ota-url').value.trim();
if (!url) {
showToast('Please enter a firmware URL', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/api/ota/update`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ url: url })
});
const result = await response.json();
if (result.success) {
showToast('OTA update started', 'success');
document.getElementById('ota-btn').disabled = true;
document.getElementById('ota-btn').textContent = 'Updating...';
document.getElementById('ota-progress-container').style.display = 'flex';
// Start polling for progress
otaPollInterval = setInterval(updateOTAStatus, 1000);
} else {
showToast(result.error || 'Failed to start OTA update', 'error');
}
} catch (error) {
console.error('OTA update error:', error);
showToast('Failed to start OTA update', 'error');
}
}
// File Upload Functions
let selectedFile = null;
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
function handleFileSelect(file) {
if (!file) return;
if (!file.name.endsWith('.bin')) {
showToast('Please select a .bin firmware file', 'error');
return;
}
selectedFile = file;
document.getElementById('file-info').style.display = 'flex';
document.getElementById('file-name').textContent = file.name;
document.getElementById('file-size').textContent = formatFileSize(file.size);
document.getElementById('ota-upload-btn').disabled = false;
}
async function uploadFirmware() {
if (!selectedFile) {
showToast('Please select a firmware file first', 'error');
return;
}
const uploadBtn = document.getElementById('ota-upload-btn');
uploadBtn.disabled = true;
uploadBtn.textContent = 'Uploading...';
document.getElementById('ota-progress-container').style.display = 'flex';
document.getElementById('ota-progress-bar').style.width = '0%';
document.getElementById('ota-progress-text').textContent = '0%';
try {
const formData = new FormData();
formData.append('firmware', selectedFile);
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
document.getElementById('ota-progress-bar').style.width = percent + '%';
document.getElementById('ota-progress-text').textContent = percent + '%';
}
};
xhr.onload = () => {
if (xhr.status === 200) {
showToast('Firmware uploaded successfully! Device will restart...', 'success');
document.getElementById('ota-progress-bar').style.width = '100%';
document.getElementById('ota-progress-text').textContent = '100%';
} else if (xhr.status === 401) {
showToast('Session expired. Please login again.', 'error');
clearCredentials();
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-container').style.display = 'none';
} else {
let errorMsg = 'Upload failed';
try {
const result = JSON.parse(xhr.responseText);
errorMsg = result.error || errorMsg;
} catch (e) {}
showToast(errorMsg, 'error');
uploadBtn.disabled = false;
uploadBtn.textContent = 'Upload Firmware';
}
};
xhr.onerror = () => {
showToast('Upload failed - network error', 'error');
uploadBtn.disabled = false;
uploadBtn.textContent = 'Upload Firmware';
};
xhr.open('POST', `${API_BASE}/api/ota/upload`);
// Add authentication header
const authHeader = getAuthHeader();
if (authHeader) {
xhr.setRequestHeader('Authorization', authHeader);
}
xhr.send(formData);
} catch (error) {
console.error('Upload error:', error);
showToast('Failed to upload firmware', 'error');
uploadBtn.disabled = false;
uploadBtn.textContent = 'Upload Firmware';
}
}
// Initialize function called after successful login
function initialize() {
updateSystemInfo();
updateGPIOStates();
updatePowerStatus();
updateOTAStatus();
connectWebSocket();
// Setup drag and drop
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('ota-file-input');
dropZone.addEventListener('click', () => fileInput.click());
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFileSelect(files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFileSelect(e.target.files[0]);
}
});
// Initialize logs
refreshLogs();
// Periodic updates
setInterval(updateSystemInfo, 5000);
setInterval(updateGPIOStates, 2000);
setInterval(updatePowerStatus, 2000);
setInterval(updateOTAStatus, 5000);
}
// Check authentication on page load
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
});
// Log viewer functions
let logsAutoRefreshInterval = null;
async function refreshLogs() {
try {
const authHeader = getAuthHeader();
const response = await fetch(`${API_BASE}/api/logs`, {
headers: authHeader ? { 'Authorization': authHeader } : {}
});
if (response.status === 401) {
clearCredentials();
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-container').style.display = 'none';
return;
}
const logs = await response.text();
const logsOutput = document.getElementById('logs-output');
logsOutput.innerHTML = `<pre>${escapeHtml(logs)}</pre>`;
// Scroll to bottom
logsOutput.scrollTop = logsOutput.scrollHeight;
} catch (error) {
console.error('Failed to fetch logs:', error);
document.getElementById('logs-output').innerHTML = '<pre>Failed to load logs</pre>';
}
}
async function clearLogs() {
try {
const authHeader = getAuthHeader();
const response = await fetch(`${API_BASE}/api/logs/clear`, {
method: 'POST',
headers: authHeader ? { 'Authorization': authHeader } : {}
});
if (response.status === 401) {
clearCredentials();
document.getElementById('login-overlay').classList.remove('hidden');
document.getElementById('main-container').style.display = 'none';
return;
}
refreshLogs();
showToast('Logs cleared', 'success');
} catch (error) {
console.error('Failed to clear logs:', error);
showToast('Failed to clear logs', 'error');
}
}
function toggleAutoRefreshLogs() {
const checkbox = document.getElementById('auto-refresh-logs');
if (checkbox.checked) {
logsAutoRefreshInterval = setInterval(refreshLogs, 2000);
} else {
if (logsAutoRefreshInterval) {
clearInterval(logsAutoRefreshInterval);
logsAutoRefreshInterval = null;
}
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>