Files
paliad/internal/handlers/scenario_flags.go
mAi d36cc9ee15 feat(deadline-system): P0 — per-project scenario_flags SSoT (m/paliad#149)
Phase 2 P0 of the deadline + procedural-events revision. Establishes
paliad.projects.scenario_flags (jsonb) + paliad.scenario_flag_catalog as
the single source of truth for per-project scenario state — replacing
the three fragmented stores athena flagged (project_event_choices,
scenarios.spec, DOM-only). All three were empty per the audit so no
data migration is needed.

The jsonb map carries two key shapes:

  * named flags (whitelist via scenario_flag_catalog) — today
    with_ccr / with_amend / with_cci
  * per-rule selection deviations of shape "rule:<uuid>" — wired up
    here for validation; the consumer UI lands in P3

Endpoints:

  GET   /api/projects/{id}/scenario-flags
  PATCH /api/projects/{id}/scenario-flags

PATCH semantics: bool = write; null = delete (priority-driven default
returns); missing key = leave alone. The service validates every key
on write (catalog lookup + UUID rule-membership + mandatory-cannot-be-
deselected) before persisting, so a single bad key fails the whole
patch.

Frontend bind: new scenario-flags.ts client module + Mode B's flag
checkboxes (ccr-flag / inf-amend-flag / rev-amend-flag / rev-cci-flag)
now hydrate from / persist to the project's scenario_flags on every
toggle. Kontextfrei (no project) is unchanged. Cross-surface coherence
via a scenario-flag-changed CustomEvent (peer surfaces — Verfahrens-
ablauf strip, Mode B result-view — will subscribe in P3).

Mig 154 is audit-defensive (set_config of paliad.audit_reason); no
audit trigger fires on paliad.projects today but a future one will
inherit the reason. Seeds the three known flags. CHECK constraints
enforce the top-level shape (jsonb_typeof = 'object') and the
catalog key pattern (lowercase, not 'rule:%' prefix).

Verified against the live DB: 18 projects default to '{}', catalog
has 3 rows, applied_migrations advanced to 154.

Design: docs/design-deadline-system-revision-2026-05-27.md §2.3, §2.4a,
§4.1, §5 (P0 row). t-paliad-331.
2026-05-27 15:02:01 +02:00

86 lines
2.2 KiB
Go

package handlers
import (
"encoding/json"
"net/http"
"github.com/google/uuid"
)
// GET /api/projects/{id}/scenario-flags returns the project's current
// flag map and the catalog. See ScenarioFlagsService.Get for semantics.
func handleGetScenarioFlags(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
view, err := dbSvc.scenarioFlags.Get(r.Context(), uid, id)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, view)
}
// PATCH /api/projects/{id}/scenario-flags merges a partial delta into
// the project's scenario_flags. Body shape:
//
// { "with_ccr": true, "with_amend": null, "rule:<uuid>": false }
//
// `null` deletes a key from the map so the priority-driven default
// returns; bool values are persisted verbatim.
func handlePatchScenarioFlags(w http.ResponseWriter, r *http.Request) {
if !requireDB(w) {
return
}
uid, ok := requireUser(w, r)
if !ok {
return
}
id, err := uuid.Parse(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
// Decode as map[string]*bool so JSON null cleanly resolves to nil
// (= delete the key) while bool literals stay distinguishable from
// the zero value.
var raw map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"})
return
}
delta := make(map[string]*bool, len(raw))
for k, v := range raw {
if len(v) == 0 || string(v) == "null" {
delta[k] = nil
continue
}
var b bool
if err := json.Unmarshal(v, &b); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{
"error": "scenario-flag values must be bool or null (got non-bool for key " + k + ")",
})
return
}
bv := b
delta[k] = &bv
}
view, err := dbSvc.scenarioFlags.Patch(r.Context(), uid, id, delta)
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusOK, view)
}