Commit Graph

1 Commits

Author SHA1 Message Date
mAi
b93c42a6e0 feat(db): solver + setup templates + cables/bundles store
Migration 004:
- setup_templates + setup_template_devices + setup_template_requirements
- 3 built-in templates seeded: Living Room (TV+Soundbar+ChromeCast,
  2× HDMI), Home Office (PC+Screen+Keyboard+Mouse, 1× HDMI + 2× USB),
  Server Rack (NAS+Switch+fritz, 2× RJ45).

Cables store (cables.go):
- CRUD with endpoint validation (port|device|io exactly-one, project-
  scoped). Tx-aware: validateEndpointEx + assertCableTypeEx avoid
  deadlocks when the solver Apply tx holds the MaxOpenConns(1) connection.

Bundles store (bundles.go):
- CRUD with cable_ids replacement on PATCH. createBundle(ex, …, ownTx)
  inherits the caller's tx for solver-internal use; returns a locally-
  constructed Bundle when ownTx=false (re-fetching via s.db would
  deadlock).

Solver (solver.go) implements design v4.1 §5b.2 exactly:
- Pre-fetch devices/ports/cables/requirements/bundles.
- Reserve ports used by manual cables (auto=0) so the solver can't
  reuse them.
- For each requirement (must_connect DESC, id ASC):
    * Resolve cable type: preferred, or T = port-types(from) ∩
      port-types(to). |T|==0 → unsatisfied "no compat type"; |T|>1 →
      "ambiguous"; |T|==1 → that one.
    * Pick lowest-id free port on each side. None → unsatisfied with
      WhichSide hint + cable-type name.
- Endpoint-pair bundle: ≥2 staged cables between the same device pair
  → auto bundle.
- Diff against existing auto cables by (type_id, MIN(from,to), MAX(from,to))
  signature. Matched = kept; new = added; orphans = removed.
- Preview returns the diff without writing; Apply runs in a single tx
  that wipes auto bundles, deletes orphan auto cables, inserts new
  ones, and rebuilds bundles.
- PortsAndResolve: combo helper for the inspector quick-fix —
  inserts a port + re-runs Solve.

Setup-templates store (setup_templates.go):
- List/Get with hydrated devices + requirements.
- ApplyTemplate(projectID, templateID, opts) seeds devices + requirements
  in one tx. Per-device name overrides + opt-out. Name collisions skip
  the device (skipped_devices); requirements whose endpoints both fail
  are also skipped (requirements_skipped). UNIQUE-collision on an
  existing requirement is non-fatal; logged in requirements_skipped.

Snapshot: cables + bundles fields tightened to []Cable / []Bundle and
populated from the store.

11 new tests (solver_test.go), all green with -race:
- Basic NAS↔Switch (RJ45) → 1 cable, auto=true
- Ambiguous cable type → unsatisfied
- No free port → unsatisfied with side hint
- Preview doesn't write
- Apply then re-apply → idempotent (kept=N, added=0)
- Manual cable reserves its port → solver can't claim it
- ApplyTemplate Living Room → 3 devices + 2 requirements + 7 ports
  (from the device-type port seeder)
- Home Office template then Solve → 3 cables, 0 unsatisfied
- Name-collision pre-existing device → skipped + req-pair skipped
2026-05-16 01:02:31 +02:00