Commit Graph

85 Commits

Author SHA1 Message Date
mAi
1ea6082948 feat: db store — IO markers CRUD, snapshot wiring
Schema already in 001_init.sql; this is just the Go store layer.

IO markers are project-scoped wall-outlet terminators (a cable's
"this end plugs into a wall socket outside the diagram" endpoint).
Power-by-convention; no schema-level type enforcement.

- CreateIOMarker validates frame_id is in the same project (cross-project
  ref → ErrInvalidInput), defaults label to "IO" when blank.
- GetIOMarker is project-scoped — wrong-project read returns ErrNotFound.
- UpdateIOMarker uses the FrameRef tri-state for frame_id (same as
  DeviceUpdate) so callers can clear it explicitly.
- DeleteIOMarker is direct delete — ON DELETE SET NULL from the schema
  drops the io_markers.frame_id ref cleanly when the frame is deleted
  (verified by TestDeleteFrame_SetsIOMarkerFrameIDToNull).

Snapshot now populates IOMarkers from the store; field type tightened
from []any to []IOMarker.

7 new table-driven tests, all green with -race.
2026-05-16 00:05:40 +02:00
mAi
376ffd8197 merge: design v4.1 — schematic-only, templates folded in
m's review of v4 locked 6 answers. Tight doc pass:
- Schematic-only bundling: dropped trunk-segment/frame-edge/cable-tray
  language. v3 endpoint-pair rule is the only bundle rule.
- Setup templates folded in (not post-MVP): migration 004 with 3
  built-ins (Living Room, Home Office, Server Rack) + 3 new device
  types (Screen, Keyboard, Mouse) + apply-template API in slice 6.
- Unmet-requirement quick-fix: combo endpoint adds a missing port and
  re-solves in one server roundtrip.
- Solver still button-only, catalog still SQL-seeded, promote still
  explicit on cable inspector.

All 9 §9 questions resolved.
2026-05-16 00:03:51 +02:00
mAi
e42b351280 docs: design v4.1 — schematic-only bundling, setup templates folded in
Tight pass on m's review of v4 (single commit per head's instruction).

Six locked answers integrated:

1. mCables is a schematic, not a physical-routing tool. Stripped
   'trunk', 'frame-edge corridor', 'cable tray', 'path optimisation'
   from §5b.1, §5b.2, §7, §8, §9. Bundling reduces to the v3 endpoint-
   pair rule: ≥2 cables between the same A↔B endpoint pair → group as
   one bundle. Anything path-shaped is "out of scope, period" (§8).
2. Solver button-only for v0 (no change). Live-solve parked at 9+.
3. Unmet-requirement quick-fix: red badge on the affected device in the
   inspector with a single "+ Add <type> port to <device> and re-solve"
   button per §5b.4. New endpoint
   POST /api/projects/:pid/devices/:id/ports-and-resolve chains the
   port insert + the solve re-run in one transaction.
4. Setup templates fold INTO v4.1. New §2.4 with the schema for
   setup_templates + setup_template_devices + setup_template_requirements
   (migration 004), 3 built-in templates seeded (Living Room, Home
   Office, Server Rack). New API: GET /api/setup-templates,
   POST /api/projects/:pid/apply-template. New UI flow: "or start from
   a template" section in the New Project modal + an "Apply template"
   action on empty projects. Built-in catalog grows to 14 types
   (adds Screen, Keyboard, Mouse).
5. Catalog SQL seed in migration 002 (no change).
6. Promote-to-manual: explicit button on cable inspector (no change).

§8 slice 6 absorbs the templates work alongside the solver MVP.
§9 closes all six v4 questions; no open design questions remain.
Trailer changes to "DESIGN v4.1 READY FOR REVIEW".

CLAUDE.md mirrors: schematic-only framing, 14-type catalog, setup
templates as a first-class feature, quick-fix UX note.
2026-05-16 00:03:19 +02:00
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
mAi
4f862e741a merge: fix inspector not updating on device/frame selection
startDrag set state.selection but never re-rendered. One render() call
after the assignment fixes it. Now selecting a device or frame
populates the inspector with name/dims/delete-button as designed.
2026-05-15 23:39:15 +02:00
mAi
29e221e080 fix(ui): inspector now updates on device/frame selection
startDrag set state.selection but didn't render until pointerup's onUp
ran — and onUp can throw on `e.currentTarget.classList.remove` if the
event reference is stale after pointer capture release, which leaves
the inspector stuck on 'Nothing selected.'

