dimming and fix for manual add
Build Images and Deploy / Update-PROD-Stack (push) Successful in 19s

This commit is contained in:
2026-03-30 22:16:17 -04:00
parent 70c98af759
commit da2693ae68
5 changed files with 342 additions and 75 deletions
+194 -65
View File
@@ -134,6 +134,62 @@
.icon-btn:hover { border-color: var(--accent); color: var(--accent); }
.icon-btn.del:hover { border-color: var(--danger); color: var(--danger); }
/* ── Brightness Slider ── */
.brightness-control {
background: var(--bg);
border-radius: 8px;
padding: 8px;
border: 1px solid var(--border);
}
.brightness-slider {
flex: 1;
height: 6px;
border-radius: 3px;
background: linear-gradient(to right, var(--off) 0%, var(--on) 100%);
outline: none;
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
.brightness-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
border: 2px solid var(--card);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
transition: all .15s;
}
.brightness-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
}
.brightness-slider::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
border: 2px solid var(--card);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
transition: all .15s;
}
.brightness-slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0,0,0,0.3);
}
.brightness-slider:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.brightness-value {
font-weight: 600;
color: var(--accent);
}
/* ── Modal ── */
.modal-backdrop { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.55);
z-index: 200; align-items: flex-end; justify-content: center; }
@@ -196,8 +252,9 @@
<div id="page-devices" class="page active">
<div class="toolbar">
<h2>Devices</h2>
<div style="flex:1"></div>
<button class="btn btn-ghost btn-sm" onclick="openAddDeviceModal()"> Add IP</button>
<button class="btn btn-primary" id="btn-discover" onclick="discoverDevices()">⟳ Scan</button>
<button class="btn btn-secondary btn-sm" id="btn-manual-add" onclick="openManualModal()" title="Add device manually" style="width:auto;padding:9px 12px;"></button>
</div>
<div id="page-header-devices" class="page-header">
<span class="ws-dot" id="ws-dot-d"></span>
@@ -258,7 +315,10 @@
<table style="font-size:12px;color:var(--text2);width:100%;border-collapse:collapse;line-height:1.8;">
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/devices</td><td>List devices</td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/discover</td><td>Scan network</td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/add</td><td>Add device manually <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"host":"192.168.1.100","port":49153}</code></td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/:ip/:port/state</td><td>Toggle power <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"on":true}</code></td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/devices/:ip/:port/brightness</td><td>Get brightness (dimmer only)</td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/devices/:ip/:port/brightness</td><td>Set brightness <code style="background:var(--bg);padding:1px 4px;border-radius:3px;">{"brightness":50}</code></td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">GET /api/dwm-rules</td><td>List DWM rules</td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">POST /api/dwm-rules</td><td>Create rule</td></tr>
<tr><td style="padding-right:8px;white-space:nowrap;color:var(--accent);font-family:monospace;">PUT /api/dwm-rules/:id</td><td>Update rule</td></tr>
@@ -437,25 +497,30 @@
</div>
</div>
<!-- ── Manual Add Modal ── -->
<div class="modal-backdrop" id="modal-manual" onclick="closeManualModal(event)">
<!-- ── Add Device Modal ── -->
<div class="modal-backdrop" id="modal-add-device" onclick="closeAddDeviceModal(event)">
<div class="modal" onclick="event.stopPropagation()">
<div class="modal-header">
<span class="modal-title">Add Device Manually</span>
<button class="modal-close" onclick="closeManualModal()"></button>
<span class="modal-title">Add Wemo Device Manually</span>
<button class="modal-close" onclick="closeAddDeviceModal()">×</button>
</div>
<div class="form-group">
<label class="form-label">IP Address</label>
<input class="form-input" id="f-manual-host" type="text" placeholder="192.168.1.100" autofocus />
<input class="form-input" id="add-device-host" type="text" placeholder="192.168.1.100" />
</div>
<div class="form-group">
<label class="form-label">Port</label>
<input class="form-input" id="f-manual-port" type="number" placeholder="49153" value="49153" />
<label class="form-label">Port (optional)</label>
<input class="form-input" id="add-device-port" type="number" placeholder="49153" min="1" max="65535" />
<div style="font-size:12px;color:var(--text2);margin-top:4px;">Default: 49153 (try 49152-49156 if needed)</div>
</div>
<div id="modal-manual-error" style="display:none;color:var(--danger);font-size:13px;margin-bottom:10px;"></div>
<div id="add-device-error" style="display:none;color:var(--danger);font-size:13px;margin-bottom:10px;"></div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeManualModal()">Cancel</button>
<button class="btn btn-primary" id="btn-add-manual" onclick="addManualDevice()">Add</button>
<button class="btn btn-ghost" onclick="closeAddDeviceModal()">Cancel</button>
<button class="btn btn-primary" id="btn-add-device" onclick="addDeviceManually()">Add Device</button>
</div>
</div>
</div>
@@ -585,14 +650,29 @@ function renderDevices() {
}
el.innerHTML = devices.map((d, i) => {
const name = d.friendlyName || d.name || d.host;
const isDimmer = d.isDimmer || false;
const icon = isDimmer ? '🔆' : '💡';
return `
<div class="card" id="dev-${i}">
<div style="font-size:24px;flex-shrink:0">💡</div>
<div style="font-size:24px;flex-shrink:0">${icon}</div>
<div class="card-body">
<div class="card-name">${esc(name)}</div>
<div class="card-meta">${esc(d.host)}:${d.port}
${d.productModel ? ' · ' + esc(d.productModel) : ''}
${isDimmer ? ' · Dimmer' : ''}
</div>
${isDimmer ? `
<div class="brightness-control" style="margin-top:8px;">
<div style="display:flex;align-items:center;gap:8px;">
<span style="font-size:12px;color:var(--text2);">🔅</span>
<input type="range" class="brightness-slider" id="bright-${i}"
min="0" max="100" value="50"
oninput="updateBrightnessPreview(${i}, this.value)"
onchange="setBrightness(${i}, this.value)">
<span class="brightness-value" id="bright-val-${i}" style="font-size:12px;color:var(--text2);min-width:30px;">50%</span>
</div>
</div>
` : ''}
</div>
<label class="toggle" id="dtog-${i}" onclick="toggleDevice(${i},event)">
<input type="checkbox" id="dchk-${i}">
@@ -601,11 +681,29 @@ function renderDevices() {
</label>
</div>`;
}).join('');
// fetch current state for each
// fetch current state for each device
devices.forEach((d, i) => {
api('GET', `/api/devices/${d.host}/${d.port}/state`)
.then((on) => { const c = document.getElementById('dchk-'+i); if (c) c.checked = !!on; })
.then((on) => {
const c = document.getElementById('dchk-'+i);
if (c) c.checked = !!on;
})
.catch(() => {});
// fetch brightness for dimmer devices
if (d.isDimmer) {
api('GET', `/api/devices/${d.host}/${d.port}/brightness`)
.then((data) => {
const slider = document.getElementById('bright-'+i);
const value = document.getElementById('bright-val-'+i);
if (slider && value && data.brightness !== undefined) {
slider.value = data.brightness;
value.textContent = Math.round(data.brightness) + '%';
}
})
.catch(() => {});
}
});
}
@@ -627,64 +725,34 @@ async function toggleDevice(i, e) {
}
}
// ── Manual Add ─────────────────────────────────────────────────────────────
function openManualModal() {
document.getElementById('f-manual-host').value = '';
document.getElementById('f-manual-port').value = '49153';
document.getElementById('modal-manual-error').style.display = 'none';
document.getElementById('modal-manual').classList.add('open');
setTimeout(() => document.getElementById('f-manual-host').focus(), 100);
}
function closeManualModal(e) {
if (e && e.target !== document.getElementById('modal-manual')) return;
document.getElementById('modal-manual').classList.remove('open');
}
async function addManualDevice() {
const errEl = document.getElementById('modal-manual-error');
errEl.style.display = 'none';
const host = document.getElementById('f-manual-host').value.trim();
const port = parseInt(document.getElementById('f-manual-port').value, 10) || 49153;
if (!host) {
errEl.textContent = '⚠ Enter an IP address';
errEl.style.display = 'block';
return;
// ── Brightness control ───────────────────────────────────────────────────────
function updateBrightnessPreview(i, value) {
const valueEl = document.getElementById('bright-val-' + i);
if (valueEl) {
valueEl.textContent = Math.round(value) + '%';
}
}
const btn = document.getElementById('btn-add-manual');
btn.disabled = true; btn.textContent = 'Adding…';
async function setBrightness(i, value) {
const dev = devices[i];
const slider = document.getElementById('bright-' + i);
if (!dev || !slider) return;
slider.disabled = true;
try {
const result = await api('POST', '/api/devices/discover', {
manualEntries: [{ host, port }]
});
await api('POST', `/api/devices/${dev.host}/${dev.port}/brightness`, { brightness: Number(value) });
if (!result || result.length === 0) {
errEl.textContent = '⚠ No device found at that address';
errEl.style.display = 'block';
btn.disabled = false; btn.textContent = 'Add';
return;
}
const device = result.find((d) => d.host === host);
if (device) {
const friendlyName = device.friendlyName || device.name || host;
toast(`Added ${friendlyName}`, 'success');
devices = result;
renderDevices();
closeManualModal();
} else {
errEl.textContent = '⚠ Device added but not found in results';
errEl.style.display = 'block';
// Update power toggle state based on brightness
const chk = document.getElementById('dchk-' + i);
if (chk) {
chk.checked = Number(value) > 0;
}
toast(`Brightness set to ${Math.round(value)}%`, 'success');
} catch (err) {
errEl.textContent = `${err.message}`;
errEl.style.display = 'block';
toast(`Failed to set brightness: ${err.message}`, 'error');
} finally {
btn.disabled = false; btn.textContent = 'Add';
slider.disabled = false;
}
}
@@ -927,6 +995,67 @@ function closeRuleModal(e) {
document.getElementById('modal-rule').classList.remove('open');
}
// ── Add Device Modal ───────────────────────────────────────────────────────
function openAddDeviceModal() {
document.getElementById('modal-add-device').classList.add('open');
document.getElementById('add-device-error').style.display = 'none';
document.getElementById('add-device-host').value = '';
document.getElementById('add-device-port').value = '';
setTimeout(() => document.getElementById('add-device-host').focus(), 100);
}
function closeAddDeviceModal(e) {
if (e && e.target !== document.getElementById('modal-add-device')) return;
document.getElementById('modal-add-device').classList.remove('open');
}
async function addDeviceManually() {
const errEl = document.getElementById('add-device-error');
const btn = document.getElementById('btn-add-device');
errEl.style.display = 'none';
const host = document.getElementById('add-device-host').value.trim();
const port = document.getElementById('add-device-port').value.trim();
if (!host) {
errEl.textContent = 'IP address is required';
errEl.style.display = 'block';
return;
}
// Basic IP validation
const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (!ipPattern.test(host)) {
errEl.textContent = 'Please enter a valid IP address';
errEl.style.display = 'block';
return;
}
const portNum = port ? parseInt(port, 10) : 49153;
if (portNum < 1 || portNum > 65535) {
errEl.textContent = 'Port must be between 1 and 65535';
errEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.textContent = 'Adding…';
try {
const device = await api('POST', '/api/devices/add', { host, port: portNum });
toast(`Successfully added ${device.friendlyName || device.host}`, 'success');
closeAddDeviceModal();
await loadDevices(); // Refresh device list
} catch (err) {
errEl.textContent = err.message || 'Failed to add device';
errEl.style.display = 'block';
} finally {
btn.disabled = false;
btn.textContent = 'Add Device';
}
}
async function saveRule() {
const errEl = document.getElementById('modal-rule-error');
errEl.style.display = 'none';