Files
cgalo5758 257955c9d3 Add operator list-scale contract and People directory
Governed operator lists (organizations, grants, people, billing×4) gain
server-side search, status filters, and 50-row pages with true totals
from count(*) OVER(); state is URL-addressable, out-of-range pages
clamp,
and no-match is distinct from true-empty.

People is the eighth flat sidebar entry: /operator/persons lists persons
newest-joined first (excluding the reserved system person), rows linking
to the existing detail.

Billing gains an operator invoice detail at
/operator/billing/invoices/{invoiceID} reusing the member projection;
open invoices past due present as Overdue (derived, filterable, stored
status untouched); all four views lead with the linked organization and
mute object IDs.

Grants filter over the derived Live/Superseded/Inactive state, the SQL
HAVING predicate pinned to the Go derivation by test. Embedded lists
(org composite ledger, Tier changes) adopt the shared controls under
namespaced params with sibling-state-preserving URLs and scoped htmx
swaps that hold the viewport.

Review corrections: blocked ladder Delete renders disabled with tooltip
and mutations fire toasts; collapse triggers paint their open state;
sections use outside headings; plan topology drops the orphan-product
check; domains policy collapses behind a disclosure.
2026-08-24 03:58:18 -05:00
..

Server Package

This package implements the HTTP server for the member-console application, including HTMX partial handlers.

Overview

The server uses Go's standard net/http package with:

  • Bootstrap 5 for styling
  • HTMX 2.0 for dynamic content updates
  • gorilla/csrf for CSRF protection
  • scs (alexedwards/scs) for server-side session management (Valkey/Redis-backed)

Architecture

The HTTP server handles several types of requests:

  • Static Files (/static/*) - CSS, JS, images served from embedded assets
  • Page Handlers (/) - Full HTML page rendering
  • Partial Handlers (/partials/*) - HTMX partial HTML responses
  • Auth Handlers (/login, /logout, /callback) - OAuth2/OIDC authentication

Partial handlers interact with Temporal workflows for async operations like site provisioning.

Files

  • server.go - Main server setup, middleware chain, route registration
  • member_products.go, member_invoices.go, workspace_partials.go, operator*.go - HTMX partial handlers for this package's own features

Integration-owned handlers (e.g. FedWiki's member-facing API and partials) live in their own tree under internal/integrations/<slug>/web instead of here — see docs/building-an-integration.md and internal/integrations/fedwiki/web for the worked example. The patterns below apply equally to handlers in either location.

HTMX Integration

Partial Handler Pattern

Partial handlers return HTML fragments (not full pages) that HTMX swaps into the DOM:

// Handler returns HTML fragment
func (h *Handler) GetItems(w http.ResponseWriter, r *http.Request) {
    // ... fetch data ...

    h.Templates.Render(w, "items.html", data)
}
<!-- Template returns partial HTML -->
<div class="table-responsive">
  <table class="table">
    {{ range .Sites }}
    <tr>...</tr>
    {{ end }}
  </table>
</div>

HTMX Triggers

Use HX-Trigger header to trigger events that refresh other parts of the page:

// After successful operation, trigger refresh
w.Header().Set("HX-Trigger", `{"refreshSites": true}`)
<!-- Element listens for the trigger -->
<div hx-get="/partials/items" 
     hx-trigger="load, refreshItems from:body">
</div>

CSRF with HTMX

CSRF tokens are included via hx-headers on forms:

<form hx-post="/partials/items"
      hx-headers='{"X-CSRF-Token": "{{ .CSRFToken }}"}'>

Or globally in the page template using hx-vals or a meta tag approach.

Synchronous Workflow Execution

Unlike fire-and-forget patterns, this server waits for Temporal workflows to complete:

func (h *Handler) CreateSite(w http.ResponseWriter, r *http.Request) {
    // 1. Start the workflow
    we, err := h.TemporalClient.ExecuteWorkflow(ctx, options, workflow, input)
    
    // 2. Wait for completion (BLOCKING)
    var result WorkflowOutput
    err = we.Get(ctx, &result)
    
    // 3. Render result based on workflow outcome
    if !result.Success {
        h.renderError(w, result.ErrorMessage)
        return
    }
    h.renderSuccess(w, result)
}

Benefits:

  • User sees accurate success/failure immediately
  • Spinner shows during actual operation
  • Error messages come from workflow

Trade-offs:

  • Request blocks until workflow completes
  • Long operations may timeout (mitigated by workflow retry handling)

Route Organization

Routes are organized by feature using handler structs:

// ItemPartialsHandler groups related routes
type ItemPartialsHandler struct { ... }

func (h *ItemPartialsHandler) RegisterRoutes(mux *http.ServeMux) {
    mux.HandleFunc("GET /partials/items", h.GetItems)
    mux.HandleFunc("POST /partials/items", h.CreateItem)
    mux.HandleFunc("DELETE /partials/items/{id}", h.DeleteItem)
    // ...
}

Error Rendering

Errors are rendered back into the same partial/modal that initiated the request:

func (h *Handler) renderCreateFormError(w http.ResponseWriter, userID int64, errMsg string) {
    // Re-fetch form data
    data := h.getCreateFormData(userID)
    data.Error = errMsg

    // Re-render the form partial with error displayed
    h.Templates.Render(w, "create_form.html", data)
}

This keeps the user in context - they see the error in the same modal/form they were using.

Middleware Stack

handler = middleware.Logging(logger)(handler)      // Request logging
handler = middleware.Recover(logger)(handler)      // Panic recovery
handler = middleware.Compress()(handler)           // Gzip compression
handler = middleware.Decompress()(handler)         // Gzip decompression
handler = middleware.SecurityHeaders()(handler)   // Security headers
handler = middleware.RequestID()(handler)          // Request ID generation
handler = csrf.Protect(key, opts...)(handler)     // CSRF protection