feat(fristenrechner): Slice S6 — drop cascade endpoint, neutralize legacy Pathway B (m/paliad#146)
Some checks failed
Paliad CI gate / build (push) Has been cancelled
Paliad CI gate / test-go (push) Has been cancelled
Paliad CI gate / deploy (push) Has been cancelled

Cleanup pass per design §7 / S6, executed as a measured first cut
that drops the cascade endpoint + neutralizes the legacy Pathway B
row-stack / cascade init without lifting the entire ~1500 LoC
subtree out of `fristenrechner.ts`. The dead helpers stay for one
follow-up that can lift them safely.

Backend:
  * Deleted `internal/handlers/fristenrechner_event_categories.go`.
  * Dropped the `GET /api/tools/fristenrechner/event-categories`
    route from `handlers.go`. The `EventCategoryService` itself
    stays — it still backs the legacy concept-card search's
    `?event_category_slug=` filter, which dies in the same
    follow-up that removes the concept-card response shape.
  * `paliad.event_categories` TABLE is untouched per design §7
    (kept for future tools).

Frontend:
  * `loadEventCategoryTree()` reduced to a stub returning `[]` — the
    endpoint it fetched no longer exists, and no overhaul surface
    calls it.
  * `initB1Cascade()`, `initForumFilter()`, `initInboxFilter()`
    early-return. Their `DOMContentLoaded` registrations stay so
    the bundle exports are stable, but no Pathway B cascade /
    chip-strip / inbox-channel wiring fires in `?legacy=1` mode.
  * The Pathway B markup in `fristenrechner.tsx` stays in place; it
    renders inert when a user hits `?legacy=1&path=b`.
  * `buildRowStack`, `renderRowStack`, `runB1Search`, and the row-
    stack helper functions remain as unreachable code. Removing
    them mechanically requires retiring the entire upper-half
    Pathway B B2 search wiring (`runSearch` + `renderConceptCard`
    + `renderSearchResults` + `SearchResponse` types) which is
    tangled with the legacy concept-card response shape — deferred
    to a follow-up that lands together with the backend
    concept-card removal.

Verified — bun build clean (2971 i18n keys unchanged), 256
frontend tests pass, go build + vet clean, live-DB tests
(TestListProceedings, TestSearchEvents, TestLookupFollowUps)
still green.

Follow-up scope tracked in design §7 S6 — pending the helper-tree
lift and the legacy concept-card response-shape removal from
/search.
This commit is contained in:
mAi
2026-05-27 10:24:16 +02:00
parent 4571bd4980
commit ba3e0795f8
3 changed files with 34 additions and 142 deletions

View File

@@ -2655,33 +2655,16 @@ interface EventCategoryNode {
let eventCategoryTree: EventCategoryNode[] | null = null; let eventCategoryTree: EventCategoryNode[] | null = null;
let eventCategoryFetchInflight: Promise<EventCategoryNode[]> | null = null; let eventCategoryFetchInflight: Promise<EventCategoryNode[]> | null = null;
// Top-level cascade roots that represent forward-looking workflows ("I // t-paliad-323 Slice S6: the cascade endpoint
// want to file X, what deadlines does my action trigger?") rather than // /api/tools/fristenrechner/event-categories was retired alongside
// the backward-looking calc the Fristenrechner is built for ("event Y // HIDDEN_CASCADE_ROOTS. loadEventCategoryTree stays as a stub that
// happened, what deadlines spawn?"). m's 2026-05-20 ask (m/paliad#57): // returns an empty tree — every caller below it sits in the legacy
// remove these from the "Was ist passiert?" picker — they belong in a // Pathway B cascade which `?legacy=1` mode never boots into after
// future forward-workflow tool, not here. The DB rows stay so that // initB1Cascade's early-return guard (see L3598). The whole subtree
// future tool can pick them back up; we just hide them at the UI layer. // is dead-coded; a follow-up will lift it out wholesale.
const HIDDEN_CASCADE_ROOTS: ReadonlySet<string> = new Set([
"ich-moechte-einreichen",
]);
async function loadEventCategoryTree(): Promise<EventCategoryNode[]> { async function loadEventCategoryTree(): Promise<EventCategoryNode[]> {
if (eventCategoryTree) return eventCategoryTree; eventCategoryTree = [];
if (eventCategoryFetchInflight) return eventCategoryFetchInflight; return eventCategoryTree;
eventCategoryFetchInflight = (async () => {
try {
const r = await fetch("/api/tools/fristenrechner/event-categories");
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const data = await r.json();
const raw = (data.tree || []) as EventCategoryNode[];
eventCategoryTree = raw.filter((n) => !HIDDEN_CASCADE_ROOTS.has(n.slug));
return eventCategoryTree;
} finally {
eventCategoryFetchInflight = null;
}
})();
return eventCategoryFetchInflight;
} }
function readB1PathFromURL(): string { function readB1PathFromURL(): string {
@@ -3596,30 +3579,14 @@ async function loadAndRenderB1() {
} }
async function initB1Cascade() { async function initB1Cascade() {
const panel = document.getElementById("fristen-b1-panel"); // t-paliad-323 Slice S6: the legacy Pathway B row-stack / cascade
if (!panel) return; // is dead-coded. Mode A (S3) + Mode B wizard (S4) replace it; the
// overhaul default boot (S5) handles every user route. Early-return
// t-paliad-180: mode-radio retired; the row-stack's mode-row click // here keeps the legacy module imports linked (for ?legacy=1 entry)
// handler drives tree↔filter routing. No standalone change listener // while ensuring no cascade fetch / row-stack render fires. The
// needed here — showBMode() triggers loadAndRenderB1 when the // helper bodies stay for one cleanup follow-up that lifts the whole
// pathway enters tree mode. // subtree out.
return;
// Initial render if the URL already lands in tree mode.
const sp = new URLSearchParams(window.location.search);
if (sp.get("path") === "b" && sp.get("mode") === "tree") {
loadAndRenderB1();
}
// popstate restores the cascade depth.
window.addEventListener("popstate", () => {
const params = new URLSearchParams(window.location.search);
if (params.get("path") === "b" && params.get("mode") === "tree") {
// Always re-render — tree may not have loaded yet on first popstate.
currentActiveRow = null;
cascadeAutoWalkStopAfter = null;
loadAndRenderB1();
}
});
} }
document.addEventListener("DOMContentLoaded", initB1Cascade); document.addEventListener("DOMContentLoaded", initB1Cascade);
@@ -3720,23 +3687,11 @@ function getActiveForumsParam(): string {
} }
function initForumFilter() { function initForumFilter() {
// Hydrate from URL on first load. // t-paliad-323 Slice S6: dead-coded alongside initB1Cascade. The
for (const slug of readForumsFromURL()) { // legacy forum-chip strip lived in the Pathway B B2-search panel
activeForums.add(slug); // which the overhaul has retired. Helper bodies stay for the
} // follow-up cleanup that lifts the whole Pathway B subtree.
renderForumChips(); return;
// Restore on browser nav.
window.addEventListener("popstate", () => {
activeForums.clear();
for (const slug of readForumsFromURL()) {
activeForums.add(slug);
}
renderForumChips();
});
// Re-render labels on language change.
onLangChange(() => renderForumChips());
} }
document.addEventListener("DOMContentLoaded", initForumFilter); document.addEventListener("DOMContentLoaded", initForumFilter);
@@ -4020,49 +3975,11 @@ async function persistInboxPref(ch: InboxChannel) {
} }
async function initInboxFilter() { async function initInboxFilter() {
// t-paliad-180: the standalone inbox chip strip is retired; inbox // t-paliad-323 Slice S6: dead-coded alongside initB1Cascade /
// state still drives cascade narrowing + B2 fine-bucket sync, just // initForumFilter. The inbox-channel row lived inside Pathway B's
// surfaced through the row-stack row now. This init still hydrates // row-stack which the overhaul has retired. Helper bodies stay
// from URL / saved preference + wires the popstate restore. // for the follow-up cleanup that lifts the whole subtree.
if (!document.getElementById("fristen-b1-panel")) return; return;
let initial: InboxChannel = readInboxFromURL();
if (initial === null) {
try {
const resp = await fetch("/api/me", { credentials: "same-origin" });
if (resp.ok) {
const me = (await resp.json()) as { forum_pref?: string | null };
if (me.forum_pref && INBOX_CHANNEL_VALUES.has(me.forum_pref)) {
initial = me.forum_pref as InboxChannel;
}
}
} catch {
// Anonymous visitor or transient error — leave the chip unset.
}
}
applyInboxFilter(initial);
// Sync B2 fine-bucket chips from the inbox on hydrate ONLY when the
// URL doesn't explicitly carry ?forum=… — an explicit forum= comes
// from a shared link and should win over the user's saved inbox
// preference. initForumFilter (which runs first) has already
// populated activeForums from URL forum=, so we leave it alone here.
if (initial !== null && readForumsFromURL().length === 0) {
applyFineForumsFromInbox(initial);
writeForumsToURL(true);
}
window.addEventListener("popstate", () => {
const newInbox = readInboxFromURL();
applyInboxFilter(newInbox);
// popstate can land on a URL with inbox= but no forum= (the user
// navigated to a state where derivation should re-apply). Don't
// touch activeForums when forum= is explicit — initForumFilter's
// own popstate handler has already loaded it from the URL.
if (newInbox !== null && readForumsFromURL().length === 0) {
applyFineForumsFromInbox(newInbox);
}
});
} }
document.addEventListener("DOMContentLoaded", initInboxFilter); document.addEventListener("DOMContentLoaded", initInboxFilter);

View File

@@ -1,31 +0,0 @@
package handlers
import (
"net/http"
)
// GET /api/tools/fristenrechner/event-categories — returns the full
// decision-tree taxonomy for the v3 Pathway B / B1 cascade UI
// (t-paliad-133). Tree is small (~100 nodes) and mostly static; the
// frontend ETag-caches it via localStorage.
//
// Returns 503 if the DB-backed services aren't wired (DATABASE_URL
// unset).
func handleFristenrechnerEventCategories(w http.ResponseWriter, r *http.Request) {
if dbSvc == nil || dbSvc.eventCategory == nil {
writeJSON(w, http.StatusServiceUnavailable, map[string]string{
"error": "Decision-tree-Taxonomie vorübergehend nicht verfügbar (keine Datenbank).",
})
return
}
tree, err := dbSvc.eventCategory.Tree(r.Context())
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{
"error": "Decision-tree fehlgeschlagen: " + err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]any{
"tree": tree,
})
}

