Files
CableGUI/internal/server/server.go
mAi c61bff7cf2 feat(backend): ports CRUD endpoints for slice 7
New store methods on internal/db/ports.go:
- CreatePort / GetPort / UpdatePort / DeletePort (all project-scoped)
- ListPortsForDevice for the inspector's per-device list

New handlers (internal/server/ports.go):
- GET    /api/projects/:pid/devices/:id/ports
- POST   /api/projects/:pid/devices/:id/ports  ← {type_id, label?, x_offset, y_offset}
- PATCH  /api/projects/:pid/ports/:id           ← partial
- DELETE /api/projects/:pid/ports/:id          (cables ref → ON DELETE SET NULL)

Lets slice 7's +Port tool add/remove instance ports without going
through the type-seeded auto-creation path from slice 4.
2026-05-16 01:10:59 +02:00

117 lines
5.5 KiB
Go

// Package server wires the HTTP API + the embedded frontend onto a
// single net/http handler. Routes use Go 1.22 ServeMux pattern matching
// (no router framework).
package server
import (
"io/fs"
"net/http"
"mgit.msbls.de/m/mcables/internal/db"
)
// New returns an http.Handler serving the mCables API at /api/ and the
// embedded frontend at /. The frontend FS should be rooted such that
// "index.html" is at its root.
func New(store *db.Store, frontend fs.FS) http.Handler {
mux := http.NewServeMux()
h := &handlers{store: store}
// Health
mux.HandleFunc("GET /api/healthz", h.healthz)
// Projects
mux.HandleFunc("GET /api/projects", h.listProjects)
mux.HandleFunc("POST /api/projects", h.createProject)
mux.HandleFunc("GET /api/projects/{pid}", h.getProject)
mux.HandleFunc("PATCH /api/projects/{pid}", h.patchProject)
mux.HandleFunc("DELETE /api/projects/{pid}", h.deleteProject)
// Cable types (global)
mux.HandleFunc("GET /api/cable-types", h.listCableTypes)
mux.HandleFunc("POST /api/cable-types", h.createCableType)
mux.HandleFunc("PATCH /api/cable-types/{id}", h.patchCableType)
mux.HandleFunc("DELETE /api/cable-types/{id}", h.deleteCableType)
// Frames (project-scoped)
mux.HandleFunc("GET /api/projects/{pid}/frames", h.listFrames)
mux.HandleFunc("POST /api/projects/{pid}/frames", h.createFrame)
mux.HandleFunc("PATCH /api/projects/{pid}/frames/{id}", h.patchFrame)
mux.HandleFunc("DELETE /api/projects/{pid}/frames/{id}", h.deleteFrame)
// Devices (project-scoped)
mux.HandleFunc("GET /api/projects/{pid}/devices", h.listDevices)
mux.HandleFunc("POST /api/projects/{pid}/devices", h.createDevice)
mux.HandleFunc("PATCH /api/projects/{pid}/devices/{id}", h.patchDevice)
mux.HandleFunc("DELETE /api/projects/{pid}/devices/{id}", h.deleteDevice)
// IO markers (project-scoped) — wall-outlet terminators
mux.HandleFunc("GET /api/projects/{pid}/io-markers", h.listIOMarkers)
mux.HandleFunc("POST /api/projects/{pid}/io-markers", h.createIOMarker)
mux.HandleFunc("PATCH /api/projects/{pid}/io-markers/{id}", h.patchIOMarker)
mux.HandleFunc("DELETE /api/projects/{pid}/io-markers/{id}", h.deleteIOMarker)
// Ports — slice 7 lets m add/edit/remove instance ports on a device.
mux.HandleFunc("GET /api/projects/{pid}/devices/{id}/ports", h.listPortsForDevice)
mux.HandleFunc("POST /api/projects/{pid}/devices/{id}/ports", h.createPort)
mux.HandleFunc("PATCH /api/projects/{pid}/ports/{id}", h.patchPort)
mux.HandleFunc("DELETE /api/projects/{pid}/ports/{id}", h.deletePort)
// Device-type catalog. Built-ins are read-only; project-custom rows
// support full CRUD scoped to the project.
mux.HandleFunc("GET /api/device-types", h.listBuiltInDeviceTypes)
mux.HandleFunc("GET /api/projects/{pid}/device-types", h.listDeviceTypes)
mux.HandleFunc("POST /api/projects/{pid}/device-types", h.createDeviceType)
mux.HandleFunc("PATCH /api/projects/{pid}/device-types/{id}", h.patchDeviceType)
mux.HandleFunc("DELETE /api/projects/{pid}/device-types/{id}", h.deleteDeviceType)
// Connection requirements — the solver's per-project input.
mux.HandleFunc("GET /api/projects/{pid}/connection-requirements", h.listConnectionRequirements)
mux.HandleFunc("POST /api/projects/{pid}/connection-requirements", h.createConnectionRequirement)
mux.HandleFunc("PATCH /api/projects/{pid}/connection-requirements/{id}", h.patchConnectionRequirement)
mux.HandleFunc("DELETE /api/projects/{pid}/connection-requirements/{id}", h.deleteConnectionRequirement)
// Cables — slice 6: solver writes here with auto=1; slice 7 lets m
// hand-draw with auto=0. PATCH supports `promote: true` to flip auto→0.
mux.HandleFunc("GET /api/projects/{pid}/cables", h.listCables)
mux.HandleFunc("POST /api/projects/{pid}/cables", h.createCable)
mux.HandleFunc("PATCH /api/projects/{pid}/cables/{id}", h.patchCable)
mux.HandleFunc("DELETE /api/projects/{pid}/cables/{id}", h.deleteCable)
// Bundles — manual + auto.
mux.HandleFunc("GET /api/projects/{pid}/bundles", h.listBundles)
mux.HandleFunc("POST /api/projects/{pid}/bundles", h.createBundle)
mux.HandleFunc("PATCH /api/projects/{pid}/bundles/{id}", h.patchBundle)
mux.HandleFunc("DELETE /api/projects/{pid}/bundles/{id}", h.deleteBundle)
// Solver + quick-fix combo + setup templates.
mux.HandleFunc("POST /api/projects/{pid}/solve", h.solve)
mux.HandleFunc("POST /api/projects/{pid}/devices/{id}/ports-and-resolve", h.portsAndResolve)
mux.HandleFunc("GET /api/setup-templates", h.listSetupTemplates)
mux.HandleFunc("POST /api/projects/{pid}/apply-template", h.applyTemplate)
// Frontend (embedded). Serve "/" → index.html via http.FileServerFS.
// Wrap in noCache so the browser revalidates with the ETag/Last-Modified
// the file server already emits — without this, browsers cache aggressively
// and m sees the old main.js after every redeploy until hard-reload.
mux.Handle("/", noCache(http.FileServerFS(frontend)))
return mux
}
// noCache wraps a static handler so each response carries
// Cache-Control: no-cache. Combined with the ETag/Last-Modified headers
// http.FileServer(FS) already emits, this turns every fetch into a
// cheap revalidation request — the browser uses its cached body when
// the ETag matches but always asks first, so freshly-built assets show
// up on the next page load without a hard-reload.
//
// Applied to the static-asset handler only — API responses write their
// own headers and aren't routed through this.
func noCache(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-cache")
h.ServeHTTP(w, r)
})
}