diff --git a/internal/server/clamps.go b/internal/server/clamps.go new file mode 100644 index 0000000..f5719b6 --- /dev/null +++ b/internal/server/clamps.go @@ -0,0 +1,195 @@ +package server + +import ( + "encoding/json" + "errors" + "net/http" + + "mgit.msbls.de/m/mcables/internal/db" +) + +type clampCreate struct { + X float64 `json:"x"` + Y float64 `json:"y"` + Label string `json:"label,omitempty"` + FrameID json.RawMessage `json:"frame_id,omitempty"` +} + +type clampPatch struct { + X *float64 `json:"x,omitempty"` + Y *float64 `json:"y,omitempty"` + Label *string `json:"label,omitempty"` + FrameID json.RawMessage `json:"frame_id,omitempty"` +} + +type cableClampAttach struct { + ClampID int64 `json:"clamp_id"` + Ord int `json:"ord,omitempty"` +} + +type cableClampReorder struct { + ClampIDs []int64 `json:"clamp_ids"` +} + +func (h *handlers) listClamps(w http.ResponseWriter, r *http.Request) { + pid, ok := parseInt64Path(r, "pid") + if !ok { + writeError(w, db.ErrInvalidInput, "pid must be a positive integer") + return + } + cs, err := h.store.ListClamps(pid) + if err != nil { + writeError(w, err, nil) + return + } + writeJSON(w, http.StatusOK, cs) +} + +func (h *handlers) createClamp(w http.ResponseWriter, r *http.Request) { + pid, ok := parseInt64Path(r, "pid") + if !ok { + writeError(w, db.ErrInvalidInput, "pid must be a positive integer") + return + } + var body clampCreate + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, errors.Join(db.ErrInvalidInput, err), nil) + return + } + ref, err := parseFrameRef(body.FrameID) + if err != nil { + writeError(w, errors.Join(db.ErrInvalidInput, err), nil) + return + } + c, err := h.store.CreateClamp(pid, db.ClampCreate{ + X: body.X, Y: body.Y, Label: body.Label, FrameID: ref.ID, + }) + if err != nil { + writeError(w, err, nil) + return + } + writeJSON(w, http.StatusCreated, c) +} + +func (h *handlers) patchClamp(w http.ResponseWriter, r *http.Request) { + pid, ok := parseInt64Path(r, "pid") + if !ok { + writeError(w, db.ErrInvalidInput, "pid must be a positive integer") + return + } + id, ok := parseInt64Path(r, "id") + if !ok { + writeError(w, db.ErrInvalidInput, "id must be a positive integer") + return + } + var body clampPatch + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, errors.Join(db.ErrInvalidInput, err), nil) + return + } + ref, err := parseFrameRef(body.FrameID) + if err != nil { + writeError(w, errors.Join(db.ErrInvalidInput, err), nil) + return + } + c, err := h.store.UpdateClamp(pid, id, db.ClampUpdate{ + X: body.X, Y: body.Y, Label: body.Label, FrameID: ref, + }) + if err != nil { + writeError(w, err, nil) + return + } + writeJSON(w, http.StatusOK, c) +} + +func (h *handlers) deleteClamp(w http.ResponseWriter, r *http.Request) { + pid, ok := parseInt64Path(r, "pid") + if !ok { + writeError(w, db.ErrInvalidInput, "pid must be a positive integer") + return + } + id, ok := parseInt64Path(r, "id") + if !ok { + writeError(w, db.ErrInvalidInput, "id must be a positive integer") + return + } + if err := h.store.DeleteClamp(pid, id); err != nil { + writeError(w, err, nil) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// POST /api/projects/:pid/cables/:cid/clamps — attach a clamp to a cable. +func (h *handlers) attachClampToCable(w http.ResponseWriter, r *http.Request) { + pid, ok := parseInt64Path(r, "pid") + if !ok { + writeError(w, db.ErrInvalidInput, "pid must be a positive integer") + return + } + cid, ok := parseInt64Path(r, "cid") + if !ok { + writeError(w, db.ErrInvalidInput, "cid must be a positive integer") + return + } + var body cableClampAttach + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, errors.Join(db.ErrInvalidInput, err), nil) + return + } + cc, err := h.store.AttachClampToCable(pid, cid, body.ClampID, body.Ord) + if err != nil { + writeError(w, err, nil) + return + } + writeJSON(w, http.StatusCreated, cc) +} + +// DELETE /api/projects/:pid/cables/:cid/clamps/:cmid — detach a clamp. +func (h *handlers) detachClampFromCable(w http.ResponseWriter, r *http.Request) { + pid, ok := parseInt64Path(r, "pid") + if !ok { + writeError(w, db.ErrInvalidInput, "pid must be a positive integer") + return + } + cid, ok := parseInt64Path(r, "cid") + if !ok { + writeError(w, db.ErrInvalidInput, "cid must be a positive integer") + return + } + cmid, ok := parseInt64Path(r, "cmid") + if !ok { + writeError(w, db.ErrInvalidInput, "cmid must be a positive integer") + return + } + if err := h.store.DetachClampFromCable(pid, cid, cmid); err != nil { + writeError(w, err, nil) + return + } + w.WriteHeader(http.StatusNoContent) +} + +// PUT /api/projects/:pid/cables/:cid/clamps — replace clamp sequence. +func (h *handlers) reorderCableClamps(w http.ResponseWriter, r *http.Request) { + pid, ok := parseInt64Path(r, "pid") + if !ok { + writeError(w, db.ErrInvalidInput, "pid must be a positive integer") + return + } + cid, ok := parseInt64Path(r, "cid") + if !ok { + writeError(w, db.ErrInvalidInput, "cid must be a positive integer") + return + } + var body cableClampReorder + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + writeError(w, errors.Join(db.ErrInvalidInput, err), nil) + return + } + out, err := h.store.ReorderCableClamps(pid, cid, body.ClampIDs) + if err != nil { + writeError(w, err, nil) + return + } + writeJSON(w, http.StatusOK, out) +} diff --git a/internal/server/server.go b/internal/server/server.go index b89f4c8..9c66c52 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -93,6 +93,15 @@ func New(store *db.Store, frontend fs.FS) http.Handler { // Slice 8 — export to mxdrw.msbls.de mux.HandleFunc("POST /api/projects/{pid}/sync/export", h.syncExport) + // v5 — clamps + cable routing. + mux.HandleFunc("GET /api/projects/{pid}/clamps", h.listClamps) + mux.HandleFunc("POST /api/projects/{pid}/clamps", h.createClamp) + mux.HandleFunc("PATCH /api/projects/{pid}/clamps/{id}", h.patchClamp) + mux.HandleFunc("DELETE /api/projects/{pid}/clamps/{id}", h.deleteClamp) + mux.HandleFunc("POST /api/projects/{pid}/cables/{cid}/clamps", h.attachClampToCable) + mux.HandleFunc("PUT /api/projects/{pid}/cables/{cid}/clamps", h.reorderCableClamps) + mux.HandleFunc("DELETE /api/projects/{pid}/cables/{cid}/clamps/{cmid}", h.detachClampFromCable) + // 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