366 lines
12 KiB
HTML
366 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>LED Controller - Settings</title>
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<style>
|
||
.settings-container {
|
||
padding: 2rem;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
overflow-y: auto;
|
||
height: 100%;
|
||
}
|
||
|
||
.settings-header {
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.settings-header h1 {
|
||
font-size: 2rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.settings-header p {
|
||
color: #aaa;
|
||
}
|
||
|
||
.settings-section {
|
||
background-color: #1a1a1a;
|
||
border-radius: 8px;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
border: 1px solid #4a4a4a;
|
||
}
|
||
|
||
.settings-section h2 {
|
||
font-size: 1.3rem;
|
||
margin-bottom: 1rem;
|
||
color: #fff;
|
||
border-bottom: 2px solid #4a4a4a;
|
||
padding-bottom: 0.5rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 0.5rem;
|
||
color: #ccc;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.form-group input[type="text"],
|
||
.form-group input[type="password"],
|
||
.form-group input[type="number"],
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
background-color: #2e2e2e;
|
||
border: 1px solid #4a4a4a;
|
||
border-radius: 4px;
|
||
color: white;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: #5a5a5a;
|
||
}
|
||
|
||
.form-group small {
|
||
display: block;
|
||
margin-top: 0.25rem;
|
||
color: #888;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.status-info {
|
||
background-color: #2e2e2e;
|
||
border: 1px solid #4a4a4a;
|
||
border-radius: 4px;
|
||
padding: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.status-info h3 {
|
||
font-size: 1rem;
|
||
margin-bottom: 0.5rem;
|
||
color: #fff;
|
||
}
|
||
|
||
.status-info p {
|
||
color: #aaa;
|
||
margin: 0.25rem 0;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.status-connected {
|
||
color: #4caf50;
|
||
}
|
||
|
||
.status-disconnected {
|
||
color: #f44336;
|
||
}
|
||
|
||
.btn-group {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
margin-top: 1rem;
|
||
}
|
||
|
||
.btn-full {
|
||
flex: 1;
|
||
}
|
||
|
||
.back-link {
|
||
display: inline-block;
|
||
margin-bottom: 1rem;
|
||
color: #aaa;
|
||
text-decoration: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.back-link:hover {
|
||
background-color: #2e2e2e;
|
||
color: white;
|
||
}
|
||
|
||
.message {
|
||
padding: 0.75rem;
|
||
border-radius: 4px;
|
||
margin-bottom: 1rem;
|
||
display: none;
|
||
}
|
||
|
||
.message.success {
|
||
background-color: #1b5e20;
|
||
color: #4caf50;
|
||
border: 1px solid #4caf50;
|
||
}
|
||
|
||
.message.error {
|
||
background-color: #5e1b1b;
|
||
color: #f44336;
|
||
border: 1px solid #f44336;
|
||
}
|
||
|
||
.message.show {
|
||
display: block;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-container">
|
||
<div class="settings-container">
|
||
<a href="/" class="back-link">← Back to Dashboard</a>
|
||
|
||
<div class="settings-header">
|
||
<h1>Device Settings</h1>
|
||
<p>Configure WiFi Access Point and ESP-NOW options</p>
|
||
</div>
|
||
|
||
<div id="message" class="message"></div>
|
||
|
||
<!-- ESP-NOW (LED driver / bridge channel) -->
|
||
<div class="settings-section">
|
||
<h2>ESP-NOW</h2>
|
||
<form id="espnow-form">
|
||
<div class="form-group">
|
||
<label for="wifi-channel-page-input">WiFi channel (ESP-NOW)</label>
|
||
<input type="number" id="wifi-channel-page-input" name="wifi_channel" min="1" max="11" required>
|
||
<small>STA channel (1–11) for LED drivers and the serial bridge. Use the same value on every device.</small>
|
||
</div>
|
||
<div class="btn-group">
|
||
<button type="submit" class="btn btn-primary btn-full">Save channel</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- WiFi Access Point Settings -->
|
||
<div class="settings-section">
|
||
<h2>WiFi Access Point Settings</h2>
|
||
|
||
<div id="ap-status" class="status-info">
|
||
<h3>AP Status</h3>
|
||
<p>Loading...</p>
|
||
</div>
|
||
|
||
<form id="ap-form">
|
||
<div class="form-group">
|
||
<label for="ap-ssid">AP SSID (Network Name)</label>
|
||
<input type="text" id="ap-ssid" name="ssid" placeholder="Enter AP name" required>
|
||
<small>The name of the WiFi access point this device creates</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="ap-password">AP Password</label>
|
||
<input type="password" id="ap-password" name="password" placeholder="Enter AP password (min 8 chars)" data-bwignore>
|
||
<small>Leave empty for open network (min 8 characters if set)</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="ap-channel">Channel (1-11)</label>
|
||
<input type="number" id="ap-channel" name="channel" min="1" max="11" placeholder="Auto">
|
||
<small>WiFi channel (1-11 for 2.4GHz). Leave empty for auto.</small>
|
||
</div>
|
||
|
||
<div class="btn-group">
|
||
<button type="submit" class="btn btn-primary btn-full">Configure AP</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Show message helper
|
||
function showMessage(text, type = 'success') {
|
||
const messageEl = document.getElementById('message');
|
||
messageEl.textContent = text;
|
||
messageEl.className = `message ${type} show`;
|
||
setTimeout(() => {
|
||
messageEl.classList.remove('show');
|
||
}, 5000);
|
||
}
|
||
|
||
async function loadEspnowChannel() {
|
||
try {
|
||
const response = await fetch('/settings');
|
||
const data = await response.json();
|
||
const chInput = document.getElementById('wifi-channel-page-input');
|
||
if (chInput && data && typeof data === 'object') {
|
||
const ch = data.wifi_channel;
|
||
chInput.value =
|
||
ch !== undefined && ch !== null && ch !== '' ? String(ch) : '6';
|
||
}
|
||
} catch (error) {
|
||
console.error('Error loading ESP-NOW channel:', error);
|
||
}
|
||
}
|
||
|
||
document.getElementById('espnow-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const chRaw = document.getElementById('wifi-channel-page-input').value;
|
||
const wifiChannel = parseInt(chRaw, 10);
|
||
if (Number.isNaN(wifiChannel) || wifiChannel < 1 || wifiChannel > 11) {
|
||
showMessage('WiFi channel must be between 1 and 11', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
const response = await fetch('/settings', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ wifi_channel: wifiChannel }),
|
||
});
|
||
const result = await response.json();
|
||
if (response.ok) {
|
||
showMessage('ESP-NOW channel saved.', 'success');
|
||
} else {
|
||
showMessage(`Error: ${result.error || 'Failed to save'}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
showMessage(`Error: ${error.message}`, 'error');
|
||
}
|
||
});
|
||
|
||
// Load AP status and config
|
||
async function loadAPStatus() {
|
||
try {
|
||
const response = await fetch('/settings/wifi/ap');
|
||
const config = await response.json();
|
||
|
||
const statusEl = document.getElementById('ap-status');
|
||
if (config.active) {
|
||
statusEl.innerHTML = `
|
||
<h3>AP Status: <span class="status-connected">Active</span></h3>
|
||
<p><strong>SSID:</strong> ${config.ssid || 'N/A'}</p>
|
||
<p><strong>Channel:</strong> ${config.channel || 'Auto'}</p>
|
||
<p><strong>IP Address:</strong> ${config.ip || 'N/A'}</p>
|
||
`;
|
||
} else {
|
||
statusEl.innerHTML = `
|
||
<h3>AP Status: <span class="status-disconnected">Inactive</span></h3>
|
||
<p>Access Point is not currently active</p>
|
||
`;
|
||
}
|
||
|
||
// Load saved values
|
||
if (config.saved_ssid) document.getElementById('ap-ssid').value = config.saved_ssid;
|
||
if (config.saved_channel) document.getElementById('ap-channel').value = config.saved_channel;
|
||
} catch (error) {
|
||
console.error('Error loading AP status:', error);
|
||
}
|
||
}
|
||
|
||
// AP form submission
|
||
document.getElementById('ap-form').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
|
||
const formData = {
|
||
ssid: document.getElementById('ap-ssid').value,
|
||
password: document.getElementById('ap-password').value,
|
||
channel: document.getElementById('ap-channel').value || null
|
||
};
|
||
|
||
// Validate password length if provided
|
||
if (formData.password && formData.password.length > 0 && formData.password.length < 8) {
|
||
showMessage('AP password must be at least 8 characters', 'error');
|
||
return;
|
||
}
|
||
|
||
// Convert channel to number if provided
|
||
if (formData.channel) {
|
||
formData.channel = parseInt(formData.channel);
|
||
if (formData.channel < 1 || formData.channel > 11) {
|
||
showMessage('Channel must be between 1 and 11', 'error');
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('/settings/wifi/ap', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(formData)
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (response.ok) {
|
||
showMessage('Access Point configured successfully!', 'success');
|
||
setTimeout(loadAPStatus, 1000);
|
||
} else {
|
||
showMessage(`Error: ${result.error || 'Failed to configure AP'}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
showMessage(`Error: ${error.message}`, 'error');
|
||
}
|
||
});
|
||
|
||
// Load all data on page load
|
||
loadEspnowChannel();
|
||
loadAPStatus();
|
||
|
||
// Refresh status every 10 seconds
|
||
setInterval(loadAPStatus, 10000);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
|