package handlers import ( "context" "encoding/json" "net/http" "strings" "time" "github.com/jmoiron/sqlx" "mgit.msbls.de/m/paliad/internal/auth" "mgit.msbls.de/m/paliad/internal/services" ) var authClient *auth.Client // noCacheAssets wraps a static-file handler so /assets/* and /icons/* always // trigger a conditional GET. http.FileServer already emits Last-Modified, so // browsers send If-Modified-Since on the next visit and the server replies // 304 when the file is unchanged — fast for repeat loads, fresh on deploy. func noCacheAssets(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache, must-revalidate") h.ServeHTTP(w, r) }) } // patentstyleDownload sets a Content-Disposition with the spaced filename // "HL Patents Style.dotm" for .dotm requests under /patentstyle/. The URL // path stays clean (dashes), browsers and download tools land the file // with the name PAs expect to see. func patentstyleDownload(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasSuffix(r.URL.Path, ".dotm") { w.Header().Set("Content-Disposition", `attachment; filename="HL Patents Style.dotm"`) } h.ServeHTTP(w, r) }) } // noCachePages wraps a handler so its response always revalidates. Combined // with the build-time `?v=` stamp on /assets/*.js and /css URLs // in dist/*.html, this is what makes a deploy actually reach users: the HTML // is re-fetched (or 304'd) on every navigation, and any change to the asset // version triggers a fresh script load. func noCachePages(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-cache, must-revalidate") h.ServeHTTP(w, r) }) } // Services bundles the Phase B + C database-backed services. Pass nil if // DATABASE_URL was unset; the matter-management endpoints will return 503. type Services struct { // Pool is the raw connection pool. Held so the readiness probe // (/health/ready) can ping it without going through any individual // service. nil when DATABASE_URL was unset — in that case // /health/ready returns 503. Pool *sqlx.DB Project *services.ProjectService Team *services.TeamService PartnerUnit *services.PartnerUnitService Party *services.PartyService Deadline *services.DeadlineService Appointment *services.AppointmentService CalDAV *services.CalDAVService CalDAVBindings *services.CalendarBindingService Rules *services.DeadlineRuleService Calculator *services.DeadlineCalculator Users *services.UserService Fristenrechner *services.FristenrechnerService EventDeadline *services.EventDeadlineService EventTrigger *services.EventTriggerService RuleEditor *services.RuleEditorService DeadlineSearch *services.DeadlineSearchService EventCategory *services.EventCategoryService EventType *services.EventTypeService Dashboard *services.DashboardService Note *services.NoteService ChecklistInst *services.ChecklistInstanceService ChecklistCatalog *services.ChecklistCatalogService ChecklistTemplate *services.ChecklistTemplateService ChecklistShare *services.ChecklistShareService ChecklistPromotion *services.ChecklistPromotionService Mail *services.MailService Invite *services.InviteService Agenda *services.AgendaService Audit *services.AuditService EmailTemplate *services.EmailTemplateService Link *services.LinkService Event *services.EventService Courts *services.CourtService Approval *services.ApprovalService Derivation *services.DerivationService UserView *services.UserViewService Broadcast *services.BroadcastService Pin *services.PinService CardLayout *services.CardLayoutService DashboardLayout *services.DashboardLayoutService // FirmDashboardDefault is the firm-wide /dashboard default layout // (Slice C). Admin-only writes; per-user seed/reset reads it via // DashboardLayoutService.defaultLayout(). Nil-safe — falls back to // the code-resident FactoryDefaultLayout. FirmDashboardDefault *services.FirmDashboardDefaultService Projection *services.ProjectionService Export *services.ExportService // t-paliad-246 — Backup Mode (org-scope admin backups). Nil when // DATABASE_URL or PALIAD_EXPORT_DIR is unset; the /admin/backups // routes return 503 in that case. Backup *services.BackupRunner // t-paliad-238 — dedicated Submissions/Schriftsätze editor. SubmissionDraft *services.SubmissionDraftService // t-paliad-313 (m/paliad#141) Composer Slice A + B — base catalog, // per-draft section rows, render-pipeline assembler. All three // nil in DATABASE_URL-less deploys (the Composer surfaces return // 503 / hide the picker). SubmissionBase *services.BaseService SubmissionSection *services.SectionService SubmissionComposer *services.SubmissionComposer // t-paliad-315 Composer Slice C — building-block library + admin // editor. Per Q2: paste sources only, no lineage on sections. SubmissionBuildingBlock *services.BuildingBlockService // t-paliad-349 docforge slice 4/6 — uploaded-template store backing // the authoring surface. TemplateStore *services.PgTemplateStore // t-paliad-265 / m/paliad#96 — per-event-card optional choices on // the Verfahrensablauf timeline. EventChoice *services.EventChoiceService // Slice D (m/paliad#124 §5, mig 145) — named scenario compositions // per project or as abstract templates. Nil when DATABASE_URL is // unset; the /api/scenarios routes return 503 in that case. Scenario *services.ScenarioService // m/paliad#149 Phase 2 P0 — per-project scenario_flags SSoT (mig 154). // Drives Verfahrensablauf + Mode B result-view conditional rendering // and per-rule selection state (`rule:` keys). ScenarioFlags *services.ScenarioFlagsService // t-paliad-340 / m/paliad#153 B0 — Litigation Builder. CRUD over the // new normalised scenario shape (paliad.scenarios with owner_id + // scenario_proceedings + scenario_events + scenario_shares, mig 157). // Nil when DATABASE_URL is unset — /api/builder/scenarios* routes 503. ScenarioBuilder *services.ScenarioBuilderService // Paliadin is wired when DATABASE_URL is set. The concrete backend // is picked in cmd/server/main.go based on PALIADIN_REMOTE_HOST // (remote → mRiver via SSH) or local tmux availability. Stays nil // without DATABASE_URL; in that case the per-request handler gate // 404s anyway. Paliadin services.Paliadin } func Register(mux *http.ServeMux, client *auth.Client, giteaAPIToken string, svc *Services) { authClient = client giteaToken = giteaAPIToken if svc != nil && svc.Paliadin != nil { paliadinSvc = svc.Paliadin } if svc != nil { dbSvc = &dbServices{ projects: svc.Project, team: svc.Team, partnerUnit: svc.PartnerUnit, parties: svc.Party, deadline: svc.Deadline, appointment: svc.Appointment, caldav: svc.CalDAV, caldavBindings: svc.CalDAVBindings, rules: svc.Rules, calc: svc.Calculator, users: svc.Users, fristenrechner: svc.Fristenrechner, eventDeadline: svc.EventDeadline, eventTrigger: svc.EventTrigger, ruleEditor: svc.RuleEditor, deadlineSearch: svc.DeadlineSearch, eventCategory: svc.EventCategory, eventType: svc.EventType, dashboard: svc.Dashboard, note: svc.Note, checklistInst: svc.ChecklistInst, checklistCatalog: svc.ChecklistCatalog, checklistTemplate: svc.ChecklistTemplate, checklistShare: svc.ChecklistShare, checklistPromotion: svc.ChecklistPromotion, mail: svc.Mail, invite: svc.Invite, agenda: svc.Agenda, audit: svc.Audit, emailTemplate: svc.EmailTemplate, link: svc.Link, event: svc.Event, courts: svc.Courts, approval: svc.Approval, derivation: svc.Derivation, userView: svc.UserView, broadcast: svc.Broadcast, pin: svc.Pin, cardLayout: svc.CardLayout, dashboardLayout: svc.DashboardLayout, firmDashboardDefault: svc.FirmDashboardDefault, projection: svc.Projection, export: svc.Export, backup: svc.Backup, submissionDraft: svc.SubmissionDraft, submissionBase: svc.SubmissionBase, submissionSection: svc.SubmissionSection, submissionComposer: svc.SubmissionComposer, submissionBuildingBlock: svc.SubmissionBuildingBlock, templateStore: svc.TemplateStore, eventChoice: svc.EventChoice, scenario: svc.Scenario, scenarioFlags: svc.ScenarioFlags, scenarioBuilder: svc.ScenarioBuilder, } } // Liveness probe. Public, no auth, no DB touch — just confirms the // process bound the listener and the goroutine is alive. Used by the // boot-smoke test (cmd/server/main_smoke_test.go) to assert the server // reaches a serving state after migrations apply; also safe for any // future container orchestrator or uptime check. mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/plain; charset=utf-8") _, _ = w.Write([]byte("ok\n")) }) // Readiness probe. Public, no auth. Distinct from /healthz: this // returns 200 only when the DB pool is reachable. Reaching Register // at all implies db.ApplyMigrations succeeded (cmd/server/main.go // calls it before constructing svc), so a 200 here means "migrations // applied AND pool responsive" — the contract Dokploy / Traefik should // gate on, not the bind-and-serve check that /healthz answers. // // Three outcomes: // - svc == nil OR svc.Pool == nil → 503 (DB-less knowledge-platform // deployments report not-ready so an external orchestrator can // distinguish them from a full prod boot). // - PingContext fails within 2 s → 503 (pool unreachable). // - PingContext succeeds → 200 "ready". // // Used by docker-compose.yml's healthcheck (Slice B) and by the // post-deploy verification step in .gitea/workflows/test.yaml. mux.HandleFunc("GET /health/ready", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") w.Header().Set("Content-Type", "text/plain; charset=utf-8") if svc == nil || svc.Pool == nil { http.Error(w, "db not configured\n", http.StatusServiceUnavailable) return } ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() if err := svc.Pool.PingContext(ctx); err != nil { http.Error(w, "db unreachable\n", http.StatusServiceUnavailable) return } _, _ = w.Write([]byte("ready\n")) }) // API endpoints (JSON, public) mux.HandleFunc("POST /api/login", handleAPILogin) mux.HandleFunc("POST /api/register", handleAPIRegister) // Public pages — wrapped in noCachePages so the HTML always revalidates // and picks up new build-versioned /assets URLs after a deploy. mux.Handle("GET /login", noCachePages(http.HandlerFunc(handleLoginPage))) mux.Handle("GET /logout", noCachePages(http.HandlerFunc(handleLogout))) // Landing page: public to unauthenticated visitors, redirects logged-in // users to /dashboard. Handled on the outer mux so the auth Middleware // doesn't bounce unauthenticated visitors to /login. mux.Handle("GET /{$}", noCachePages(http.HandlerFunc(handleRootPage))) // Static assets (public). Cache-Control: no-cache forces browsers to // revalidate every load. Combined with the Last-Modified header that // http.FileServer emits, repeat visits get a fast 304 when assets are // unchanged but pick up a fresh bundle the moment we deploy. Without // this, browsers were heuristically caching /assets/*.js for the whole // freshness window and continuing to execute the previous deploy's // (broken) bundle even after the SW had been kill-switched (t-paliad-043). mux.Handle("GET /assets/", noCacheAssets(http.StripPrefix("/assets/", http.FileServer(http.Dir("dist/assets"))))) // PWA static surface (public): manifest, icons, service worker. The SW // must be served from the application origin; its scope is /, so the file // has to live at /sw.js (a SW served from /assets/sw.js could only claim // /assets/* by default). mux.HandleFunc("GET /manifest.json", servePWAManifest) mux.Handle("GET /icons/", noCacheAssets(http.StripPrefix("/icons/", http.FileServer(http.Dir("dist/icons"))))) mux.HandleFunc("GET /sw.js", servePWAServiceWorker) // HL Patents Style auto-update endpoint. version.json is the manifest // the installed Word client polls; HL-Patents-Style.dotm is fetched on // version mismatch. Source files live in frontend/public/patentstyle/ // (copied into dist/ at build time). noCacheAssets ensures the manifest // is never stale after a release. patentstyleDownload renames the .dotm // to "HL Patents Style.dotm" (with spaces) on download — the on-disk // filename has dashes so the URL is clean, but Word users expect the // spaced name in their downloads folder. mux.Handle("GET /patentstyle/", noCacheAssets(patentstyleDownload(http.StripPrefix("/patentstyle/", http.FileServer(http.Dir("dist/patentstyle")))))) // Protected routes protected := http.NewServeMux() 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("GET /tools/procedures", handleProceduresPage) protected.HandleFunc("POST /api/tools/fristenrechner", handleFristenrechnerAPI) protected.HandleFunc("POST /api/tools/fristenrechner/calculate-rule", handleFristenrechnerCalculateRule) protected.HandleFunc("GET /api/tools/proceeding-types", handleProceedingTypes) protected.HandleFunc("GET /api/tools/trigger-events", handleTriggerEventsList) protected.HandleFunc("POST /api/tools/event-deadlines", handleEventDeadlinesCalculate) protected.HandleFunc("POST /api/tools/event-trigger", handleEventTriggerCalculate) protected.HandleFunc("GET /api/tools/courts", handleCourtsList) protected.HandleFunc("GET /api/tools/fristenrechner/search", handleFristenrechnerSearch) protected.HandleFunc("GET /api/tools/fristenrechner/follow-ups", handleFristenrechnerFollowUps) // 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 /glossary", handleGlossaryPage) protected.HandleFunc("GET /api/glossary", handleGlossaryAPI) protected.HandleFunc("POST /api/glossary/suggest", handleGlossarySuggest) protected.HandleFunc("GET /files/{filename}", handleFileDownload) protected.HandleFunc("POST /api/files/refresh", handleFileRefresh) protected.HandleFunc("GET /links", handleLinksPage) protected.HandleFunc("GET /api/links", handleLinksAPI) protected.HandleFunc("POST /api/links/suggest", handleLinkSuggest) protected.HandleFunc("POST /api/links/feedback", handleLinkFeedback) protected.HandleFunc("GET /api/links/suggestions/count", handleSuggestionCount) protected.HandleFunc("GET /tools/gebuehrentabellen", handleGebuehrentabellenPage) protected.HandleFunc("GET /api/tools/gebuehrentabellen", handleGebuehrentabellenAPI) protected.HandleFunc("GET /api/tools/gebuehrentabellen/lookup", handleGebuehrentabellenLookup) protected.HandleFunc("POST /api/tools/gebuehrentabellen/feedback", handleGebuehrentabellenFeedback) protected.HandleFunc("GET /checklists", handleChecklistsPage) protected.HandleFunc("GET /checklists/new", handleChecklistsAuthorPage) protected.HandleFunc("GET /checklists/instances/{id}", handleChecklistInstancePage) protected.HandleFunc("GET /checklists/templates/{slug}/edit", handleChecklistsAuthorPage) protected.HandleFunc("GET /checklists/{slug}", handleChecklistDetailPage) protected.HandleFunc("GET /api/checklists", handleChecklistsAPI) protected.HandleFunc("GET /api/checklists/{slug}", handleChecklistAPI) protected.HandleFunc("POST /api/checklists/feedback", handleChecklistsFeedback) // t-paliad-225 Slice A — user-authored templates (paliad.checklists). protected.HandleFunc("GET /api/checklists/templates/mine", handleListMyChecklistTemplates) protected.HandleFunc("POST /api/checklists/templates", handleCreateChecklistTemplate) protected.HandleFunc("PATCH /api/checklists/templates/{slug}", handleUpdateChecklistTemplate) protected.HandleFunc("PATCH /api/checklists/templates/{slug}/visibility", handleSetChecklistTemplateVisibility) protected.HandleFunc("DELETE /api/checklists/templates/{slug}", handleDeleteChecklistTemplate) // t-paliad-225 Slice B — explicit sharing + admin promotion. protected.HandleFunc("GET /api/checklists/templates/{slug}/shares", handleListChecklistShares) protected.HandleFunc("POST /api/checklists/templates/{slug}/shares", handleGrantChecklistShare) protected.HandleFunc("DELETE /api/checklists/shares/{id}", handleRevokeChecklistShare) protected.HandleFunc("POST /api/admin/checklists/{slug}/promote", handlePromoteChecklist) protected.HandleFunc("POST /api/admin/checklists/{slug}/demote", handleDemoteChecklist) protected.HandleFunc("GET /api/checklists/{slug}/instances", handleListChecklistInstancesForTemplate) protected.HandleFunc("POST /api/checklists/{slug}/instances", handleCreateChecklistInstance) protected.HandleFunc("GET /api/checklist-instances", handleListAllChecklistInstances) protected.HandleFunc("GET /api/checklist-instances/{id}", handleGetChecklistInstance) protected.HandleFunc("PATCH /api/checklist-instances/{id}", handleUpdateChecklistInstance) protected.HandleFunc("POST /api/checklist-instances/{id}/reset", handleResetChecklistInstance) protected.HandleFunc("DELETE /api/checklist-instances/{id}", handleDeleteChecklistInstance) protected.HandleFunc("GET /api/projects/{id}/checklists", handleListChecklistInstancesForProject) // t-paliad-240 — global Schriftsätze drafts index (top-level sidebar // entry). Lists every draft the caller owns across visible projects. // The per-project Schriftsätze tab keeps the editor itself project- // scoped; this index is the cross-project landing. protected.HandleFunc("GET /submissions", gateOnboarded(handleSubmissionsIndexPage)) protected.HandleFunc("GET /courts", handleCourtsPage) protected.HandleFunc("GET /api/courts", handleCourtsAPI) protected.HandleFunc("POST /api/courts/feedback", handleCourtsFeedback) protected.HandleFunc("GET /changelog", handleChangelogPage) protected.HandleFunc("GET /api/changelog", handleChangelogAPI) protected.HandleFunc("GET /api/changelog/unseen-count", handleChangelogUnseenCount) // Phase B (DB-backed) — return 503 if DATABASE_URL unset. protected.HandleFunc("GET /api/deadline-rules", handleListDeadlineRules) protected.HandleFunc("GET /api/proceeding-types-db", handleListProceedingTypesDB) protected.HandleFunc("POST /api/deadlines/calculate", handleCalculateDeadlines) // Projects v2 (hierarchical tree — t-paliad-024). protected.HandleFunc("GET /api/projects", handleListProjects) protected.HandleFunc("POST /api/projects", handleCreateProject) protected.HandleFunc("GET /api/projects/tree", handleGetProjectsTree) protected.HandleFunc("GET /api/projects/{id}", handleGetProject) protected.HandleFunc("PATCH /api/projects/{id}", handleUpdateProject) protected.HandleFunc("DELETE /api/projects/{id}", handleDeleteProject) protected.HandleFunc("GET /api/projects/{id}/events", handleListProjectEvents) // m/paliad#149 Phase 2 P0 — per-project scenario_flags SSoT (mig 154). // Verfahrensablauf + Mode B result-view bind their conditional // checkboxes here; P3 will add per-rule "rule:" selection entries // on top of the same endpoint. protected.HandleFunc("GET /api/projects/{id}/scenario-flags", handleGetScenarioFlags) protected.HandleFunc("PATCH /api/projects/{id}/scenario-flags", handlePatchScenarioFlags) // t-paliad-171 / t-paliad-173 — SmartTimeline (Verlauf-tab redesign). // /timeline returns the merged timeline (actuals + Slice 2 projections). // /timeline/milestone is the "Eigener Meilenstein" write path. // /timeline/anchor is the click-to-anchor write (Slice 2). // /timeline/skip is the "ist nicht eingetreten" decision (§6.4). protected.HandleFunc("GET /api/projects/{id}/timeline", handleGetProjectTimeline) // t-paliad-177 Slice 2 — iCal feed (deadlines + appointments only). protected.HandleFunc("GET /api/projects/{id}/timeline.ics", handleGetProjectTimelineICS) // t-paliad-214 Slice 2 — project-subtree data export. ?direct_only=1 // narrows to the root project only; default = root + descendants. // Permission gate: responsibility ∈ {lead, member} OR global_admin. protected.HandleFunc("GET /api/projects/{id}/export", handleProjectExport) protected.HandleFunc("POST /api/projects/{id}/timeline/milestone", handleCreateProjectTimelineMilestone) protected.HandleFunc("POST /api/projects/{id}/timeline/anchor", handleProjectTimelineAnchor) protected.HandleFunc("POST /api/projects/{id}/timeline/skip", handleProjectTimelineSkip) // t-paliad-230 — submission generator (format-only). /submissions // lists the project's published filing rules; /generate fetches the // universal HL Patents Style .dotm, strips the macro project, and // streams a clean .docx attachment. POST because each generation // writes an audit row. protected.HandleFunc("GET /api/projects/{id}/submissions", handleListProjectSubmissions) protected.HandleFunc("POST /api/projects/{id}/submissions/{code}/generate", handleGenerateProjectSubmission) // t-paliad-238 — dedicated Submissions/Schriftsätze draft editor. // Per (project, submission_code, user) named drafts with autosaved // variable overrides; export merges the bag with the universal HL // Patents Style template (Slice A) or the per-code template // (Slice B, future). protected.HandleFunc("GET /api/projects/{id}/submissions/{code}/drafts", handleListSubmissionDrafts) protected.HandleFunc("POST /api/projects/{id}/submissions/{code}/drafts", handleCreateSubmissionDraft) protected.HandleFunc("GET /api/projects/{id}/submissions/{code}/drafts/{draft_id}", handleGetSubmissionDraft) protected.HandleFunc("PATCH /api/projects/{id}/submissions/{code}/drafts/{draft_id}", handlePatchSubmissionDraft) protected.HandleFunc("DELETE /api/projects/{id}/submissions/{code}/drafts/{draft_id}", handleDeleteSubmissionDraft) protected.HandleFunc("GET /api/projects/{id}/submissions/{code}/drafts/{draft_id}/preview", handlePreviewSubmissionDraft) protected.HandleFunc("POST /api/projects/{id}/submissions/{code}/drafts/{draft_id}/export", handleExportSubmissionDraft) // t-paliad-240 — global drafts index (across visible projects). protected.HandleFunc("GET /api/user/submission-drafts", handleListUserSubmissionDrafts) // t-paliad-243 — global Schriftsätze drafts with optional project // binding. The picker page at /submissions/new lists the full // cross-proceeding catalog (without a project context) and posts to // POST /api/submission-drafts to spawn a draft. The // /api/submission-drafts/{draft_id}* endpoints back the project-less // editor and ALSO accept project-scoped drafts (the draft row // carries its own project_id so the project segment is redundant). protected.HandleFunc("GET /api/submissions/catalog", handleListSubmissionCatalog) protected.HandleFunc("POST /api/submission-drafts", handleCreateGlobalSubmissionDraft) protected.HandleFunc("GET /api/submission-drafts/{draft_id}", handleGetGlobalSubmissionDraft) protected.HandleFunc("PATCH /api/submission-drafts/{draft_id}", handleGlobalPatchSubmissionDraft) protected.HandleFunc("DELETE /api/submission-drafts/{draft_id}", handleGlobalDeleteSubmissionDraft) protected.HandleFunc("POST /api/submission-drafts/{draft_id}/export", handleGlobalExportSubmissionDraft) // t-paliad-313 (m/paliad#141) Composer Slice A — base catalog for // the sidebar picker. Wide-open SELECT (any authenticated user); // admin mutations are not exposed yet (Slice C). protected.HandleFunc("GET /api/submission-bases", handleListSubmissionBases) // t-paliad-349 (m/paliad#157) docforge slice 5 — the variable // catalogue (Go-side SSOT) the sidebar form + authoring palette read. protected.HandleFunc("GET /api/docforge/variables", handleDocforgeVariables) // t-paliad-349 slice 7 — firm-shared template picker list for // generation (any authenticated lawyer; admin authoring stays gated). protected.HandleFunc("GET /api/templates", handlePickerTemplates) // t-paliad-313 (m/paliad#141) Composer Slice B — per-section PATCH // for inline editor autosave. URL keyed on draft_id + section_id; // owner-scoped via SubmissionDraftService.Get. protected.HandleFunc("PATCH /api/submission-drafts/{draft_id}/sections/{section_id}", handlePatchSubmissionSection) // t-paliad-318 (m/paliad#141) Composer Slice F — add custom // section, delete section, reorder. protected.HandleFunc("POST /api/submission-drafts/{draft_id}/sections", handleCreateSubmissionSection) protected.HandleFunc("DELETE /api/submission-drafts/{draft_id}/sections/{section_id}", handleDeleteSubmissionSection) protected.HandleFunc("POST /api/submission-drafts/{draft_id}/sections/reorder", handleReorderSubmissionSections) // t-paliad-315 (m/paliad#141) Composer Slice C — building blocks // library. Lawyer-facing picker + paste mechanic. protected.HandleFunc("GET /api/submission-building-blocks", handleListBuildingBlocks) protected.HandleFunc("POST /api/submission-building-blocks/{block_id}/insert-into/{section_id}", handleInsertBlockIntoSection) // t-paliad-277 / m/paliad#109 — refresh project-derived variables on // the draft. Strips overrides for project.* / parties.* / deadline.* // / procedural_event.* / rule.* prefixes and bumps last_imported_at. protected.HandleFunc("POST /api/submission-drafts/{draft_id}/import-from-project", handleImportFromProject) // /counterclaim creates a CCR sub-project linked via the new // paliad.projects.counterclaim_of FK (t-paliad-174 Slice 3). protected.HandleFunc("POST /api/projects/{id}/counterclaim", handleCreateProjectCounterclaim) protected.HandleFunc("GET /api/projects/{id}/children", handleListProjectChildren) protected.HandleFunc("GET /api/projects/{id}/tree", handleGetProjectTree) protected.HandleFunc("POST /api/projects/{id}/pin", handlePinProject) protected.HandleFunc("DELETE /api/projects/{id}/pin", handleUnpinProject) protected.HandleFunc("GET /api/user-pinned-projects", handleListPinnedProjects) protected.HandleFunc("GET /api/projects/cards-preview", handleProjectsCardsPreview) protected.HandleFunc("GET /api/user-card-layouts", handleListCardLayouts) protected.HandleFunc("POST /api/user-card-layouts", handleCreateCardLayout) protected.HandleFunc("PATCH /api/user-card-layouts/{id}", handleUpdateCardLayout) protected.HandleFunc("DELETE /api/user-card-layouts/{id}", handleDeleteCardLayout) protected.HandleFunc("POST /api/user-card-layouts/{id}/set-default", handleSetDefaultCardLayout) // t-paliad-219 — per-user configurable dashboard layout. protected.HandleFunc("GET /api/me/dashboard-layout", handleGetDashboardLayout) protected.HandleFunc("PUT /api/me/dashboard-layout", handlePutDashboardLayout) protected.HandleFunc("POST /api/me/dashboard-layout/reset", handleResetDashboardLayout) protected.HandleFunc("GET /api/dashboard-widget-catalog", handleGetWidgetCatalog) protected.HandleFunc("GET /api/projects/{id}/ancestors", handleListProjectAncestors) protected.HandleFunc("GET /api/projects/{id}/parties", handleListParties) protected.HandleFunc("POST /api/projects/{id}/parties", handleCreateParty) // Team membership endpoints for Project detail "Team" tab. protected.HandleFunc("GET /api/projects/{id}/team", handleListProjectTeam) protected.HandleFunc("POST /api/projects/{id}/team", handleAddProjectTeamMember) protected.HandleFunc("PATCH /api/projects/{id}/team/{user_id}", handleChangeProjectTeamMemberResponsibility) protected.HandleFunc("DELETE /api/projects/{id}/team/{user_id}", handleRemoveProjectTeamMember) // t-paliad-139 — sub-team aggregation surfaces for the Team tab. protected.HandleFunc("GET /api/projects/{id}/team/derived", handleListDerivedTeam) protected.HandleFunc("GET /api/projects/{id}/team/from-descendants", handleListDescendantStaffedTeam) // t-paliad-139 — project ↔ partner-unit attachment management. protected.HandleFunc("GET /api/projects/{id}/partner-units", handleListAttachedUnits) protected.HandleFunc("POST /api/projects/{id}/partner-units", handleAttachPartnerUnit) protected.HandleFunc("DELETE /api/projects/{id}/partner-units/{unit_id}", handleDetachPartnerUnit) // t-paliad-265 — per-event-card choices on the Verfahrensablauf timeline. protected.HandleFunc("GET /api/projects/{id}/event-choices", handleListProjectEventChoices) protected.HandleFunc("PUT /api/projects/{id}/event-choices", handlePutProjectEventChoice) protected.HandleFunc("DELETE /api/projects/{id}/event-choices/{submission_code}/{choice_kind}", handleDeleteProjectEventChoice) // Slice D (m/paliad#124 §5, mig 145) — named scenario compositions // per project or as abstract templates on /tools/verfahrensablauf. protected.HandleFunc("GET /api/scenarios", handleScenariosList) protected.HandleFunc("GET /api/scenarios/{id}", handleScenarioGet) protected.HandleFunc("POST /api/scenarios", handleScenarioCreate) protected.HandleFunc("PATCH /api/scenarios/{id}", handleScenarioPatch) protected.HandleFunc("DELETE /api/scenarios/{id}", handleScenarioDelete) protected.HandleFunc("PUT /api/projects/{id}/active-scenario", handleSetActiveScenario) // t-paliad-340 / m/paliad#153 B0 — Litigation Builder API over the // new normalised scenario shape (mig 157). Coexists with the legacy // /api/scenarios surface during the B0→B6 migration; B6 cleanup // retires the legacy routes. protected.HandleFunc("GET /api/builder/scenarios", handleBuilderScenariosList) protected.HandleFunc("POST /api/builder/scenarios", handleBuilderScenarioCreate) // m/paliad#153 B4 — Akte mode entry point. Creates a project-backed // scenario from a paliad.projects row; subsequent edits dual-write // through to paliad.deadlines + paliad.projects.scenario_flags. protected.HandleFunc("POST /api/builder/scenarios/from-project", handleBuilderScenarioFromProject) // m/paliad#153 B5 — "Geteilt mit mir" bucket. Literal segment wins // over {id} in Go 1.22+ ServeMux precedence, so this never shadows GET .../{id}. protected.HandleFunc("GET /api/builder/scenarios/shared", handleBuilderScenariosShared) protected.HandleFunc("GET /api/builder/scenarios/{id}", handleBuilderScenarioGet) protected.HandleFunc("PATCH /api/builder/scenarios/{id}", handleBuilderScenarioPatch) protected.HandleFunc("POST /api/builder/scenarios/{id}/proceedings", handleBuilderProceedingCreate) protected.HandleFunc("PATCH /api/builder/scenarios/{id}/proceedings/{pid}", handleBuilderProceedingPatch) protected.HandleFunc("DELETE /api/builder/scenarios/{id}/proceedings/{pid}", handleBuilderProceedingDelete) protected.HandleFunc("POST /api/builder/scenarios/{id}/proceedings/{pid}/events", handleBuilderEventCreate) protected.HandleFunc("PATCH /api/builder/scenarios/{id}/events/{eid}", handleBuilderEventPatch) protected.HandleFunc("DELETE /api/builder/scenarios/{id}/events/{eid}", handleBuilderEventDelete) protected.HandleFunc("POST /api/builder/scenarios/{id}/shares", handleBuilderShareCreate) protected.HandleFunc("DELETE /api/builder/scenarios/{id}/shares/{sid}", handleBuilderShareDelete) // m/paliad#153 B5 — transactional promote-to-project wizard commit. protected.HandleFunc("POST /api/builder/scenarios/{id}/promote", handleBuilderScenarioPromote) // m/paliad#153 B2 — read-only passthrough so the builder can render // per-triplet flag toggles without a per-project round-trip. protected.HandleFunc("GET /api/builder/scenario-flag-catalog", handleBuilderFlagCatalog) // m/paliad#153 B3 — universal search (events + scenarios + projects). protected.HandleFunc("GET /api/builder/search", handleBuilderSearch) // Dev-only test route — gated to PaliadinOwnerEmail (m). protected.HandleFunc("GET /dev/scenario-builder", handleBuilderDevTestPage) // Partner units (structural partner-led units; legacy "Dezernate"). protected.HandleFunc("GET /api/partner-units", handleListPartnerUnits) protected.HandleFunc("POST /api/partner-units", handleCreatePartnerUnit) protected.HandleFunc("GET /api/partner-units/{id}", handleGetPartnerUnit) protected.HandleFunc("PATCH /api/partner-units/{id}", handleUpdatePartnerUnit) protected.HandleFunc("DELETE /api/partner-units/{id}", handleDeletePartnerUnit) protected.HandleFunc("GET /api/partner-units/{id}/members", handleListPartnerUnitMembers) protected.HandleFunc("POST /api/partner-units/{id}/members", handleAddPartnerUnitMember) protected.HandleFunc("DELETE /api/partner-units/{id}/members/{user_id}", handleRemovePartnerUnitMember) // t-paliad-139 — set unit_role on a member. protected.HandleFunc("PATCH /api/partner-units/{id}/members/{user_id}/role", handleSetUnitMemberRole) protected.HandleFunc("GET /api/parties/search", handlePartiesSearch) protected.HandleFunc("DELETE /api/parties/{id}", handleDeleteParty) // Phase F — Appointments (appointments) protected.HandleFunc("GET /api/appointments", handleListAppointments) protected.HandleFunc("GET /api/appointments/summary", handleAppointmentsSummary) protected.HandleFunc("POST /api/appointments", handleCreateAppointment) protected.HandleFunc("GET /api/appointments/{id}", handleGetAppointment) protected.HandleFunc("PATCH /api/appointments/{id}", handleUpdateAppointment) protected.HandleFunc("DELETE /api/appointments/{id}", handleDeleteAppointment) protected.HandleFunc("GET /api/projects/{id}/appointments", handleListAppointmentsForProject) // Phase F — CalDAV configuration (per-user, encrypted at rest) protected.HandleFunc("GET /api/caldav-config", handleGetCalDAVConfig) protected.HandleFunc("PUT /api/caldav-config", handlePutCalDAVConfig) protected.HandleFunc("DELETE /api/caldav-config", handleDeleteCalDAVConfig) protected.HandleFunc("POST /api/caldav-config/test", handleTestCalDAVConfig) protected.HandleFunc("GET /api/caldav-config/log", handleCalDAVSyncLog) // t-paliad-212 Slice 2a/2b — multi-calendar binding CRUD. protected.HandleFunc("GET /api/caldav-bindings", handleListCalDAVBindings) protected.HandleFunc("POST /api/caldav-bindings", handleCreateCalDAVBinding) protected.HandleFunc("PATCH /api/caldav-bindings/{id}", handlePatchCalDAVBinding) protected.HandleFunc("DELETE /api/caldav-bindings/{id}", handleDeleteCalDAVBinding) // /api/caldav-discover — calendar-home-set walk (RFC 6764) for picker. protected.HandleFunc("GET /api/caldav-discover", handleCalDAVDiscover) // Slice 2c — MKCALENDAR ("Create new calendar" affordance in picker). protected.HandleFunc("POST /api/caldav-mkcalendar", handleCalDAVMakeCalendar) // t-paliad-088 — Event Types (categorization for Deadlines). protected.HandleFunc("GET /api/event-types", handleListEventTypes) protected.HandleFunc("GET /api/event-types/suggest", handleSuggestEventTypes) protected.HandleFunc("POST /api/event-types", handleCreateEventType) protected.HandleFunc("PATCH /api/event-types/{id}", handleUpdateEventType) // t-paliad-110 — unified events endpoint backing the shared EventsPage // rendered on /deadlines and /appointments. Coexists with the // type-specific /api/deadlines + /api/appointments endpoints (calendars, // project-detail panes, mobile/PWA still call those directly). protected.HandleFunc("GET /api/events", handleListEvents) protected.HandleFunc("GET /api/events/summary", handleEventsSummary) // Phase E — Deadlines (persistent deadlines) protected.HandleFunc("GET /api/deadlines", handleListDeadlines) protected.HandleFunc("GET /api/deadlines/summary", handleDeadlinesSummary) protected.HandleFunc("GET /api/deadlines/{id}", handleGetDeadline) protected.HandleFunc("PATCH /api/deadlines/{id}", handleUpdateDeadline) protected.HandleFunc("PATCH /api/deadlines/{id}/complete", handleCompleteDeadline) protected.HandleFunc("PATCH /api/deadlines/{id}/reopen", handleReopenDeadline) protected.HandleFunc("DELETE /api/deadlines/{id}", handleDeleteDeadline) protected.HandleFunc("GET /api/projects/{id}/deadlines", handleListDeadlinesForProject) protected.HandleFunc("POST /api/projects/{id}/deadlines", handleCreateDeadline) protected.HandleFunc("POST /api/projects/{id}/deadlines/bulk", handleBulkCreateDeadlines) // Phase I — Notes (polymorphic notes) protected.HandleFunc("GET /api/projects/{id}/notes", handleListNotesForProject) protected.HandleFunc("POST /api/projects/{id}/notes", handleCreateNoteForProject) protected.HandleFunc("GET /api/deadlines/{id}/notes", handleListNotesForDeadline) protected.HandleFunc("POST /api/deadlines/{id}/notes", handleCreateNoteForDeadline) protected.HandleFunc("GET /api/appointments/{id}/notes", handleListNotesForAppointment) protected.HandleFunc("POST /api/appointments/{id}/notes", handleCreateNoteForAppointment) protected.HandleFunc("PATCH /api/notes/{id}", handleUpdateNote) protected.HandleFunc("DELETE /api/notes/{id}", handleDeleteNote) // Global search — mixes static curated content with visibility-gated DB // lookups across every major surface. See internal/handlers/search.go. protected.HandleFunc("GET /api/search", handleSearch) protected.HandleFunc("GET /api/me", handleGetMe) protected.HandleFunc("PATCH /api/me", handleUpdateMe) // t-paliad-214 Slice 1 — personal-scope data export. Bundles xlsx + // JSON + per-sheet CSVs in one deterministic .zip; streams the result // inline. Audit row written to paliad.system_audit_log. protected.HandleFunc("GET /api/me/export", handleMeExport) protected.HandleFunc("GET /api/users", handleListUsers) protected.HandleFunc("GET /api/offices", handleListOffices) protected.HandleFunc("GET /api/dashboard", handleDashboardAPI) protected.HandleFunc("GET /api/agenda", handleAgendaAPI) // Invitations — send a colleague a Paliad invite email. protected.HandleFunc("POST /api/invite", handleInvite) protected.HandleFunc("GET /api/invite", handleInviteStatus) // First-login profile capture — authenticated but NOT behind the // onboarding gate (it's the one page a user without paliad.users may reach). protected.HandleFunc("GET /onboarding", handleOnboardingPage) protected.HandleFunc("POST /api/onboarding", handleCreateOnboarding) // Phase G — Dashboard (logged-in landing). Server-renders the data // payload inline; client boots from window.__PALIAD_DASHBOARD__ with no // waterfall fetch (design audit §2.3). protected.HandleFunc("GET /dashboard", gateOnboarded(handleDashboardPage)) protected.HandleFunc("GET /agenda", gateOnboarded(handleAgendaPage)) // Phase D — server-rendered Projects pages. protected.HandleFunc("GET /projects", gateOnboarded(handleProjectsListPage)) protected.HandleFunc("GET /projects/new", gateOnboarded(handleProjectsNewPage)) protected.HandleFunc("GET /projects/{id}", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/history", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/events", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/children", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/parties", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/deadlines", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/appointments", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/documents", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/notes", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/checklists", gateOnboarded(handleProjectsDetailPage)) protected.HandleFunc("GET /projects/{id}/team", gateOnboarded(handleProjectsDetailPage)) // t-paliad-230 Schriftsätze tab — same shape as every other tab above. // Without this route the deep-link 404s; the tab still works via // in-page click since it just toggles a panel. protected.HandleFunc("GET /projects/{id}/submissions", gateOnboarded(handleProjectsDetailPage)) // t-paliad-238 — dedicated submission draft editor. Both routes // render the project detail page; the actual editor mounts // client-side based on the URL path. protected.HandleFunc("GET /projects/{id}/submissions/{code}/draft", gateOnboarded(handleSubmissionDraftPage)) protected.HandleFunc("GET /projects/{id}/submissions/{code}/draft/{draft_id}", gateOnboarded(handleSubmissionDraftPage)) // t-paliad-243 — global Schriftsätze pages: picker + project-less // editor. Both render dist/* files; client bundles parse the URL // and branch on whether a project segment is present. protected.HandleFunc("GET /submissions/new", gateOnboarded(handleSubmissionsNewPage)) protected.HandleFunc("GET /submissions/draft/{draft_id}", gateOnboarded(handleSubmissionDraftGlobalPage)) // t-paliad-177 — standalone Project Timeline / Chart page (Slice 1). // Horizontal SVG renderer mounted client-side; reuses the existing // /api/projects/{id}/timeline JSON endpoint for data. protected.HandleFunc("GET /projects/{id}/chart", gateOnboarded(handleProjectsChartPage)) protected.HandleFunc("GET /projects/{id}/deadlines/new", gateOnboarded(handleDeadlinesNewPage)) protected.HandleFunc("GET /projects/{id}/appointments/new", gateOnboarded(handleAppointmentsNewPage)) // t-paliad-115 — canonical /events URL for the unified Fristen + Termine // surface. The page reads ?type=… for prefilled type filter and ?view=… // for prefilled view (cards / list / calendar). The legacy /deadlines and // /appointments list URLs 301-redirect to the typed /events variants. protected.HandleFunc("GET /events", gateOnboarded(handleEventsListPage)) protected.HandleFunc("GET /deadlines", gateOnboarded(handleDeadlinesListRedirect)) protected.HandleFunc("GET /deadlines/new", gateOnboarded(handleDeadlinesNewPage)) protected.HandleFunc("GET /deadlines/calendar", gateOnboarded(handleDeadlinesCalendarPage)) protected.HandleFunc("GET /deadlines/{id}", gateOnboarded(handleDeadlinesDetailPage)) // Phase F — Appointments pages protected.HandleFunc("GET /appointments", gateOnboarded(handleAppointmentsListRedirect)) protected.HandleFunc("GET /appointments/new", gateOnboarded(handleAppointmentsNewPage)) protected.HandleFunc("GET /appointments/calendar", gateOnboarded(handleAppointmentsCalendarPage)) protected.HandleFunc("GET /appointments/{id}", gateOnboarded(handleAppointmentsDetailPage)) // Team directory — browsable list of all onboarded users (t-paliad-029). protected.HandleFunc("GET /team", gateOnboarded(handleTeamPage)) // t-paliad-147 — bulk team-email broadcast. // /api/team/broadcast: project lead OR global_admin → BroadcastService gates. // /admin/broadcasts page + list/detail API: visibility-gated in service // (global_admin sees all; sender sees own). protected.HandleFunc("GET /api/team/memberships", gateOnboarded(handleListMembershipsIndex)) protected.HandleFunc("POST /api/team/broadcast", gateOnboarded(handleTeamBroadcast)) protected.HandleFunc("GET /admin/broadcasts", gateOnboarded(handleAdminBroadcastsPage)) protected.HandleFunc("GET /api/admin/broadcasts", gateOnboarded(handleListBroadcasts)) protected.HandleFunc("GET /api/admin/broadcasts/{id}", gateOnboarded(handleGetBroadcast)) // Settings protected.HandleFunc("GET /settings", gateOnboarded(handleSettingsPage)) protected.HandleFunc("GET /settings/{tab}", handleSettingsTabRedirect) // Admin team management — page + API endpoints. RequireAdmin gates every // route at the middleware layer so the handlers themselves don't repeat // the role check. Only available when the DB is configured (the lookup // hits paliad.users). if svc != nil && svc.Users != nil { adminGate := auth.RequireAdminFunc users := svc.Users protected.HandleFunc("GET /admin", adminGate(users, gateOnboarded(handleAdminIndexPage))) protected.HandleFunc("GET /admin/team", adminGate(users, gateOnboarded(handleAdminTeamPage))) protected.HandleFunc("GET /admin/audit-log", adminGate(users, gateOnboarded(handleAdminAuditLogPage))) protected.HandleFunc("GET /admin/partner-units", adminGate(users, gateOnboarded(handleAdminPartnerUnitsPage))) protected.HandleFunc("GET /admin/email-templates", adminGate(users, gateOnboarded(handleAdminEmailTemplatesPage))) protected.HandleFunc("GET /admin/email-templates/{key}", adminGate(users, gateOnboarded(handleAdminEmailTemplatesEditPage))) protected.HandleFunc("GET /admin/event-types", adminGate(users, gateOnboarded(handleAdminEventTypesPage))) // t-paliad-246 / m/paliad#77 Slice A — Backup Mode admin page + // API. Routes only register when Users is wired (matches the // other admin routes); per-request 503 if BackupRunner itself // is unwired (PALIAD_EXPORT_DIR unset). protected.HandleFunc("GET /admin/backups", adminGate(users, gateOnboarded(handleAdminBackupsPage))) protected.HandleFunc("POST /api/admin/backups/run", adminGate(users, handleAdminRunBackup)) protected.HandleFunc("GET /api/admin/backups", adminGate(users, handleAdminListBackups)) protected.HandleFunc("GET /api/admin/backups/{id}", adminGate(users, handleAdminGetBackup)) protected.HandleFunc("GET /api/admin/backups/{id}/file", adminGate(users, handleAdminDownloadBackup)) // t-paliad-349 docforge slice 6 — template authoring surface // (upload base .docx → place variable slots → save). Admin-only, // firm-shared catalog like submission_bases. protected.HandleFunc("GET /admin/templates", adminGate(users, gateOnboarded(handleTemplatesAuthoringPage))) protected.HandleFunc("GET /api/admin/templates", adminGate(users, handleListTemplates)) protected.HandleFunc("POST /api/admin/templates", adminGate(users, handleUploadTemplate)) protected.HandleFunc("GET /api/admin/templates/{id}", adminGate(users, handleGetTemplateAuthoring)) protected.HandleFunc("POST /api/admin/templates/{id}/slots", adminGate(users, handlePlaceTemplateSlot)) protected.HandleFunc("GET /api/admin/users", adminGate(users, handleAdminListUsers)) protected.HandleFunc("POST /api/admin/users", adminGate(users, handleAdminCreateUser)) protected.HandleFunc("POST /api/admin/users/full", adminGate(users, handleAdminCreateFullUser)) protected.HandleFunc("GET /api/admin/users/unonboarded", adminGate(users, handleAdminListUnonboarded)) protected.HandleFunc("PATCH /api/admin/users/{id}", adminGate(users, handleAdminUpdateUser)) protected.HandleFunc("DELETE /api/admin/users/{id}", adminGate(users, handleAdminDeleteUser)) protected.HandleFunc("GET /api/audit-log", adminGate(users, handleListAuditLog)) // t-paliad-219 Slice C — firm-wide dashboard default + admin promote. protected.HandleFunc("GET /api/admin/firm-dashboard-default", adminGate(users, handleGetFirmDashboardDefault)) protected.HandleFunc("PUT /api/admin/firm-dashboard-default", adminGate(users, handlePutFirmDashboardDefault)) protected.HandleFunc("DELETE /api/admin/firm-dashboard-default", adminGate(users, handleDeleteFirmDashboardDefault)) protected.HandleFunc("POST /api/me/dashboard-layout/promote", adminGate(users, handlePromoteDashboardLayoutToFirmDefault)) // t-paliad-315 (m/paliad#141) Composer Slice C — admin building blocks editor. protected.HandleFunc("GET /admin/submission-building-blocks", adminGate(users, gateOnboarded(handleAdminBuildingBlocksPage))) protected.HandleFunc("GET /api/admin/submission-building-blocks", adminGate(users, handleAdminListBuildingBlocks)) protected.HandleFunc("POST /api/admin/submission-building-blocks", adminGate(users, handleAdminCreateBuildingBlock)) protected.HandleFunc("GET /api/admin/submission-building-blocks/{block_id}", adminGate(users, handleAdminGetBuildingBlock)) protected.HandleFunc("PATCH /api/admin/submission-building-blocks/{block_id}", adminGate(users, handleAdminUpdateBuildingBlock)) protected.HandleFunc("DELETE /api/admin/submission-building-blocks/{block_id}", adminGate(users, handleAdminDeleteBuildingBlock)) protected.HandleFunc("GET /api/admin/submission-building-blocks/{block_id}/versions", adminGate(users, handleAdminListBuildingBlockVersions)) protected.HandleFunc("POST /api/admin/submission-building-blocks/{block_id}/restore/{version_id}", adminGate(users, handleAdminRestoreBuildingBlockVersion)) protected.HandleFunc("GET /api/admin/email-templates", adminGate(users, handleAdminListEmailTemplates)) protected.HandleFunc("GET /api/admin/email-templates/{key}/variables", adminGate(users, handleAdminEmailTemplateVariables)) protected.HandleFunc("GET /api/admin/email-templates/{key}/{lang}", adminGate(users, handleAdminGetEmailTemplate)) protected.HandleFunc("PUT /api/admin/email-templates/{key}/{lang}", adminGate(users, handleAdminSaveEmailTemplate)) protected.HandleFunc("POST /api/admin/email-templates/{key}/{lang}/preview", adminGate(users, handleAdminPreviewEmailTemplate)) protected.HandleFunc("POST /api/admin/email-templates/{key}/{lang}/reset", adminGate(users, handleAdminResetEmailTemplate)) protected.HandleFunc("GET /api/admin/email-templates/{key}/{lang}/versions", adminGate(users, handleAdminListEmailTemplateVersions)) protected.HandleFunc("POST /api/admin/email-templates/{key}/{lang}/restore/{version_id}", adminGate(users, handleAdminRestoreEmailTemplateVersion)) // t-paliad-089 — admin Event-Type moderation panel. // t-paliad-191 Slice 11a — admin rule-editor API. // t-paliad-192 Slice 11b — admin rule-editor UI pages + orphan list/resolve. // Slice B.6 (t-paliad-305) — canonical URL paths under // /admin/procedural-events with 301 redirects from the legacy // /admin/rules paths so existing bookmarks and audit-log // entries continue to resolve. New paths point at the same // handlers; the canonical-URL name aligns with the umbrella // term locked in Slice A. protected.HandleFunc("GET /admin/procedural-events", adminGate(users, gateOnboarded(handleAdminRulesListPage))) protected.HandleFunc("GET /admin/procedural-events/{id}/edit", adminGate(users, gateOnboarded(handleAdminRulesEditPage))) protected.HandleFunc("GET /admin/api/procedural-events", adminGate(users, handleAdminListRules)) protected.HandleFunc("GET /admin/api/procedural-events/{id}", adminGate(users, handleAdminGetRule)) protected.HandleFunc("POST /admin/api/procedural-events", adminGate(users, handleAdminCreateRule)) protected.HandleFunc("PATCH /admin/api/procedural-events/{id}", adminGate(users, handleAdminPatchRule)) protected.HandleFunc("POST /admin/api/procedural-events/{id}/clone-as-draft", adminGate(users, handleAdminCloneAsDraft)) protected.HandleFunc("POST /admin/api/procedural-events/{id}/publish", adminGate(users, handleAdminPublishRule)) protected.HandleFunc("POST /admin/api/procedural-events/{id}/archive", adminGate(users, handleAdminArchiveRule)) protected.HandleFunc("POST /admin/api/procedural-events/{id}/restore", adminGate(users, handleAdminRestoreRule)) protected.HandleFunc("GET /admin/api/procedural-events/{id}/audit", adminGate(users, handleAdminGetRuleAudit)) protected.HandleFunc("GET /admin/api/procedural-events/{id}/preview", adminGate(users, handleAdminPreviewRule)) // Legacy /admin/rules paths — 301 redirect to the canonical // /admin/procedural-events paths. One-slice deprecation window // per design §8.2 (B.6 optional; m authorised the rename // 2026-05-26). After the next slice that audits external // references, these can be retired entirely. protected.HandleFunc("GET /admin/rules", adminGate(users, redirectToProceduralEvents("/admin/procedural-events"))) protected.HandleFunc("GET /admin/rules/{id}/edit", adminGate(users, redirectToProceduralEventEdit)) protected.HandleFunc("GET /admin/api/rules", adminGate(users, redirectToProceduralEvents("/admin/api/procedural-events"))) protected.HandleFunc("GET /admin/api/rules/{id}", adminGate(users, redirectToProceduralEventAPI(""))) protected.HandleFunc("POST /admin/api/rules", adminGate(users, redirectToProceduralEvents("/admin/api/procedural-events"))) protected.HandleFunc("PATCH /admin/api/rules/{id}", adminGate(users, redirectToProceduralEventAPI(""))) protected.HandleFunc("POST /admin/api/rules/{id}/clone-as-draft", adminGate(users, redirectToProceduralEventAPI("/clone-as-draft"))) protected.HandleFunc("POST /admin/api/rules/{id}/publish", adminGate(users, redirectToProceduralEventAPI("/publish"))) protected.HandleFunc("POST /admin/api/rules/{id}/archive", adminGate(users, redirectToProceduralEventAPI("/archive"))) protected.HandleFunc("POST /admin/api/rules/{id}/restore", adminGate(users, redirectToProceduralEventAPI("/restore"))) protected.HandleFunc("GET /admin/api/rules/{id}/audit", adminGate(users, redirectToProceduralEventAPI("/audit"))) protected.HandleFunc("GET /admin/api/rules/{id}/preview", adminGate(users, redirectToProceduralEventAPI("/preview"))) protected.HandleFunc("GET /admin/api/orphans", adminGate(users, handleAdminListOrphans)) protected.HandleFunc("POST /admin/api/orphans/{id}/resolve", adminGate(users, handleAdminResolveOrphan)) protected.HandleFunc("GET /api/admin/event-types", adminGate(users, handleAdminListEventTypes)) protected.HandleFunc("GET /api/admin/event-types/private", adminGate(users, handleAdminListPrivateEventTypes)) protected.HandleFunc("POST /api/admin/event-types/archive", adminGate(users, handleAdminBulkArchiveEventTypes)) protected.HandleFunc("POST /api/admin/event-types/merge", adminGate(users, handleAdminMergeEventTypes)) protected.HandleFunc("POST /api/admin/event-types/{id}/promote", adminGate(users, handleAdminPromoteEventType)) protected.HandleFunc("POST /api/admin/event-types/{id}/restore", adminGate(users, handleAdminRestoreEventType)) // t-paliad-138 — approval-policy CRUD (admin only). The inbox // + decision endpoints are NOT admin-only — they're below. protected.HandleFunc("GET /api/projects/{id}/approval-policies", adminGate(users, handleListApprovalPolicies)) protected.HandleFunc("PUT /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}", adminGate(users, handlePutApprovalPolicy)) protected.HandleFunc("DELETE /api/projects/{id}/approval-policies/{entity_type}/{lifecycle}", adminGate(users, handleDeleteApprovalPolicy)) // t-paliad-154 — approval-policy authoring page + admin APIs for // per-partner-unit defaults, matrix view, bulk-apply, and the // existence-check used by /inbox. protected.HandleFunc("GET /admin/approval-policies", adminGate(users, gateOnboarded(handleAdminApprovalPoliciesPage))) protected.HandleFunc("GET /api/admin/partner-units/{unit_id}/approval-policies", adminGate(users, handleListUnitApprovalPolicies)) protected.HandleFunc("PUT /api/admin/partner-units/{unit_id}/approval-policies/{entity_type}/{lifecycle}", adminGate(users, handlePutUnitApprovalPolicy)) protected.HandleFunc("DELETE /api/admin/partner-units/{unit_id}/approval-policies/{entity_type}/{lifecycle}", adminGate(users, handleDeleteUnitApprovalPolicy)) protected.HandleFunc("GET /api/admin/approval-policies/seeded", adminGate(users, handleApprovalPoliciesSeeded)) protected.HandleFunc("GET /api/admin/approval-policies/matrix", adminGate(users, handleApprovalPoliciesMatrix)) protected.HandleFunc("POST /api/admin/approval-policies/apply-to-descendants", adminGate(users, handleApplyMatrixToDescendants)) } // t-paliad-138 — approval inbox + decision endpoints (any authenticated // user; the service layer gates approve/reject by required-role match). if svc != nil && svc.Approval != nil { protected.HandleFunc("GET /inbox", gateOnboarded(handleInboxPage)) protected.HandleFunc("GET /api/inbox/pending-mine", handleListInboxPendingMine) protected.HandleFunc("GET /api/inbox/mine", handleListInboxMine) protected.HandleFunc("GET /api/inbox/count", handleInboxCount) protected.HandleFunc("POST /api/inbox/mark-all-seen", handleInboxMarkAllSeen) protected.HandleFunc("GET /api/approval-requests/{id}", handleGetApprovalRequest) protected.HandleFunc("POST /api/approval-requests/{id}/approve", handleApproveApprovalRequest) protected.HandleFunc("POST /api/approval-requests/{id}/reject", handleRejectApprovalRequest) protected.HandleFunc("POST /api/approval-requests/{id}/revoke", handleRevokeApprovalRequest) // t-paliad-252 — non-destructive sibling of /revoke: lets the // requester revise the in-flight entity without withdrawing. protected.HandleFunc("POST /api/approval-requests/{id}/edit-entity", handleEditPendingEntity) protected.HandleFunc("POST /api/approval-requests/{id}/suggest-changes", handleSuggestChangesApprovalRequest) // t-paliad-154 — form-time effective policy lookup. Reachable by // every authenticated user (NOT admin-gated) so deadline + // appointment forms can render the 4-eye hint. protected.HandleFunc("GET /api/projects/{id}/approval-policies/effective", gateOnboarded(handleProjectEffectivePolicy)) } // t-paliad-144 Phase A1+A2 — Custom Views (substrate + user_views CRUD // + page shells). API endpoints register when the substrate services are // wired; page shells register unconditionally so /views itself stays // reachable for the empty-state onboarding. if svc != nil && svc.UserView != nil && svc.Event != nil { // API protected.HandleFunc("GET /api/user-views", handleListUserViews) protected.HandleFunc("POST /api/user-views", handleCreateUserView) protected.HandleFunc("GET /api/user-views/{id}", handleGetUserView) protected.HandleFunc("PATCH /api/user-views/{id}", handleUpdateUserView) protected.HandleFunc("DELETE /api/user-views/{id}", handleDeleteUserView) protected.HandleFunc("POST /api/user-views/{id}/touch", handleTouchUserView) protected.HandleFunc("POST /api/views/run", handleRunAdhocView) protected.HandleFunc("POST /api/views/{slug}/run", handleRunSavedView) protected.HandleFunc("GET /api/views/system", handleListSystemViews) // Page shells (A2) protected.HandleFunc("GET /views", gateOnboarded(handleViewsLandingPage)) protected.HandleFunc("GET /views/new", gateOnboarded(handleViewsNewPage)) protected.HandleFunc("GET /views/{slug}/edit", gateOnboarded(handleViewsEditPage)) protected.HandleFunc("GET /views/{slug}", gateOnboarded(handleViewsShellPage)) } // t-paliad-146 — Paliadin (PoC). Routes register unconditionally; // the per-request handler gate (requirePaliadinOwner) returns 404 // for any authenticated user other than services.PaliadinOwnerEmail. // No deploy-time toggle — the gate is in the code, not in the env. protected.HandleFunc("GET /paliadin", gateOnboarded(handlePaliadinPage)) protected.HandleFunc("POST /api/paliadin/turn", handlePaliadinTurn) protected.HandleFunc("GET /api/paliadin/stream/{id}", handlePaliadinStream) protected.HandleFunc("GET /api/paliadin/turns/{id}", handlePaliadinTurnGet) // Recovery endpoint (t-paliad-235): when the SSE stream drops mid-turn, // the frontend hits this to ask whether aichat actually finished the // turn upstream. Dispatches per backend — aichat hits the conversation // API; legacy backends fall through to the local row read + janitor. protected.HandleFunc("GET /api/paliadin/turns/{id}/recover", handlePaliadinTurnRecover) // Crash-resistant history hydrate (t-paliad-161 follow-up): both // Paliadin surfaces use this to seed their UI from the DB before // consulting localStorage. protected.HandleFunc("GET /api/paliadin/history", handlePaliadinHistory) protected.HandleFunc("POST /api/paliadin/reset", handlePaliadinReset) // Agent-suggested write path (t-paliad-161 Slice D). Owner-gated; // drafts a deadline / appointment that lands in the approval pipeline. protected.HandleFunc("POST /api/paliadin/suggest/deadline", handlePaliadinSuggestDeadline) protected.HandleFunc("POST /api/paliadin/suggest/appointment", handlePaliadinSuggestAppointment) protected.HandleFunc("GET /admin/paliadin", gateOnboarded(handleAdminPaliadinPage)) protected.HandleFunc("GET /api/admin/paliadin/stats", handleAdminPaliadinStats) protected.HandleFunc("GET /api/admin/paliadin/turns", handleAdminPaliadinTurns) // Catch-all 404 — runs for any authenticated path that no more-specific // pattern claimed. Renders the chromed shell with HTTP 404 (Bug 9 from // tests/smoke-auth-2026-04-25.md). Must be registered last on this mux. protected.HandleFunc("/", handleNotFoundPage) // Legacy German URLs redirect 301 on the OUTER mux so old bookmarks // land on the new path before hitting the auth middleware (avoids a // login→redirect round-trip from unauthenticated visitors). registerLegacyRedirects(mux) // Session middleware refreshes tokens; user-id middleware extracts the // JWT sub claim into the request context for handlers that need it. // noCachePages wraps the lot so every page response revalidates — combined // with build-time ?v= stamping on /assets URLs, that's how a new deploy // reaches users on their next navigation. mux.Handle("/", noCachePages(client.Middleware(client.WithUserID(protected)))) } func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } // parseDirectOnly reads a `direct_only=true|false` query value. Returns true // only for the explicit "true" / "1" forms; everything else (including empty) // is the subtree-aggregating default per t-paliad-139. func parseDirectOnly(raw string) bool { switch raw { case "true", "1": return true default: return false } }