From f1af2820e1bfe1491d5e22ab3d9aba777acb1317 Mon Sep 17 00:00:00 2001 From: mAi Date: Sat, 16 May 2026 11:03:32 +0200 Subject: [PATCH] =?UTF-8?q?fix(catalog):=20migration=20006=20=E2=80=94=20I?= =?UTF-8?q?Ox-*=20and=20Multi-plug-*=20are=20power=20strips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit m's actual hardware: IOx-3/6/8 are power strips, not USB hubs. v4 seeded them as Power × 1 + USB × N which doesn't match reality. Multi-plug 3-6 and Wifi-plug from v5 lumped every Power port on the same bottom edge without distinguishing input from outputs. Migration 006 wipes and re-seeds the port profile for all 8 power-distribution types with the canonical 2-row layout: Power In × 1 on top (back, sort_order 0) Power Out × N on bottom (front, sort_order 1) N for each: IOx-3 / Multi-plug 3 → 3 IOx-6 / Multi-plug 6 → 6 IOx-8 → 8 Multi-plug 4 → 4 Multi-plug 5 → 5 Wifi-plug → 1 (pass-through outlet) Existing device instances keep their already-seeded ports per design §2.3 (ports are instance-owned). m needs to delete + recreate any IOx-* / Multi-plug-* / Wifi-plug instances to pick up the new layout. Tests: - TestSeed_PortProfiles: comments updated; totals unchanged (Power In 1 + Power Out N matches old Power 1 + USB N / Power N). - TestSeed_PowerHubs (was TestSeed_PowerCatalog, rewritten): table-drives all 8 affected types. Asserts exactly 2 port rows — top/Power In/1 and bottom/Power Out/N — plus kind/icon for the v5 catalog entries. Design §2.2 catalog table refreshed to match. --- docs/design.md | 24 +++-- internal/db/device_types_test.go | 73 ++++++++++------ internal/db/migrations/006_fix_power_hubs.sql | 87 +++++++++++++++++++ 3 files changed, 148 insertions(+), 36 deletions(-) create mode 100644 internal/db/migrations/006_fix_power_hubs.sql diff --git a/docs/design.md b/docs/design.md index 78655c5..ab39a41 100644 --- a/docs/design.md +++ b/docs/design.md @@ -453,18 +453,26 @@ Office setup template: | fritz | network | Power × 1; RJ45 × 4 | | ChromeCast | display | Power × 1; HDMI × 1 | | SteamLink | compute | Power × 1; HDMI × 1; USB × 2 | -| IOx-3 | hub | Power × 1; (3× port slots — concrete cable type per slot is set at instantiation; defaults to USB × 3 for v0) | -| IOx-6 | hub | Power × 1; USB × 6 | -| IOx-8 | hub | Power × 1; USB × 8 | +| IOx-3 | hub | Power In × 1 (top/back); Power Out × 3 (bottom/front) | +| IOx-6 | hub | Power In × 1 (top/back); Power Out × 6 (bottom/front) | +| IOx-8 | hub | Power In × 1 (top/back); Power Out × 8 (bottom/front) | | **Screen** | display | Power × 1; HDMI × 1 | | **Keyboard** | accessory | USB × 1 | | **Mouse** | accessory | USB × 1 | +| **Multi-plug 3** | hub | Power In × 1 (top/back); Power Out × 3 (bottom/front) | +| **Multi-plug 4** | hub | Power In × 1 (top/back); Power Out × 4 (bottom/front) | +| **Multi-plug 5** | hub | Power In × 1 (top/back); Power Out × 5 (bottom/front) | +| **Multi-plug 6** | hub | Power In × 1 (top/back); Power Out × 6 (bottom/front) | +| **Wifi-plug** | accessory | Power In × 1 (top/back); Power Out × 1 (bottom/front) — pass-through outlet | -"Hub" devices like IOx-* have ambiguous port profiles (the seed drawing -shows them in red because most carry Power, but they also hub USB). v0 -seeds them as USB hubs; m overrides per-instance. The catalog is editable -in the UI (slice 4.5 — "Manage device types") so m can refine the IOx-3 -profile once and not re-override every instance. +v5 (migration 005) added the Multi-plug 3–6 strips and the Wifi-plug +pass-through outlet. v6 (migration 006) re-shaped the IOx-* and +Multi-plug-* profiles to the "1 in on top / N out on bottom" layout — +the IOx-* devices are physical power strips, not USB hubs (m's +hardware), and the Multi-plug-* outputs are now visually distinct from +the input. Convention: `top = back`, `bottom = front`. Existing device +instances keep their already-seeded ports per §2.3 — to pick up the +new layout, delete + re-create the instance. m can also add **project-custom types** at any time (UI: "+ New device type" inside the device-create modal) with `project_id = current`. diff --git a/internal/db/device_types_test.go b/internal/db/device_types_test.go index fad9925..bb27ad4 100644 --- a/internal/db/device_types_test.go +++ b/internal/db/device_types_test.go @@ -55,17 +55,17 @@ func TestSeed_PortProfiles(t *testing.T) { "fritz": {5}, // Power 1 + RJ45 4 "ChromeCast": {2}, // Power 1 + HDMI 1 "SteamLink": {4}, // Power 1 + HDMI 1 + USB 2 - "IOx-3": {4}, // Power 1 + USB 3 - "IOx-6": {7}, // Power 1 + USB 6 - "IOx-8": {9}, // Power 1 + USB 8 + "IOx-3": {4}, // Power In 1 + Power Out 3 (after v6) + "IOx-6": {7}, // Power In 1 + Power Out 6 (after v6) + "IOx-8": {9}, // Power In 1 + Power Out 8 (after v6) "Screen": {2}, // Power 1 + HDMI 1 "Keyboard": {1}, // USB 1 "Mouse": {1}, // USB 1 - "Multi-plug 3": {4}, // Power 4 - "Multi-plug 4": {5}, // Power 5 - "Multi-plug 5": {6}, // Power 6 - "Multi-plug 6": {7}, // Power 7 - "Wifi-plug": {2}, // Power 2 + "Multi-plug 3": {4}, // Power In 1 + Power Out 3 (after v6) + "Multi-plug 4": {5}, // Power In 1 + Power Out 4 (after v6) + "Multi-plug 5": {6}, // Power In 1 + Power Out 5 (after v6) + "Multi-plug 6": {7}, // Power In 1 + Power Out 6 (after v6) + "Wifi-plug": {2}, // Power In 1 + Power Out 1 (after v6) } for name, want := range cases { dt, ok := byName[name] @@ -83,10 +83,16 @@ func TestSeed_PortProfiles(t *testing.T) { } } -// TestSeed_PowerCatalog locks down migration 005: the 5 power-distribution -// device types are present with the expected kind/icon/port profile, and -// the total built-in count rose from 16 to 21. -func TestSeed_PowerCatalog(t *testing.T) { +// TestSeed_PowerHubs locks down the post-migration-006 port profile for +// every power-distribution device type: IOx-3/6/8, Multi-plug 3/4/5/6, +// and Wifi-plug. Each carries exactly two profile rows — a single +// "Power In" port on the top (back) edge and N "Power Out" ports on the +// bottom (front) edge, where N is the device-specific output count. +// +// This test covers the v5 catalog identity (kind, icon, built-in) for +// the 5 power-distribution types and the v6 port-profile fix for all +// 8 hubs in one table. +func TestSeed_PowerHubs(t *testing.T) { s := newTestStore(t) all, err := s.ListBuiltInDeviceTypes() if err != nil { @@ -100,16 +106,23 @@ func TestSeed_PowerCatalog(t *testing.T) { byName[d.Name] = d } cases := []struct { - name string - kind string - icon string - powerPort int // count on the single Power port row + name string + // kind/icon are only set for the 5 v5-power types; empty means + // "don't check" (the IOx-* keep their v4-seeded kind=hub icon=nil). + kind string + icon string + outCount int // N — number of "Power Out" outlets on the bottom edge }{ - {"Multi-plug 3", "hub", "🔌", 4}, - {"Multi-plug 4", "hub", "🔌", 5}, - {"Multi-plug 5", "hub", "🔌", 6}, - {"Multi-plug 6", "hub", "🔌", 7}, - {"Wifi-plug", "accessory", "📶", 2}, + // v5 catalog (kind+icon checked) + {name: "Multi-plug 3", kind: "hub", icon: "🔌", outCount: 3}, + {name: "Multi-plug 4", kind: "hub", icon: "🔌", outCount: 4}, + {name: "Multi-plug 5", kind: "hub", icon: "🔌", outCount: 5}, + {name: "Multi-plug 6", kind: "hub", icon: "🔌", outCount: 6}, + {name: "Wifi-plug", kind: "accessory", icon: "📶", outCount: 1}, + // v4 hubs re-shaped by v6 (kind/icon left blank → not checked) + {name: "IOx-3", outCount: 3}, + {name: "IOx-6", outCount: 6}, + {name: "IOx-8", outCount: 8}, } for _, c := range cases { dt, ok := byName[c.name] @@ -123,19 +136,23 @@ func TestSeed_PowerCatalog(t *testing.T) { if dt.ProjectID != nil { t.Errorf("%s: project_id should be nil", c.name) } - if dt.Kind != c.kind { + if c.kind != "" && dt.Kind != c.kind { t.Errorf("%s: kind = %q, want %q", c.name, dt.Kind, c.kind) } - if dt.Icon == nil || *dt.Icon != c.icon { + if c.icon != "" && (dt.Icon == nil || *dt.Icon != c.icon) { t.Errorf("%s: icon = %v, want %q", c.name, dt.Icon, c.icon) } - if len(dt.Ports) != 1 { - t.Errorf("%s: expected 1 port-profile row, got %d", c.name, len(dt.Ports)) + if len(dt.Ports) != 2 { + t.Errorf("%s: expected 2 port-profile rows, got %d", c.name, len(dt.Ports)) continue } - p := dt.Ports[0] - if p.CableTypeID != 1 || p.Count != c.powerPort || p.Edge != "bottom" || p.LabelPrefix != "Power" { - t.Errorf("%s: port profile mismatch: %+v", c.name, p) + in := dt.Ports[0] + out := dt.Ports[1] + if in.CableTypeID != 1 || in.Count != 1 || in.Edge != "top" || in.LabelPrefix != "Power In" { + t.Errorf("%s: Power In row mismatch: %+v", c.name, in) + } + if out.CableTypeID != 1 || out.Count != c.outCount || out.Edge != "bottom" || out.LabelPrefix != "Power Out" { + t.Errorf("%s: Power Out row mismatch: %+v (want count=%d)", c.name, out, c.outCount) } } } diff --git a/internal/db/migrations/006_fix_power_hubs.sql b/internal/db/migrations/006_fix_power_hubs.sql new file mode 100644 index 0000000..257fbbc --- /dev/null +++ b/internal/db/migrations/006_fix_power_hubs.sql @@ -0,0 +1,87 @@ +-- mCables v6 — fix IOx-* and Multi-plug-* + Wifi-plug port profiles. +-- +-- v4 seeded the IOx-3 / IOx-6 / IOx-8 as USB hubs (Power × 1 + USB × N), +-- but m's physical IOx-* devices are power strips (1 power input on +-- the back, N power outputs on the front). v5's Multi-plug 3/4/5/6 +-- profiles also lumped every Power port on the bottom edge without +-- distinguishing the input from the outputs. +-- +-- This migration replaces the port profile for the 8 power-distribution +-- types with the canonical "1 in (top/back) + N out (bottom/front)" +-- layout. Convention: top=back, bottom=front. +-- +-- N for each type: +-- IOx-3 / Multi-plug 3 → 3 outputs +-- IOx-6 → 6 outputs +-- IOx-8 → 8 outputs +-- Multi-plug 4 → 4 outputs +-- Multi-plug 5 → 5 outputs +-- Multi-plug 6 → 6 outputs +-- Wifi-plug → 1 output (it's a pass-through outlet) +-- +-- Existing devices m may have created with the old profile keep their +-- already-seeded ports — per design §2.3, ports are instance-owned. To +-- get the new layout on an existing instance, delete it and re-create. +-- +-- cable_types id 1 = Power (seeded in 001). + +-- 1) Drop the existing port-profile rows for each affected type. +DELETE FROM device_type_ports + WHERE device_type_id IN ( + SELECT id FROM device_types + WHERE project_id IS NULL + AND name IN ( + 'IOx-3', 'IOx-6', 'IOx-8', + 'Multi-plug 3', 'Multi-plug 4', 'Multi-plug 5', 'Multi-plug 6', + 'Wifi-plug' + ) + ); + +-- 2) Insert the canonical (1 in on top, N out on bottom) profile. +-- IOx-3 — 1 in + 3 out +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='IOx-3' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 3, 'bottom', 1 FROM device_types WHERE name='IOx-3' AND project_id IS NULL; + +-- IOx-6 — 1 in + 6 out +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='IOx-6' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 6, 'bottom', 1 FROM device_types WHERE name='IOx-6' AND project_id IS NULL; + +-- IOx-8 — 1 in + 8 out +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='IOx-8' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 8, 'bottom', 1 FROM device_types WHERE name='IOx-8' AND project_id IS NULL; + +-- Multi-plug 3 — 1 in + 3 out +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='Multi-plug 3' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 3, 'bottom', 1 FROM device_types WHERE name='Multi-plug 3' AND project_id IS NULL; + +-- Multi-plug 4 — 1 in + 4 out +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='Multi-plug 4' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 4, 'bottom', 1 FROM device_types WHERE name='Multi-plug 4' AND project_id IS NULL; + +-- Multi-plug 5 — 1 in + 5 out +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='Multi-plug 5' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 5, 'bottom', 1 FROM device_types WHERE name='Multi-plug 5' AND project_id IS NULL; + +-- Multi-plug 6 — 1 in + 6 out +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='Multi-plug 6' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 6, 'bottom', 1 FROM device_types WHERE name='Multi-plug 6' AND project_id IS NULL; + +-- Wifi-plug — 1 in + 1 out (pass-through outlet) +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power In', 1, 'top', 0 FROM device_types WHERE name='Wifi-plug' AND project_id IS NULL; +INSERT INTO device_type_ports (device_type_id, cable_type_id, label_prefix, count, edge, sort_order) + SELECT id, 1, 'Power Out', 1, 'bottom', 1 FROM device_types WHERE name='Wifi-plug' AND project_id IS NULL;