From 949ac52d3dda35605110d3836e8106b3f075b954 Mon Sep 17 00:00:00 2001 From: bharvey88 <8107750+bharvey88@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:18:14 -0500 Subject: [PATCH 1/2] Validate the optional per-variant installers map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add check_installers_shape, a network-free validator for the new optional installers field in devices.json (channel -> variant -> https URL or null). A null value means the variant has no classic installer and the link should be hidden; any other value must be an https URL, and every variant key must exist in the device's firmware map. Wired into main() alongside the existing repos/config shape checks. Includes unit tests covering the absent map, null hide, URL override, non-dict inputs, non-https values, and unknown variants. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- scripts/test_validate_registry.py | 31 +++++++++++++++++++++++++++++++ scripts/validate_registry.py | 26 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/scripts/test_validate_registry.py b/scripts/test_validate_registry.py index 7354705..775f606 100644 --- a/scripts/test_validate_registry.py +++ b/scripts/test_validate_registry.py @@ -120,5 +120,36 @@ def test_variant_not_in_firmware_errors(self): self.assertTrue(any("no such firmware variant" in e for e in errs), errs) +class InstallersShapeChecks(unittest.TestCase): + FW = {"stable": {"v16": "https://x/m.json", "v14": "https://y/m.json"}} + + def test_absent_ok(self): + self.assertEqual(vr.check_installers_shape(None, self.FW, "dev"), []) + + def test_null_hide_ok(self): + self.assertEqual(vr.check_installers_shape({"stable": {"v16": None}}, self.FW, "dev"), []) + + def test_url_override_ok(self): + ins = {"stable": {"v16": "https://apolloautomation.github.io/WLED-M1/"}} + self.assertEqual(vr.check_installers_shape(ins, self.FW, "dev"), []) + + def test_not_dict_errors(self): + errs = vr.check_installers_shape([], self.FW, "dev") + self.assertTrue(any("installers" in e for e in errs), errs) + + def test_channel_not_dict_errors(self): + errs = vr.check_installers_shape({"stable": "x"}, self.FW, "dev") + self.assertTrue(any("stable" in e for e in errs), errs) + + def test_non_https_non_null_errors(self): + for bad in ("http://x", "ftp://x", "not-a-url", 5): + errs = vr.check_installers_shape({"stable": {"v16": bad}}, self.FW, "dev") + self.assertTrue(any("https URL or null" in e for e in errs), (bad, errs)) + + def test_variant_not_in_firmware_errors(self): + errs = vr.check_installers_shape({"stable": {"ghost": None}}, self.FW, "dev") + self.assertTrue(any("no such firmware variant" in e for e in errs), errs) + + if __name__ == "__main__": unittest.main() diff --git a/scripts/validate_registry.py b/scripts/validate_registry.py index c462381..760d2ec 100644 --- a/scripts/validate_registry.py +++ b/scripts/validate_registry.py @@ -92,6 +92,31 @@ def check_repos_shape(repos, firmware, dev_id): errs.append(f"{dev_id} repos {channel}/{variant}: no such firmware variant") return errs +def check_installers_shape(installers, firmware, dev_id): + """Validate the optional `installers` map (channel -> variant -> https URL or null). + + Network-free. `installers` absent (None) is valid. A variant value of null + means "this variant has no classic installer, hide the link"; any other + value must be an https URL. Every variant key must exist in + `firmware[channel]`. Returns a list of error strings. + """ + errs = [] + if installers is None: + return errs + if not isinstance(installers, dict): + errs.append(f"{dev_id} installers: not an object") + return errs + for channel, variants in installers.items(): + if not isinstance(variants, dict): + errs.append(f"{dev_id} installers {channel}: not an object") + continue + for variant, url in variants.items(): + if url is not None and (not isinstance(url, str) or not url.startswith("https://")): + errs.append(f"{dev_id} installers {channel}/{variant}: not an https URL or null") + if variant not in firmware.get(channel, {}): + errs.append(f"{dev_id} installers {channel}/{variant}: no such firmware variant") + return errs + def check_manifest(dev_id, channel, variant, murl): where = f"{dev_id} {channel}/{variant}" try: @@ -123,6 +148,7 @@ def main(): config = dev.get("config", {}) errors.extend(check_config_shape(config, dev["id"])) errors.extend(check_repos_shape(dev.get("repos"), dev.get("firmware", {}), dev["id"])) + errors.extend(check_installers_shape(dev.get("installers"), dev.get("firmware", {}), dev["id"])) if not isinstance(config, dict): continue for channel, variants in config.items(): From 3f767af6dee875277e049676bd63060f5fa06dee Mon Sep 17 00:00:00 2001 From: bharvey88 <8107750+bharvey88@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:24:35 -0500 Subject: [PATCH 2/2] Follow the selected variant for header GitHub and classic-installer links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The header "GitHub" link and the "Classic installer" links (header + USB-fallback) were static, always pointing at device.repo and device.githubPagesInstaller regardless of the selected variant. Add a classicInstallerFor(device, channel, variant) resolver alongside the existing repoFor, and a new installers map in devices.json (mirroring the shape of the existing repos map) so a variant's classic installer can be overridden or hidden entirely with a null value. Replace the static header link row with a re-rendered #links-slot driven by a new renderLinks() function, called on initial render and on every variant/channel change, matching the existing pattern used by renderInstall/renderConfig/renderReleaseNotes. The Classic installer link is omitted from both the header and the USB-fallback list whenever the resolved value is null, which is the case for M-1's WLED 16.0.1 (Rev6 only) variant. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- devices.json | 5 +++++ js/views/device.js | 29 +++++++++++++++++++++++------ tests/installer.spec.js | 28 ++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/devices.json b/devices.json index a0f0cf8..fb8a782 100644 --- a/devices.json +++ b/devices.json @@ -239,6 +239,11 @@ "stable": { "WLED 16.0.1 (Rev6 only)": "ApolloAutomation/WLED-M1" } + }, + "installers": { + "stable": { + "WLED 16.0.1 (Rev6 only)": null + } } } ] diff --git a/js/views/device.js b/js/views/device.js index 4a46de8..f976e76 100644 --- a/js/views/device.js +++ b/js/views/device.js @@ -30,6 +30,11 @@ function repoFor(device, channel, variant) { return (device.repos && device.repos[channel] && device.repos[channel][variant]) || device.repo; } +function classicInstallerFor(device, channel, variant) { + const map = device.installers && device.installers[channel]; + return map && variant in map ? map[variant] : device.githubPagesInstaller; +} + function segHtml(id, label, keys, active, dataAttr) { if (keys.length < 2) return ''; return ` @@ -56,11 +61,7 @@ export function renderDevice(el, device) {
@@ -100,6 +101,18 @@ export function renderDevice(el, device) { const variantSlot = el.querySelector('#variant-slot'); const installSlot = el.querySelector('#install-slot'); + const linksSlot = el.querySelector('#links-slot'); + + function renderLinks() { + const repo = repoFor(device, channel, variant); + const classic = classicInstallerFor(device, channel, variant); + const parts = [ + `Setup guide`, + `GitHub`, + ]; + if (classic) parts.push(`Classic installer`); + linksSlot.innerHTML = parts.join(' · '); + } function renderVariantSeg() { variantSlot.innerHTML = segHtml('variant-seg', 'Variant', Object.keys(device.firmware[channel]), variant, 'variant'); @@ -117,6 +130,7 @@ export function renderDevice(el, device) { renderInstall(); renderConfig(); renderReleaseNotes(); + renderLinks(); }); } @@ -124,6 +138,7 @@ export function renderDevice(el, device) { const myEpoch = epoch; const manifest = selectedManifest(device, channel, variant); const repo = repoFor(device, channel, variant); + const classic = classicInstallerFor(device, channel, variant); if (hasSerial) { installSlot.innerHTML = `esptool write-flash --port <port> 0x0 <file>.