m: 'We should also be able to resize frames, the same way we do with
devices.' Mirrors the device-resize pattern (89686d0).
- 10×10 SVG handle drawn at each frame's bottom-right corner with class
.frame-resize-handle + cursor: nwse-resize. Appended after the label
so it sits on top of the rect and wins the pointerdown.
- startFrameResize captures the pointer, stops propagation so the
rect's pointerdown (= startDrag 'frame') doesn't also fire, and
updates f.width / f.height on every pointermove using svgPoint
deltas — works at any zoom level via the same world-coord conversion
the rest of the canvas uses.
- Clamps to 200×150 minimum during the drag (frames need more room
than devices since they host devices + IO markers + clamps).
- On pointerup: PATCH /api/projects/:pid/frames/:id with the new width
+ height. Contained children stay at their absolute positions — the
frame body drag is what moves them; resize only changes the frame's
own bounds, so devices/IO markers/clamps inside don't shift.
CableGUI
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 CableGUI
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/cablegui
# open http://localhost:7777
Or built:
make build
./bin/cablegui
The binary serves the frontend from an embedded web/static/ and the
JSON API under /api/. SQLite lives at ./data/cablegui.db by default.
Environment
| Var | Default | Notes |
|---|---|---|
CABLEGUI_ADDR |
0.0.0.0:7777 |
Listen address. |
CABLEGUI_DB |
./data/cablegui.db |
SQLite path. Parent dir is created on boot. |
MEXDRAW_BASE_URL |
https://mxdrw.msbls.de |
Base URL for mExDraw export. |
MEXDRAW_USER |
(unset) | Username for the mxdrw HTTP Basic Auth on export. Required. |
MEXDRAW_PASS |
(unset) | Password for the mxdrw HTTP Basic Auth on export. Required. |
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
CableGUI runs on mDock at http://mdock:7777 as a docker-compose
service under /home/m/stacks/cablegui/. Pattern matches the other
mDock services (mgreen-journal, mgeo, msports-garmin, …) — no Dokploy,
no reverse proxy, LAN-trusted.
Manual deploy
-
Build + push the image (image now lives under
m/in Gitea):docker build -t mgit.msbls.de/m/cablegui:latest . awk '/machine mgit.msbls.de/{getline; getline; print $2}' ~/.netrc \ | docker login mgit.msbls.de -u m --password-stdin docker push mgit.msbls.de/m/cablegui:latest -
Prepare directories on mDock (one-time):
ssh mdock 'mkdir -p /home/m/stacks/cablegui/data /home/m/secrets/cablegui \ && touch /home/m/secrets/cablegui/.env \ && chmod 0600 /home/m/secrets/cablegui/.env' scp docker-compose.yml mdock:/home/m/stacks/cablegui/docker-compose.yml -
Pull + start:
ssh mdock 'cd /home/m/stacks/cablegui && docker compose pull && docker compose up -d' -
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/cablegui && docker compose pull && docker compose up -d'.
Persistence
SQLite lives at /home/m/stacks/cablegui/data/cablegui.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.