Four endpoints for the per-user dashboard layout: - GET /api/me/dashboard-layout (auto-seeds factory on first call) - PUT /api/me/dashboard-layout (validates against catalog) - POST /api/me/dashboard-layout/reset (overwrites with factory default) - GET /api/dashboard-widget-catalog (catalog metadata for the picker) Catalog endpoint is DB-independent by design — knowledge-platform-only deployments (no DATABASE_URL) still surface the widget metadata. The layout endpoints 503 when the service is unwired, matching the pattern established by handleListCardLayouts / handleListPinnedProjects. Wired through services.Services → handlers.dbServices via the DashboardLayout field. main.go gains a single NewDashboardLayoutService call next to NewCardLayoutService. ErrInvalidInput from the service maps to 400; everything else flows through writeServiceError for the existing 500/503 fallthrough. go build + go vet + go test ./internal/services/ -short all clean.
110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
package handlers
|
|
|
|
// HTTP handlers for the per-user dashboard layout (t-paliad-219 Slice A2).
|
|
//
|
|
// Design: docs/design-dashboard-configurable-2026-05-20.md §9.
|
|
//
|
|
// Four endpoints:
|
|
// GET /api/me/dashboard-layout → read (auto-seeds factory default)
|
|
// PUT /api/me/dashboard-layout → replace (validates against catalog)
|
|
// POST /api/me/dashboard-layout/reset → overwrite with factory default
|
|
// GET /api/dashboard-widget-catalog → catalog metadata for the picker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"mgit.msbls.de/m/paliad/internal/services"
|
|
)
|
|
|
|
// GET /api/me/dashboard-layout — returns the caller's layout, seeding the
|
|
// factory default on first call. Always returns 200 with a valid
|
|
// DashboardLayoutSpec.
|
|
func handleGetDashboardLayout(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if dbSvc.dashboardLayout == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "dashboard-layout service not configured"})
|
|
return
|
|
}
|
|
spec, err := dbSvc.dashboardLayout.GetOrSeed(r.Context(), uid)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, spec)
|
|
}
|
|
|
|
// PUT /api/me/dashboard-layout — replaces the caller's layout. Body must
|
|
// be a complete DashboardLayoutSpec; the service validates against the
|
|
// catalog and 400s on a bad spec.
|
|
func handlePutDashboardLayout(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if dbSvc.dashboardLayout == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "dashboard-layout service not configured"})
|
|
return
|
|
}
|
|
var spec services.DashboardLayoutSpec
|
|
if err := json.NewDecoder(r.Body).Decode(&spec); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
|
|
return
|
|
}
|
|
out, err := dbSvc.dashboardLayout.Update(r.Context(), uid, spec)
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrInvalidInput) {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// POST /api/me/dashboard-layout/reset — overwrites the caller's layout
|
|
// with the factory default. The previous layout is discarded.
|
|
func handleResetDashboardLayout(w http.ResponseWriter, r *http.Request) {
|
|
if !requireDB(w) {
|
|
return
|
|
}
|
|
uid, ok := requireUser(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if dbSvc.dashboardLayout == nil {
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "dashboard-layout service not configured"})
|
|
return
|
|
}
|
|
spec, err := dbSvc.dashboardLayout.ResetToDefault(r.Context(), uid)
|
|
if err != nil {
|
|
writeServiceError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, spec)
|
|
}
|
|
|
|
// GET /api/dashboard-widget-catalog — returns the widget catalog. Auth-
|
|
// gated only because the catalog includes user-facing copy; nothing
|
|
// security-sensitive is exposed. The handler is DB-independent (the
|
|
// catalog is code-resident) so the requireDB gate is intentionally
|
|
// skipped — knowledge-platform-only deployments can still surface the
|
|
// catalog and we never want this endpoint to 503.
|
|
func handleGetWidgetCatalog(w http.ResponseWriter, r *http.Request) {
|
|
if _, ok := requireUser(w, r); !ok {
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, services.WidgetCatalog())
|
|
}
|