diff --git a/.changeset/757-setup-driver-not-listed.md b/.changeset/757-setup-driver-not-listed.md new file mode 100644 index 000000000..5b5bcfca8 --- /dev/null +++ b/.changeset/757-setup-driver-not-listed.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +The setup wizard's driver picker no longer dead-ends when a device has no catalog entry. A "My device is not listed…" option explains that more drivers can be installed from the signed repository after setup (Settings → Devices), links to requesting a driver that doesn't exist yet, and lets onboarding continue without the device. diff --git a/web/setup.html b/web/setup.html index 2b0cfcf7f..00343ab5b 100644 --- a/web/setup.html +++ b/web/setup.html @@ -619,6 +619,18 @@

Pick driver

+ +
diff --git a/web/setup.js b/web/setup.js index 93d5445dd..b5e4f9562 100644 --- a/web/setup.js +++ b/web/setup.js @@ -13,6 +13,10 @@ var selectedCatalog = null; // CatalogEntry from /api/drivers/catalog var driverCatalog = []; // full catalog cache + // Picker option for a device with no catalog entry. Deliberately not a + // number so it can never be mistaken for a catalog index. + var NOT_LISTED = '__not_listed__'; + // --- Step navigation --- function renderDots() { @@ -51,6 +55,13 @@ goStep(configuredDrivers.length > 0 ? 6 : 2); }; + // Forward path for a device the catalog cannot serve: the devices + // summary when something is already configured, otherwise straight on + // to the integrations. + window.skipUnlistedDevice = function () { + goStep(configuredDrivers.length > 0 ? 6 : 7); + }; + // --- Step 3: Scan --- window.startScan = function () { @@ -202,6 +213,14 @@ sel.appendChild(opt); }); + // The escape hatch. Without it, a device with no catalog entry + // dead-ends this step: Continue stays disabled and the only + // affordance left is Back. + var notListed = document.createElement('option'); + notListed.value = NOT_LISTED; + notListed.textContent = 'My device is not listed…'; + sel.appendChild(notListed); + // A positive fingerprint preselects the matching catalog driver while // still sending the operator through the normal configuration form. if (selectedDevice && selectedDevice.matchedFilename) { @@ -222,13 +241,26 @@ var sel = document.getElementById('driver-select'); var btn = document.getElementById('driver-next-btn'); var descEl = document.getElementById('driver-description'); + var notListedEl = document.getElementById('driver-not-listed'); if (!sel.value) { selectedCatalog = null; btn.disabled = true; descEl.style.display = 'none'; + notListedEl.style.display = 'none'; + return; + } + + if (sel.value === NOT_LISTED) { + // Nothing to configure, so Continue stays held; the panel offers + // its own forward path instead. + selectedCatalog = null; + btn.disabled = true; + descEl.style.display = 'none'; + notListedEl.style.display = 'block'; return; } + notListedEl.style.display = 'none'; selectedCatalog = driverCatalog[parseInt(sel.value, 10)]; btn.disabled = false; diff --git a/web/setup.test.mjs b/web/setup.test.mjs index c2be44ab2..b5da5a439 100644 --- a/web/setup.test.mjs +++ b/web/setup.test.mjs @@ -144,3 +144,33 @@ describe("price provider defaults", () => { assert.match(PRICE_JS, /\["sourceful", "elprisetjustnu", "entsoe", "none"\], "sourceful"/); }); }); + +describe("setup wizard driver picker — not-listed escape hatch (#757)", () => { + it("appends the not-listed option after the catalog entries", () => { + assert.match(JS, /NOT_LISTED\s*=\s*['"]__not_listed__['"]/, + "the sentinel must be a value that can never parse as a catalog index"); + assert.match(JS, /My device is not listed/, + "the picker must offer the escape hatch in its own words"); + }); + + it("handles the sentinel before parsing a catalog index", () => { + assert.match(JS, /sel\.value\s*===\s*NOT_LISTED[\s\S]*?parseInt\(sel\.value,\s*10\)/, + "onDriverSelected must branch on the sentinel before parseInt runs on it"); + }); + + it("ships the guidance panel with both forward paths", () => { + assert.match(HTML, /id=["']driver-not-listed["']/, + "the panel the sentinel reveals must exist in the markup"); + assert.match(HTML, /Settings\s*→\s*Devices/, + "it must say where repository drivers install after setup"); + assert.match(HTML, /device-drivers\/issues/, + "it must link to requesting a driver that does not exist yet"); + assert.match(HTML, /skipUnlistedDevice\(\)/, + "it must let onboarding continue without the device"); + }); + + it("skips to the devices summary only when a device already exists", () => { + assert.match(JS, /skipUnlistedDevice[\s\S]*?configuredDrivers\.length > 0 \? 6 : 7/, + "an empty summary step is a second dead-end; go straight to integrations"); + }); +});