// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial // SPDX-FileCopyrightText: 2025-2026 Christian Galo package server import ( "fmt" "html/template" "log/slog" "net/http" "net/http/httptest" "net/url" ) // Server-side composition (spec page-anatomy "A page arrives complete"; // docs/design-system.md §6). A page region that also exists as an HTMX // partial, so a mutation can swap it back in, is rendered into the page by // the server on first paint through an in-process request to the partial's // own route. The browser never fetches first-paint content after load: one // response, no spinner, and the partial stays the single source of the // region's markup. // Include renders a GET route in-process, with the caller's request context // (session, request ID) and headers, and returns its HTML fragment. Core // builds one against its router and hands it to integrations through Deps // so their card bodies compose the same way. type Include func(r *http.Request, path string) (template.HTML, error) // NewInclude returns an Include that dispatches through router. The router // is the mux the partial routes are registered on; the middleware stack sits // outside it, so an include never re-authenticates: the session already // rides the context it is given. func NewInclude(router http.Handler) Include { return func(r *http.Request, path string) (template.HTML, error) { u, err := url.Parse(path) if err != nil { return "", fmt.Errorf("include %s: %w", path, err) } req := r.Clone(r.Context()) req.Method = http.MethodGet req.URL = u req.RequestURI = path req.Body = http.NoBody req.ContentLength = 0 req.Header.Set("HX-Request", "true") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) if rec.Code != http.StatusOK { return "", fmt.Errorf("include %s: status %d", path, rec.Code) } return template.HTML(rec.Body.String()), nil } } // IncludeOr renders the fragment, or an inline notice naming what could not // be loaded, so a failing region never fails the page and never renders // blank. A nil Include (a test set without a router) renders nothing. func IncludeOr(inc Include, logger *slog.Logger, r *http.Request, path, what string) template.HTML { if inc == nil { return "" } out, err := inc(r, path) if err != nil { if logger != nil { logger.Error("page region failed to compose", slog.String("path", path), slog.Any("error", err)) } return template.HTML(``) } return out } // DashboardCardView is a declared card with its body composed for this // request. PartialPath stays the swap source for the card's RefreshEvent. type DashboardCardView struct { DashboardCard Body template.HTML } // ProductsPageData is the /products page: the shell plus its three regions, // composed from their partial routes. type ProductsPageData struct { Shell Shell Entitlements template.HTML Plans template.HTML Addons template.HTML } // BillingPageData is the /billing page: the shell plus the invoices region. type BillingPageData struct { Shell Shell Invoices template.HTML } // IndexPageData is the / dashboard page: the member's identity, the // post-checkout banner state, pending domain claims, the composed // workspaces region, and the integration dashboard cards. Named (rather // than the handler's former anonymous struct) so the page-anatomy header // and section methods in anatomy.go can hang off it. type IndexPageData struct { Name string Username string Email string KeycloakAccountURL string IsOperator bool HasMultipleWorkspaces bool CheckoutStatus string DashboardCards []DashboardCardView Workspaces template.HTML PendingDomainClaims []PendingDomainClaim Shell Shell // HasEntitlement reports whether the person's organization holds any // entitlement (a numeric or granted-boolean entitlement, or a rung on // a plan ladder) — computed alongside DashboardCards in server.go so // IsEmpty (server.go) can decide the dashboard's one empty state // (member-dashboard ADDED requirement "The dashboard has one empty // state"; design D25). HasEntitlement bool }