diff --git a/apps/desktop/resources/web/index.html b/apps/desktop/resources/web/index.html
index 4805d33..4f664a2 100644
--- a/apps/desktop/resources/web/index.html
+++ b/apps/desktop/resources/web/index.html
@@ -197,6 +197,7 @@
@@ -603,6 +627,67 @@ 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;
+ }
+
+ const btn = document.getElementById('btn-add-manual');
+ btn.disabled = true; btn.textContent = 'Adding…';
+
+ try {
+ const result = await api('POST', '/api/devices/discover', {
+ manualEntries: [{ host, port }]
+ });
+
+ 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';
+ }
+ } catch (err) {
+ errEl.textContent = `⚠ ${err.message}`;
+ errEl.style.display = 'block';
+ } finally {
+ btn.disabled = false; btn.textContent = 'Add';
+ }
+}
+
// ── Rules list ─────────────────────────────────────────────────────────────
async function loadRules() {
try {
diff --git a/docker/server.js b/docker/server.js
index a360b5a..567292b 100644
--- a/docker/server.js
+++ b/docker/server.js
@@ -94,6 +94,10 @@ async function handleRequest(req, res) {
if (url === '/api/devices/discover' && method === 'POST') {
const saved = store.getDevices();
const manual = saved.map((d) => ({ host: d.host, port: d.port }));
+ // Add any manual entries from the request body
+ if (body.manualEntries && Array.isArray(body.manualEntries)) {
+ manual.push(...body.manualEntries);
+ }
const devs = await wemo.discoverDevices(8000, manual);
store.saveDevices(devs);
return json(res, devs);