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
+
+
This list is what's installed on this gateway. More drivers can be
+ installed from the signed driver repository after setup, in
+ Settings → Devices.
+
No driver for your device anywhere? Ask for one in the
+ driver
+ repository's issue tracker — include the device make, model
+ and how it talks (Modbus, MQTT, HTTP…).
+
+
+
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");
+ });
+});