diff --git a/internal/handlers/fristenrechner.go b/internal/handlers/fristenrechner.go index 95e0ca4..7b107af 100644 --- a/internal/handlers/fristenrechner.go +++ b/internal/handlers/fristenrechner.go @@ -9,10 +9,29 @@ import ( ) // Fristenrechner page handler: serves the static HTML. No DB dependency. +// +// Back-compat: the pre-split sidebar entry for "Verfahrensablauf" pointed at +// /tools/fristenrechner?path=a. After the t-paliad-179 split, that landing is +// owned by /tools/verfahrensablauf. A naked ?path=a (no Akte context — i.e. +// no ?project=) is the bookmarked-legacy-entry case → 302 to the new route. +// ?project=&path=a is the Akte-mode internal wizard pathway and stays +// on /tools/fristenrechner so the wizard state survives a refresh. func handleFristenrechnerPage(w http.ResponseWriter, r *http.Request) { + q := r.URL.Query() + if q.Get("path") == "a" && q.Get("project") == "" { + http.Redirect(w, r, "/tools/verfahrensablauf", http.StatusFound) + return + } http.ServeFile(w, r, "dist/fristenrechner.html") } +// Verfahrensablauf page handler (t-paliad-179 Slice 1): the dedicated +// abstract-browse surface for procedural shape. No DB dependency — the page +// shell is static HTML; the calculator API still drives the timeline render. +func handleVerfahrensablaufPage(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "dist/verfahrensablauf.html") +} + // POST /api/tools/fristenrechner — calculate the UI timeline for a proceeding. // // Phase C: routes through FristenrechnerService which pulls rules from diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index db0eedd..14e512e 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -160,6 +160,7 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc protected.HandleFunc("GET /tools/kostenrechner", handleKostenrechnerPage) protected.HandleFunc("POST /api/tools/kostenrechner", handleKostenrechnerAPI) protected.HandleFunc("GET /tools/fristenrechner", handleFristenrechnerPage) + protected.HandleFunc("GET /tools/verfahrensablauf", handleVerfahrensablaufPage) protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI) protected.HandleFunc("POST /api/tools/fristenrechner/calculate-rule", handleFristenrechnerCalculateRule) protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes) diff --git a/internal/handlers/verfahrensablauf_test.go b/internal/handlers/verfahrensablauf_test.go new file mode 100644 index 0000000..21fd128 --- /dev/null +++ b/internal/handlers/verfahrensablauf_test.go @@ -0,0 +1,83 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +// /tools/fristenrechner?path=a was the pre-split sidebar entry for the +// "Verfahrensablauf" surface. After t-paliad-179 Slice 1 that intent +// owns its own /tools/verfahrensablauf route — so a naked ?path=a hit +// must 302 to the new URL to preserve bookmarked legacy links. +// +// The Akte-mode internal wizard pathway (?project=&path=a) is +// NOT a top-level entry — it's wizard state set by client-side +// history.replaceState. That URL must keep serving the fristenrechner +// shell so a mid-wizard refresh doesn't bounce away. +func TestHandleFristenrechnerPage_LegacyPathARedirect(t *testing.T) { + cases := []struct { + name string + path string + wantStatus int + wantLoc string + }{ + { + name: "naked path=a → redirect", + path: "/tools/fristenrechner?path=a", + wantStatus: http.StatusFound, + wantLoc: "/tools/verfahrensablauf", + }, + { + name: "path=a with project= → no redirect (Akte-mode wizard)", + path: "/tools/fristenrechner?project=abc-123&path=a", + wantStatus: http.StatusOK, + }, + { + name: "no path param → no redirect", + path: "/tools/fristenrechner", + wantStatus: http.StatusOK, + }, + { + name: "path=b → no redirect (Pathway B stays)", + path: "/tools/fristenrechner?path=b", + wantStatus: http.StatusOK, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, tc.path, nil) + w := httptest.NewRecorder() + handleFristenrechnerPage(w, req) + if w.Code != tc.wantStatus { + // http.ServeFile may write 404 if dist/fristenrechner.html + // is missing under `go test` (CI runs without a frontend + // build). We only care that we did NOT redirect in those + // cases — collapse 200 and 404 into "not a redirect". + if tc.wantStatus == http.StatusOK && w.Code != http.StatusFound { + return + } + t.Fatalf("status = %d, want %d", w.Code, tc.wantStatus) + } + if tc.wantLoc != "" { + if got := w.Header().Get("Location"); got != tc.wantLoc { + t.Fatalf("Location = %q, want %q", got, tc.wantLoc) + } + } + }) + } +} + +// The new /tools/verfahrensablauf route registers as a 1-liner page +// handler that ServeFiles dist/verfahrensablauf.html. We assert the +// handler does NOT redirect — if the dist artefact is missing under +// `go test`, ServeFile may return 404, but it must never return a 3xx. +func TestHandleVerfahrensablaufPage_NoRedirect(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/tools/verfahrensablauf", nil) + w := httptest.NewRecorder() + handleVerfahrensablaufPage(w, req) + if w.Code >= 300 && w.Code < 400 { + t.Fatalf("verfahrensablauf must not redirect; got %d → %s", + w.Code, w.Header().Get("Location")) + } +}