mAi e53f4b1a5e docs(debug): sherlock frame-select — not a regression, render-order clutter
Playwright drives the deployed image four ways. The frame <rect> has its
own pointerdown handler that calls startDrag; startDrag calls
e.stopPropagation() before the pan-start handler runs; the selector at
main.js:2087 already includes [data-frame-id]. Click that lands on the
rect → frame selected, inspector switches to the Frame panel. Verified.

m's complaint is real but cause is render order: frames paint first;
devices, ports, cables, clamps, IO markers all paint on top. Any click
on the frame interior that happens to land on one of those elements
hits that element, not the frame. For LOFT's big 'Entertainment' frame
the visible canvas portion is ~180×424 px and partly covered by yellow
cable polylines.

Not a one-line bug. Three UX options ordered by effort in the doc:
make the frame label the selection grip (drop pointer-events:none + add
pointerdown), parent-frame breadcrumb on the device inspector, modifier
key to escape to the enclosing frame. None applied — picasso/perseus
call.
2026-05-16 19:30:08 +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%