Files
member-console/internal/integrations/fedwiki/web/operator.go
T
cgalo5758 36e58cd821 Flatten operator sidebar to seven entries
Move sub-surfaces into their sections: billing views get a pill row,
org types a header button. Replace inline IdP handoff copy with an SVG
icon and tooltip, add help icons to dense form rows, and delete the
registry-driven sidebar nav plumbing. Update specs and tests.
2026-08-23 18:26:11 -05:00

210 lines
8.3 KiB
Go

package web
import (
"bytes"
"database/sql"
"html/template"
"io/fs"
"log/slog"
"net/http"
"git.coopcloud.tech/wiki-cafe/member-console/internal/auth"
"git.coopcloud.tech/wiki-cafe/member-console/internal/config"
"git.coopcloud.tech/wiki-cafe/member-console/internal/embeds"
fwmod "git.coopcloud.tech/wiki-cafe/member-console/internal/integrations/fedwiki/store"
"git.coopcloud.tech/wiki-cafe/member-console/internal/organization"
"git.coopcloud.tech/wiki-cafe/member-console/internal/server"
"git.coopcloud.tech/wiki-cafe/member-console/internal/workflows"
temporalsdk "go.temporal.io/sdk/client"
)
// FedWikiSiteViewModel represents a site for the operator template
// rendering. Renamed from the core package's OperatorSiteViewModel
// (openspec/changes/integration-extraction task 2.3): that name suggested
// a generic "sites" concept, but it was 100% FedWiki-specific — its only
// consumer was this page.
type FedWikiSiteViewModel struct {
ID string
Domain string
URL string // composed with the configured fedwiki-site-scheme; see GetSitesPage
OwnerOrgName string
IsCustomDomain bool
CreatedAt string
Status string // active | readonly | archived
}
// FedWikiSitesData holds data for the fedwiki_operator_sites.html partial.
// Renamed from the core package's OperatorSitesData (see
// FedWikiSiteViewModel's doc comment for why).
type FedWikiSitesData struct {
Sites []FedWikiSiteViewModel
Error string
// Sync health (latest schedule-spawned sync execution, from Temporal)
// — same panel the Discourse operator page leads with; the shared
// anatomy is documented in docs/building-an-integration.md.
SyncStatus string
SyncTime string
}
// FedWikiOperatorHandler serves the read-only operator-tier view of every
// FedWiki site in the system, at the path the FedWiki provider manifest
// declares (OperatorSurfacePath). Moved from the core OperatorPartialsHandler
// (GetFedWikiSitesPage, internal/server/operator_pages.go:100-152 pre-move)
// per openspec/changes/integration-extraction task 2.3: unlike the
// member-facing API/partials handlers, this page was previously grafted
// onto the shared operator handler rather than owning its own handler
// type — it now does, mirroring FedWikiHandler/FedWikiPartialsHandler.
type FedWikiOperatorHandler struct {
SiteQ fwmod.Querier
OrgQ organization.Querier
Database *sql.DB
Logger *slog.Logger
AuthConfig *auth.Config
FedWikiSiteScheme string // http or https; mirrors FedWikiHandler/FedWikiPartialsHandler's field
Templates *server.SafeTemplates
Temporal temporalsdk.Client
SyncScheduleID string
}
// FedWikiOperatorHandlerConfig holds configuration for FedWikiOperatorHandler.
type FedWikiOperatorHandlerConfig struct {
SiteQ fwmod.Querier
OrgQ organization.Querier
Database *sql.DB
Logger *slog.Logger
AuthConfig *auth.Config
FedWikiSiteScheme string
// TemplatesFS is FedWiki's own template directory, supplied by the
// Adapter from its embedded templates tree — see
// FedWikiPartialsConfig.TemplatesFS's doc comment for why it's injected
// rather than embedded in this package directly (the same cycle
// argument applies here). Core's operator.html shell, by contrast, is
// fetched directly from internal/embeds below — no cycle risk, since
// embeds is a leaf core package.
TemplatesFS fs.FS
// Temporal + SyncScheduleID feed the Sync health panel (nil/"" →
// "no sync yet"); see workflows.ScheduleRunStatus for why health is
// resolved through the schedule rather than a workflow ID.
Temporal temporalsdk.Client
SyncScheduleID string
}
// NewFedWikiOperatorHandler creates a new FedWikiOperatorHandler.
func NewFedWikiOperatorHandler(cfg FedWikiOperatorHandlerConfig) (*FedWikiOperatorHandler, error) {
shellFS, err := fs.Sub(embeds.Templates, "templates")
if err != nil {
return nil, err
}
// operator.html dispatches to a named body partial via the `renderBody`
// template func (html/template's `{{ template "literal" }}` action
// requires a compile-time-literal name, hence the indirection). This
// handler only ever renders its own one body partial
// (fedwiki_operator_sites.html), so, unlike OperatorPartialsHandler's
// shared set, renderBody here is a closure over that single template
// rather than a dynamic dispatcher over many.
var tmpl *template.Template
tmpl = template.New("fedwiki-operator").Funcs(template.FuncMap{
"renderBody": func(name string, data any) (template.HTML, error) {
var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
return "", err
}
return template.HTML(buf.String()), nil
},
"deploymentName": config.DeploymentName,
})
// The shell's landing branch references operator_lookup_result.html;
// html/template's escape analysis requires every referenced template in
// the set even on branches this page never takes, so parse the shell's
// partial dependency alongside it or every render fails.
tmpl, err = tmpl.ParseFS(shellFS, "operator.html", "partials/operator_lookup_result.html", "partials/operator_setup.html")
if err != nil {
return nil, err
}
tmpl, err = tmpl.ParseFS(cfg.TemplatesFS, "fedwiki_operator_sites.html")
if err != nil {
return nil, err
}
return &FedWikiOperatorHandler{
SiteQ: cfg.SiteQ,
OrgQ: cfg.OrgQ,
Database: cfg.Database,
Logger: cfg.Logger,
AuthConfig: cfg.AuthConfig,
FedWikiSiteScheme: cfg.FedWikiSiteScheme,
Templates: server.NewSafeTemplates(tmpl, cfg.Logger),
Temporal: cfg.Temporal,
SyncScheduleID: cfg.SyncScheduleID,
}, nil
}
// GetSitesPage handles GET <the FedWiki provider manifest's
// OperatorSurfacePath> — read-only integration-tier view of every FedWiki
// site in the system. The existing fedwiki_operator_sites.html partial is
// already read-only (table + external links, no forms), so no template
// changes were needed for the move.
func (h *FedWikiOperatorHandler) GetSitesPage(w http.ResponseWriter, r *http.Request) {
bodyData := FedWikiSitesData{}
bodyData.SyncStatus, bodyData.SyncTime = workflows.ScheduleRunStatus(r.Context(), h.Temporal, h.SyncScheduleID)
if bodyData.SyncStatus == "" {
bodyData.SyncStatus = "No sync has run yet"
}
sites, err := h.SiteQ.ListAllSites(r.Context())
if err != nil {
h.Logger.Error("failed to list sites", slog.Any("error", err))
bodyData.Error = "Failed to retrieve sites"
} else {
// Same config knob the member-facing FedWikiPartialsHandler.buildSiteURL
// uses (default https) — audit finding #37: this page used to hardcode
// https:// regardless, breaking links on http-only dev/test farms.
scheme := h.FedWikiSiteScheme
if scheme == "" {
scheme = "https"
}
// workspace_id → org name cache, same shape as the legacy GetSites
// handler to avoid N+1 lookups when multiple sites belong to the
// same workspace.
orgNameCache := make(map[string]string)
siteVMs := make([]FedWikiSiteViewModel, len(sites))
for i, site := range sites {
orgName := ""
if cached, ok := orgNameCache[site.WorkspaceID]; ok {
orgName = cached
} else {
if ws, wErr := h.OrgQ.GetWorkspaceByID(r.Context(), site.WorkspaceID); wErr == nil {
if org, oErr := h.OrgQ.GetOrganizationByID(r.Context(), ws.OrgID); oErr == nil {
orgName = org.Name
}
}
orgNameCache[site.WorkspaceID] = orgName
}
siteVMs[i] = FedWikiSiteViewModel{
ID: site.SiteID,
Domain: site.Domain,
URL: scheme + "://" + site.Domain,
OwnerOrgName: orgName,
IsCustomDomain: site.IsCustomDomain,
CreatedAt: site.CreatedAt.Format("Jan 2, 2006"),
Status: site.Status,
}
}
bodyData.Sites = siteVMs
}
page := server.BuildOperatorPageData(r, h.AuthConfig, h.Database, h.Logger)
// Second-level page of the Integrations section: the section's sidebar
// entry lights up via ActiveCapability (the page itself is reached from
// the Integrations home's provider cards, not the sidebar); the
// ia-position tuple nests under the section landing.
page.IAPosition = "integration:integrations:fedwiki"
page.ActiveCapability = "integrations"
page.BodyTemplate = "fedwiki_operator_sites.html"
page.BodyData = bodyData
h.Templates.Render(w, "operator.html", page)
}