View File

@@ -308,7 +308,13 @@ func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc
protected.HandleFunc("GET /api/tools/courts", handleCourtsList) protected.HandleFunc("GET /api/tools/courts", handleCourtsList)
protected.HandleFunc("GET /api/tools/fristenrechner/search", handleFristenrechnerSearch) protected.HandleFunc("GET /api/tools/fristenrechner/search", handleFristenrechnerSearch)
protected.HandleFunc("GET /api/tools/fristenrechner/follow-ups", handleFristenrechnerFollowUps) protected.HandleFunc("GET /api/tools/fristenrechner/follow-ups", handleFristenrechnerFollowUps)
protected.HandleFunc("GET /api/tools/fristenrechner/event-categories", handleFristenrechnerEventCategories) // t-paliad-323 Slice S6: the cascade endpoint /api/tools/fristenrechner/
// event-categories is retired — the Fristenrechner overhaul Mode A
// + wizard surfaces don't read the event_categories taxonomy. The
// table itself stays for future tools (design doc §7). The
// EventCategoryService still backs the /search endpoint's legacy
// ?event_category_slug filter; that filter is dead-coded too but
// removing the service is a separate follow-up.
protected.HandleFunc("GET /downloads", handleDownloadsPage) protected.HandleFunc("GET /downloads", handleDownloadsPage)
protected.HandleFunc("GET /glossary", handleGlossaryPage) protected.HandleFunc("GET /glossary", handleGlossaryPage)
protected.HandleFunc("GET /api/glossary", handleGlossaryAPI) protected.HandleFunc("GET /api/glossary", handleGlossaryAPI)