// 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) // Slice 8 — export to mxdrw.msbls.de mux.HandleFunc("POST /api/projects/{pid}/sync/export", h.syncExport) // 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) }) }