mAi e862a06e9d docs: design v4 — solver-as-core, hybrid device-type catalog, requirements
Big rescope driven by m's product-vision clarification: mCables is a
cable-management framework with a solver as its core value prop, not a
manual draw-and-click editor. m declares devices + required connections
between them; the solver emits the cable plan + bundle recommendations,
optimising for maximum bundling.

Schema additions (migrations 002 + 003):
- device_types (catalog) — built-ins (project_id NULL) + project-custom
  (project_id non-null). 11 built-in types seeded with default port
  profiles (NAS, PC, Mac, TV, Soundbar, Switch, fritz, ChromeCast,
  SteamLink, IOx-3/6/8, Notebook).
- device_type_ports (profile rows: cable_type × count × edge).
- devices.type_id (nullable). Picking a type seeds ports once;
  instance-owned thereafter (no retroactive re-seed).
- connection_requirements (per-project, from/to device + preferred type
  + must_connect flag, with order-normalised pair_lo/pair_hi for
  duplicate prevention).
- cables.auto (slice 5.5 migration) — distinguishes solver-owned cables
  from user-drawn ones.

API additions:
- GET /api/device-types (built-ins only, read-only) and
  GET /api/projects/:pid/device-types (built-ins + project-custom merged)
- POST/PATCH/DELETE under /api/projects/:pid/device-types (project-custom
  only; built-ins are 403)
- /api/projects/:pid/connection-requirements full CRUD
- POST /api/projects/:pid/solve with ?preview=1 — pure-function solver
  (greedy port allocation, endpoint-pair bundling for v0); returns
  add[], remove[], bundles_added[], unsatisfied[], warnings[]

Solver algorithm (§5b):
- Read project devices + ports + connection_requirements + manual cables
- Assign each requirement a (port_a, port_b) using the preferred cable
  type (or auto-pick if exactly one type matches both ends)
- Bundle by endpoint-pair (v3 rule, applied to auto cables only)
- Surface unsatisfied requirements per class (no compat type / ambiguous
  type / no free port) — does NOT auto-add ports; UI quick-fix instead
- ?preview=1 returns the diff without writing; default applies in a tx

UI additions:
- Device-create modal: type dropdown (built-ins grouped by kind, then
  project-custom, then "Custom (no type)" for the v3 freeform fallback)
- Left-sidebar Requirements section with + Requirement button
- Header Solve button (S keybinding) → preview modal → Apply
- Inspector for selected device: type, ports grid, unmet requirements
  with red badges + quick-fix actions
- Inspector for selected auto cable: driving requirement, parent bundle,
  Promote-to-manual button

Slice reshape (§8):
- Slices 1, 2 shipped. v4 inserts: 4 = catalog + type-aware device create,
  4.5 = catalog management, 5 = requirements CRUD + UI, 6 = solver MVP +
  Solve button. Old "manual port + manual cable draw" slides to slice 7
  as a tweak path on solver output. Export becomes slice 8.

Six new open questions (§9) for m to gate before slice 4:
1. Path source (auto-route through frame edges / user cable-trays /
   Steiner-tree)?
2. Live-solve vs. button-only?
3. UX when solver has no compatible port pair?
4. Setup templates in v4 or post-MVP?
5. Catalog as code seed or JSON file?
6. Auto-promote vs. explicit Promote-to-manual on solver cable edits?

CLAUDE.md updated to reflect the solver-core framing, hybrid catalog,
connection-requirements model, and auto/manual cable distinction.

Trailer changes to "DESIGN v4 READY FOR REVIEW".
2026-05-15 23:57:22 +02:00

mCables

Cable-management framework for m's setup — visual web editor backed by a single Go binary + SQLite, generating Excalidraw drawings via mExDraw.

Each cable-managed environment (LOFT, OFFICE, …) is a separate mCables project; each project is backed by exactly one .excalidraw drawing on mxdrw.msbls.de.

Status

Slice 1 — bootstrap shipped. Projects + global cable types are end-to-end; the SVG canvas is intentionally empty until slice 2.

Slice What's in it Status
1 Project CRUD, global cable types, empty SVG canvas, project picker
2 Frames + devices, drag-to-position pending
3 Ports + cables (click-port → click-port) pending
4 IO markers + cable-type editing pending
5 Export to mxdrw.msbls.de pending

Run it

go run ./cmd/mcables
# open http://localhost:7777

Or built:

make build
./bin/mcables