One-line fix: call render() right after state.selection assignment so
the inspector + halo update from pointerdown, independent of whether
onUp completes cleanly. The drag-completion render at the end of onUp
stays — when both fire it's idempotent (renders are pure functions of
state).
2026-05-15 23:38:12 +02:00
mAi
c7dfbe010c merge: fix +Dev inline-namer blur (sherlock's preventDefault diagnosis)
Primary fix: e.preventDefault() on the pointerdown for both armed-tool
branches in onCanvasPointerDown. Without it, the browser's default
mousedown action blurs the freshly-focused input in promptInline
(the SVG click target isn't focusable), and the blur handler calls
done(null) before m can type.

Secondary fix: clear activeNamer before fo.remove() in done(), to
prevent a re-entrant pageerror when Enter triggers blur synchronously.
2026-05-15 23:18:57 +02:00
mAi
12804619b2 fix(ui): +Dev inline-namer kept getting blurred by default mousedown
Root cause traced by sherlock with Playwright (docs/sherlock-+dev-bug.md
on the sherlock branch). The previous routing fix at 94869f3 was
necessary but not sufficient: placeDeviceAt() now reaches promptInline()
correctly, but the synchronous input.focus() is undone ~6ms later by
the browser's compatibility-mousedown default — which blurs the active
element when the mousedown landed on a non-focusable target (SVG rect /
SVG root). The blur listener then ran done(null) and ripped the
<foreignObject> out before m could type a name.

Primary fix: e.preventDefault() at the top of both armed-tool branches
in onCanvasPointerDown. Suppresses the focus-shifting default so the
input keeps focus.

+Frame is also wrapped for symmetry. It wasn't strictly affected (its
namer runs from pointerup, not pointerdown) but preventDefault avoids
a subtle text-selection side effect during rubber-band drag.

Secondary fix in promptInline.done(): clear activeNamer *before*
fo.remove(). Enter-key triggers a synchronous blur listener which
re-enters done() — if remove() ran first, the re-entry hit a
"node no longer a child" pageerror. Reordering makes the re-entry a
no-op (activeNamer is already null).

Verified locally: served /main.js shows e.preventDefault() inside both
tool branches and the reordered done() body. go test -race ./... still
green.
2026-05-15 23:17:44 +02:00
mAi
e12b449169 merge: cursor + cache fixes
- CSS: .canvas-wrap.tool-{frame,device} #canvas, #canvas * { cursor:
  crosshair !important } so frame/device rects don't display grab while
  a tool is armed
- Server: Cache-Control: no-cache on embedded static handler so browsers
  revalidate via ETag instead of serving stale main.js after redeploy
2026-05-15 20:40:07 +02:00
mAi
28a376a7f3 fix(ui+server): tool cursor wins on canvas children; no-cache static assets
Issue 1 — cursor lies about armed tool. .svg-draggable { cursor: grab }
on frame/device rects beat the .canvas-wrap.tool-device #canvas {
cursor: crosshair } rule because element-level wins over descendant.
m saw "grab" hovering a frame with +Dev armed and thought the tool was
broken even though clicks routed correctly after the previous fix. Add
a descendant rule with !important so tool-armed wraps any child cursor:
  .canvas-wrap.tool-frame  #canvas *,
  .canvas-wrap.tool-device #canvas * { cursor: crosshair !important; }

Issue 2 — stale browser cache after each redeploy. http.FileServerFS
served embedded assets with no Cache-Control header, so browsers held
on to the previous main.js/style.css until hard-reload. New noCache
middleware on the static handler emits Cache-Control: no-cache. Note:
embedded FS files have zero ModTime, so http.FileServer suppresses
Last-Modified — every fetch is a fresh 200 rather than a 304. Fine at
~30KB of JS+CSS, and fixes the staleness problem completely.

