// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package server import ( "log/slog" "net/http" "git.coopcloud.tech/wiki-cafe/member-console/internal/instance" ) // GetSetupPage handles GET /operator/setup — the persistent, revisitable // setup checklist (design D12 / operator-setup-checklist). Unlike a wizard, // this page always renders, complete or not, and never gates or sequences // any other operator surface; an operator can leave any step undone and // keep using the rest of the console. It is the checklist's only renderer: // the operator overview carries a dismissible summary banner instead of // embedding the step list (round 2; see operator.html's setup-banner // region and SetupState.ShowBanner). // // It shares OperatorHandler with the landing surface (not // OperatorPartialsHandler) because both surfaces derive from the same // DeriveSetupState so they can never disagree about what is or is not done. func (h *OperatorHandler) GetSetupPage(w http.ResponseWriter, r *http.Request) { page := BuildOperatorPageData(r, h.AuthConfig, h.Database, h.Logger) page.IAPosition = "runtime:setup" // No sidebar entry names "setup" — it is reached from the System // section header's "Setup" action, not the flat capability list — so // leaving ActiveCapability unmatched lights nothing up, same as // GetPersonPage's sidebar-less person detail page. page.ActiveCapability = "setup" page.BodyTemplate = "operator_setup.html" page.BodyData = h.DeriveSetupState(r.Context()) h.Templates.Render(w, "operator.html", page) } // DismissSetupBanner handles POST /partials/operator/setup/dismiss — the // operator overview's setup-banner close button. Records the dismissal // deployment-wide (design D12/D28: the instance setting // instance.SetupBannerDismissed, core.instance_settings — not a cookie, not // per-operator) and responds with an empty 200 body so the banner's own // hx-swap="outerHTML" removes it with nothing left in its place. // // A method on OperatorPartialsHandler, not OperatorHandler, because the // route lives among the other operator HTMX mutation routes // (operator_partials.go); it builds its own instance.Store from the // existing Database field rather than adding one, matching how it already // builds its own integration.Queries per request (GetIntegrationsPage). func (h *OperatorPartialsHandler) DismissSetupBanner(w http.ResponseWriter, r *http.Request) { page := h.buildOperatorPageData(r) store := instance.NewStore(h.Database) // updatedBy mirrors the config-override settings write path // (operator_integration_settings.go PostIntegrationSetting): the // signed-in operator's email, or unset (NULL) when unavailable. if err := store.SetBool(r.Context(), instance.SetupBannerDismissed, true, page.Email); err != nil { h.Logger.Error("failed to dismiss setup banner", slog.Any("error", err)) http.Error(w, "Failed to dismiss the setup banner.", http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) }