The binary serves the frontend from an embedded web/static/ and the JSON API under /api/. SQLite lives at ./data/mcables.db by default.

Environment

Var Default Notes
MCABLES_ADDR 0.0.0.0:7777 Listen address.
MCABLES_DB ./data/mcables.db SQLite path. Parent dir is created on boot.
MEXDRAW_BASE_URL (unset) Used by slice 5 export — not consumed yet.
MEXDRAW_TOKEN (unset) Bearer for the mExDraw export. Not consumed yet.

Tests

make test           # go test -race ./...

Store-level tests cover projects + cable-types CRUD, the drawing_name auto-default, the ?confirm=<name> guardrail on DELETE /api/projects/:pid, and the ON DELETE RESTRICT on a referenced cable type.

API (slice 1)

GET    /api/healthz                       → 200 {"status":"ok"}

GET    /api/projects                      → [Project, …]
POST   /api/projects                      ← {name, drawing_name?, description?}
                                            drawing_name defaults to "<name>.excalidraw"
GET    /api/projects/:pid                 → {project, cable_types, frames, devices, …}
PATCH  /api/projects/:pid                 ← partial
DELETE /api/projects/:pid?confirm=<name>  ← confirm must equal current name

GET    /api/cable-types                   → [CableType, …]   (global)
POST   /api/cable-types                   ← {name, color}
PATCH  /api/cable-types/:id               ← partial — affects every project
DELETE /api/cable-types/:id               ← 409 in_use if any cable references it

Deploy to mDock

mCables runs on mDock at http://mdock:7777 as a docker-compose service under /home/m/stacks/mcables/. Pattern matches the other mDock services (mgreen-journal, mgeo, msports-garmin, …) — no Dokploy, no reverse proxy, LAN-trusted.

Manual deploy (first roll)

  1. Build + push the image (from any host with docker; today the image lives in mAi's Gitea namespace because mAi doesn't have write access to m/):

    docker build -t mgit.msbls.de/mai/mcables:latest .
    awk '/machine mgit.msbls.de/{getline; getline; print $2}' ~/.netrc-mai \
      | docker login mgit.msbls.de -u mAi --password-stdin
    docker push mgit.msbls.de/mai/mcables:latest
    
  2. Prepare directories on mDock (one-time):

    ssh mdock 'mkdir -p /home/m/stacks/mcables/data /home/m/secrets/mcables \
               && touch /home/m/secrets/mcables/.env \
               && chmod 0600 /home/m/secrets/mcables/.env'
    scp docker-compose.yml mdock:/home/m/stacks/mcables/docker-compose.yml
    
  3. Pull + start:

    ssh mdock 'cd /home/m/stacks/mcables && docker compose pull && docker compose up -d'
    
  4. Verify from any LAN host:

    curl http://mdock:7777/api/healthz       # → {"status":"ok"}
    curl http://mdock:7777/api/cable-types   # → the 5 seeded types
    

To update to a new build: rebuild + push the image, then ssh mdock 'cd /home/m/stacks/mcables && docker compose pull && docker compose up -d'.

Persistence

SQLite lives at /home/m/stacks/mcables/data/mcables.db on the host (bind-mounted into the container at /app/data). Container runs as UID 1000:1000 to align with m:m ownership on mDock — DB files end up owned by m, the host user.

docker compose restart keeps the data intact (tested 2026-05-15).

Automation — follow-up task

This first roll is manual. A Gitea Actions workflow on the self-hosted runner already on mDock (/home/m/act-runner/, label self-hosted:host) — build → push → docker compose up -d on every push to main — is a separate task per the design's §10. Tracking spawned by the head if/when wanted.

Design + project conventions

  • docs/design.md — full v3 design (schema, API, importer/export conventions, slices, mDock deploy notes).
  • CLAUDE.md — project instructions for mai workers.

Architecture

Layer Tech
DB SQLite via modernc.org/sqlite (cgo-free), WAL, FKs on
Backend Go 1.22+ net/http ServeMux pattern routing, single binary
Frontend Vanilla ES modules + SVG, no build step, embedded via embed.FS
Export (slice 5) mExDraw HTTP API on mxdrw.msbls.de

LAN-trusted, no auth.

Description
Cable management — visual interface + SQLite inventory, integrates with mExDraw for diagrams.
Readme 378 KiB
Languages
Go 58.5%
JavaScript 34.7%
CSS 4.1%
HTML 2.4%
Dockerfile 0.2%