Middleware is wrapped only around the static handler. /api/* responses
write their own headers and aren't touched.

Verified locally:
  curl -I /main.js   → Cache-Control: no-cache
  curl -I /style.css → Cache-Control: no-cache + contains the new rule
  curl -I /api/healthz → unaffected (no Cache-Control from us)
go test -race ./... still green.
2026-05-15 20:38:48 +02:00
mAi
6d637e1fac merge: fix +Dev inside frame silently dropped
Move the [data-frame-id]/[data-device-id] early-return below the
tool-armed branches in onCanvasPointerDown. With a tool armed,
the canvas-level handler always wins; without a tool, the original
behaviour (frame/device pointerdown handlers capture for drag/select)
is restored.
2026-05-15 20:34:39 +02:00
mAi
94869f342e fix(ui): +Dev inside a frame was silently dropped
onCanvasPointerDown returned early whenever the click landed on a
[data-device-id] or [data-frame-id] element so the per-element drag
handlers wouldn't get hijacked. Problem: this early-return fired BEFORE
the tool check, so clicking +Dev inside an existing frame never reached
placeDeviceAt().

Reordered: tool-armed branches run first and short-circuit. Only when
no tool is armed does the "click started on a child element — leave it
alone" guard kick in. End behaviour:
- +Dev anywhere (incl. inside a frame) drops a device. frame_id
  auto-resolves via the existing frameAt() point-in-rect.
- +Frm anywhere (incl. inside an existing frame) starts a rubber-band;
  rare but not harmful.
- No tool armed: clicking a device/frame still goes to its own handler
  (drag / select). Clicking empty canvas still clears selection.

Hand-tested via the served /main.js + the equivalent backend POST/PATCH
sequence: device-in-frame, device-outside, device-drag, frame-drag with
cascaded device patches — all work.
2026-05-15 20:33:17 +02:00
mAi
a9e6d7aa62 merge: slice 2 — frames + devices + drag-to-position
picasso shipped (3 commits @ b159131):
- internal/db/frames_devices.go: project-scoped CRUD, cross-project FK
  rejection, sentinel errors (duplicate name -> 409, invalid input -> 400)
- internal/server/frames_devices.go: handlers under /api/projects/:pid/
  {frames,devices}, full CRUD
- web/static: SVG rendering + tools (+ Frm rubber-band, + Dev click-place),
  drag with frame-children-follow, inspector with debounced edits

30 store tests green with -race. Hand-test: cross-frame device drag,
frame-drag-with-children, server restart all preserve state.
2026-05-15 18:23:37 +02:00
mAi
b15913124a feat: frontend — frames + devices on SVG, tools, drag, inspector
Renders the slice-2 backend on the empty canvas from slice 1.

Canvas:
- Frames render as dashed-stroke rects with top-left label, slightly
  tinted fill. Devices render as solid-stroke rects with centred label
  in device.color.
- Selection halo via .selected class (stroke-width bump).
- Empty-state hint disappears once any geometry exists.

Tools (left sidebar + keyboard):
- F / + Frame  — rubber-band rect on the canvas. <80×60 cancels. On
  release, inline foreignObject namer → POST /api/projects/:pid/frames.
- D / + Device — single click places a 100×35 device centred at the
  click. Inline namer → POST devices. Drop-point determines initial
  frame_id via point-in-rect against all frames (smallest bbox wins).
- Esc cancels active tool / inline namer / clears selection.

Drag (pointer events + svg getScreenCTM):
- Devices: drag updates x/y live via transform, persists via
  PATCH .../devices/:id on pointerup. Also recomputes frame_id from
  drop point and includes "frame_id": null|<id> if it changed.
- Frames: dragging a frame moves its contained devices visually too;
  on pointerup, single PATCH for the frame + one PATCH per moved device.
  Children-batch is computed at pointerdown and only sent on release —
  no per-pointermove network traffic.

Inspector:
- Frame selection: name (debounced rename), x/y/w/h, device count,
  Delete button (confirm prompt — devices keep existing, frame_id → NULL
  via the schema's ON DELETE SET NULL).
- Device selection: name (debounced rename), colour picker
  (change-event PATCH, no debounce), x/y/w/h, current frame, Delete.
- Background click clears selection.

devicePatch wire format uses tri-state frame_id: key absent = leave,
key:null = clear, key:<int> = move. Frontend uses `null` explicitly
when a device drops outside all frames.
2026-05-15 18:22:49 +02:00
mAi
21bf00566c feat: http handlers — frames + devices CRUD under /api/projects/:pid/
All 8 endpoints (list, create, patch, delete) for both resources. Path
params parsed via Go 1.22 ServeMux PathValue.

devicePatch uses json.RawMessage for frame_id so the wire format
distinguishes:
  - key absent       → leave as-is
  - "frame_id": null → clear (device leaves all frames)
  - "frame_id": 42   → move to that frame
parseFrameRef translates that into the store's db.FrameRef tri-state.

Sentinel-error mapping unchanged (writeError covers ErrInvalidInput,
ErrConflict, ErrNotFound, etc.). Cross-project frame_id refs surface as
400.
2026-05-15 18:17:43 +02:00
mAi
cf1671e8c1 feat: db store — frames + devices CRUD, project-scoped
Snapshot now populates frames + devices from the DB (slice 1 left them as
empty arrays).

Frame store:
- CreateFrame requires positive width/height; rejects empty name; UNIQUE
  (project_id, name) collisions surface as ErrConflict via mapWriteErr.
- GetFrame is project-scoped — wrong-project read returns ErrNotFound.
- UpdateFrame applies a partial; project_id is not exposed (moving a
  frame across projects would orphan its devices).
- DeleteFrame relies on the schema's ON DELETE SET NULL to drop
  devices' frame_id refs cleanly; verified by test.

Device store:
- CreateDevice defaults color to #1e1e1e if blank; rejects empty name,
  non-positive size; validates frame_id is in the same project (returns
  ErrInvalidInput on cross-project ref).
- UpdateDevice uses a FrameRef tri-state for frame_id so callers can
  distinguish "leave alone" from "clear to NULL" from "move to frame X".
- Cross-project frame_id on PATCH is rejected with ErrInvalidInput.
- ListDevices supports an optional frame_id filter.

13 new table-driven tests, all green with -race.
2026-05-15 18:16:33 +02:00
mAi
d3b660d140 merge: image moved to m/mcables namespace
mAi got admin on m/mCables but Gitea container packages are
user-namespace-scoped — repo-collab perm is insufficient. Pushed
once using m's ~/.netrc token, deleted the mAi/mcables stub.

Compose now references mgit.msbls.de/m/mcables:latest.
2026-05-15 18:12:37 +02:00
mAi
dc5fafeaa8 deploy: image now under m/ namespace on mgit.msbls.de
m granted mAi admin on m/mCables, but Gitea's container registry is
user-namespace-scoped (not repo-collab-scoped) so the push had to go
through m's own credentials for this one administrative move:

    docker login mgit.msbls.de -u m -p <m's token>
    docker push mgit.msbls.de/m/mcables:latest

Image digest sha256:76624f17… is identical to the one previously living
at mgit.msbls.de/mai/mcables:latest — same build, just retagged.

Drops the workaround comment from the compose file. The mai/mcables
package will be deleted via API after the deploy verifies.
2026-05-15 18:11:31 +02:00
mAi
017a77e187 merge: deploy infra to mDock (pulled forward from §10)
picasso shipped (commit 8a31f0a on mai/picasso/deploy-mdock):
- Dockerfile: multi-stage golang:1.23-alpine -> distroless/static
- docker-compose.yml at repo root (raw-docker pattern, not Dokploy)
- .dockerignore
- README deploy section

Live: http://mdock:7777 (image sha256:76624f17, 12.2MB).
Persistence verified across compose restart.

Note: mAi lacks write on m/ in Gitea, so image lives at
mgit.msbls.de/mAi/mcables:latest. m can retag once mAi gets write
on m/mCables (see docker-compose.yml comment).
2026-05-15 18:02:09 +02:00
mAi
8a31f0af60 deploy: Dockerfile + docker-compose.yml for mDock, manual first roll
Pulls the deploy infra forward from §10 so m can see slice 1 on his LAN.

- Dockerfile: multi-stage golang:1.25-alpine → distroless/static-debian12.
  CGO_ENABLED=0 (modernc.org/sqlite is pure Go). USER 1000:1000 so the
  bind-mount on mDock (owned by m:m) is writable without chowning the
  host dir. -trimpath + -s -w; 12.2MB final image.
- docker-compose.yml: matches the mDock convention surveyed earlier
  (container_name explicit, restart: unless-stopped, env_file in
  /home/m/secrets/mcables/.env, bind-mount /home/m/stacks/mcables/data,
  port 7777 exposed on LAN). Image temporarily under the mai/ namespace
  on mgit.msbls.de because mAi doesn't have write access to m/* today —
  documented in a comment so retagging is one line when permissions land.
- .dockerignore: keeps .git, .worktrees, .m, data/, docs/, *.md,
  editor cruft out of the build context.

Manual deploy verified end-to-end:
- docker build → image sha256:76624f17 (12.2MB)
- mAi-authenticated push to mgit.msbls.de/mai/mcables:latest
- ssh mdock anonymous pull works (registry allows public reads on this
  namespace)
- POST /api/projects {"name":"LOFT"} returns the row, GET /api/projects
  shows it; docker compose restart preserves it on disk; second GET
  still shows LOFT.

Gitea Actions auto-deploy left for a follow-up task per the head's
instruction — gets us the moving parts right first.
2026-05-15 18:01:30 +02:00
mAi
98f30306a1 merge: slice 1 — bootstrap + project CRUD + global cable_types
picasso shipped (7 commits @ 905c75c):
- Go module + cmd/mcables binary
- internal/db: migrations runner + 001_init.sql (full v3 schema, 5 cable_types seeded)
- internal/db/store.go: projects + cable_types CRUD with sentinel errors
- internal/server: net/http handlers (Go 1.22 ServeMux)
- web/static: project picker, legend, modals (new project / cable type / delete), ?project= URL state
- 17 store tests green, end-to-end smoke verified

Endpoints live: /api/healthz, /api/projects {GET POST}, /api/projects/:id
{GET PATCH DELETE?confirm=<name>}, /api/cable-types {GET POST}, /api/cable-types/:id {PATCH DELETE}.

Next: slice 2 (frames + devices + drag-to-position) on m's go.
2026-05-15 16:50:02 +02:00
mAi
905c75c6db test+docs: store coverage + README for slice 1
Adds table-driven store tests:
- projects: drawing_name auto-default, explicit-name accept, empty-name
  reject, duplicate-name conflict, ordered list, GetProject not-found,
  partial PATCH semantics, blank-drawing-name re-default on PATCH,
  ?confirm=<name> guardrail (wrong / empty / correct), snapshot returns
  the 5 globally-seeded cable_types
- cable_types: 5 seeded with the legend colours, global UNIQUE(name),
  rename + recolour, RESTRICT-blocked delete when a cable references the
  type (with count surfaced via CountCablesUsingType), unused delete
  succeeds, project deletion does NOT cascade into cable_types

go test -race ./... passes. Updates README.md with run instructions,
env vars, the slice-1 API surface, and the slice roadmap.
2026-05-15 16:48:29 +02:00
mAi
c13000ee7e feat: frontend shell — project picker, legend, modals (new project / cable type / delete), URL ?project= state 2026-05-15 16:45:29 +02:00
mAi
1e3988161b feat: http server — net/http (Go 1.22 ServeMux), /api/healthz + projects + cable-types, JSON errors 2026-05-15 16:45:29 +02:00
mAi
255d52e7c4 feat: db store — projects + cable_types CRUD with sentinel errors and confirm-name guardrail 2026-05-15 16:45:29 +02:00
mAi
cd34dde133 feat: db migrations runner + 001_init.sql (full v3 schema, 5 cable_types seeded) 2026-05-15 16:45:29 +02:00
mAi
b6eb29a103 chore: untrack .m/ worker-local mai event log 2026-05-15 16:40:41 +02:00
mAi
e55993ca53 bootstrap: go module, skeleton dirs, Makefile, main.go entrypoint 2026-05-15 16:40:14 +02:00
mAi
14f0d74e44 merge: design v3 (framework, multi-project, mDock deploy)
inventor shift done by picasso. docs/design.md (760 lines) + CLAUDE.md
locked. m approved coder shift.

Next: slice 1 (bootstrap + project CRUD) on mai/picasso/slice-1.
2026-05-15 16:38:02 +02:00
mAi
c690113ea1 docs: design v3 — global cable_types, device UNIQUE, delete guardrail
Tight pass on round-4 answers (single commit per head's request):

- cable_types is GLOBAL — drop project_id, UNIQUE(name). Migration 001
  seeds the 5 defaults once; POST /api/projects no longer seeds them.
  API moves to top-level /api/cable-types. Renaming/recolouring affects
  every project. CASCADE from projects does not touch cable_types.
- devices: UNIQUE (project_id, name) added.
- projects: drawing_name defaults to "<name>.excalidraw" server-side
  on POST when omitted; editable via PATCH.
- DELETE /api/projects/:pid requires ?confirm=<name>; server checks
  name match, returns 400 if missing or mismatched.
- io_markers: no type_id (Power-by-convention, UI soft-warn). Confirmed
  v0 stance.
- Bundles ignored on export — carries over from v2.
- §0 changelog rewritten as "what changed in v3 / what carried over".
- §2 schema rewritten; FK-shape paragraph updated to call out the one
  global table.
- §3 endpoints: cable-types moved to top level; POST/DELETE projects
  show new defaults + guardrail semantics.
- §4 export table notes cable_types pulled from global.
- §7 "edit cable type" flow gains the cross-project-effect banner +
  ON DELETE RESTRICT inline-error UX.
- §8 slice 1 rewritten: no per-project seeding; legend reads global.
- §9 all six v2 questions marked resolved with the v3 answer per item.
- Trailer changes to "DESIGN v3 READY — coder shift gated".
- CLAUDE.md mirrors: global cable_types, device UNIQUE per project,
  drawing_name default, delete guardrail.
2026-05-15 16:32:20 +02:00
mAi
023bf82dbd docs: CLAUDE.md reflects v2 — framework, multi-project, mDock deploy
Sync project instructions with design v2:

- Framework framing: top-level `projects` table, LOFT/OFFICE/… as
  separate projects, frames as sub-zones inside a project.
- DB path moved from ~/.m/mcables.db to ./data/mcables.db (gitignored).
- Frontend stack locked: vanilla JS modules + SVG, no build step,
  TypeScript types via JSDoc, Preact-via-CDN-ESM as fallback.
- Deploy: raw docker on mDock under /home/m/stacks/mcables/ — explicitly
  NOT Dokploy. Port 7777, no reverse proxy, no auth (LAN-trusted).
- mExDraw access: raw HTTP API (mcp__mexdraw__* not configured for this
  project), one-way export only.
- Seed drawing reframed as visual-grammar reference, NOT a runtime
  import target. IO markers documented as wall-outlet terminators
  (type=Power), not inter-frame bridges.
- Out-of-scope list updated: no auth, no inventory fields, no runtime
  import. Worker-preference slices re-aligned with the new design.
2026-05-15 16:13:34 +02:00
mAi
b734e7f874 docs: design v2 — framework rescope, mDock deploy, no runtime importer
Revision after m's answers (2026-05-15):

- mCables is a framework. Top-level `projects` table; LOFT and OFFICE
  are separate projects, each backed by one drawing. project_id is
  denormalised onto every row for cheap project-scoped queries; CASCADE
  from projects wipes a project's whole subgraph.
- IO diamonds are wall-outlet terminators (type=Power), not inter-frame
  bridges. paired_with_id removed.
- No runtime importer. The seed Cable-Management.excalidraw is the
  visual-grammar reference for the exporter only. /api/sync/import is
  dropped from MVP; only /api/sync/export remains (one-way, manual).
- No cable inventory fields. Strictly visual structure for v0.
- DB at ./data/mcables.db (project-local, gitignored).
- Deploy: raw docker on mDock under /home/m/stacks/mcables/ (NOT Dokploy).
  Conventions verified live (mgreen, mgeo, msports-garmin patterns).
  Port 7777, container_name mcables, image from Gitea registry, Gitea
  Actions self-hosted runner builds + deploys on push to main.
- Bind 0.0.0.0:7777 on the LAN. No auth.
- UI gains a projects picker; all CRUD endpoints scoped under
  /api/projects/:pid/.
- Slices re-planned: empty bootstrap → frame+device → port+cable →
  IO+cable-type editing → export.
- Open questions trimmed; six new ones (drawing-name policy, device
  uniqueness, non-Power IO, bundle export, cross-project cable types,
  delete guardrail).

Ends with DESIGN v2 READY FOR REVIEW.
2026-05-15 16:13:24 +02:00
mAi
f429111462 docs: design pass 1 — schema, API, importer, sync, slices
Inventor shift 1. Reads the live Cable-Management.excalidraw on
mxdrw.msbls.de, lands on:

- vanilla JS modules + SVG diagram (no build step) served by a single
  Go binary with embed.FS
- SQLite schema with frames, devices, ports, cable_types, cables,
  io_markers, bundles (cgo-free driver)
- HTTP API under /api/, /api/state as the editor's one-shot loader
- importer that respects port-on-edge geometry (ports are positional,
  not containerId-bound) and resolves arrow endpoints to port/device/IO
- bundle detection MVP: same-endpoints-pair → suggestion
- sync model: DB authoritative, Excalidraw is a projection, manual
  import/export buttons with element-ID stability and conflict flagging
- five vertical slices for the coder shift, smallest end-to-end first
- nine open questions for m before code starts

Ends with DESIGN READY FOR REVIEW.
2026-05-15 12:26:27 +02:00
m
a94e8d9f60 bootstrap: README + CLAUDE.md (architecture sketch, no CLI)
Visual interface + SQLite inventory + mExDraw integration. Seeded from m's existing Cable-Management.excalidraw. Backend Go, frontend TBD.
2026-05-15 12:17:55 +02:00
m
eaf2d968dd Initial commit 2026-05-15 10:16:28 +00:00