Files
openyield/web/handlers/server.go
T
cloudinit-bot 79a3358810 feat(P3): Window authorization — open/lifecycle/audit log (REQ-042)
web/handlers/window.go: GET /window (list), GET /window/new (form),
POST /window (open), GET /window/{id} (detail+lifecycle+audit), POST
/window/{id}/activate|revoke|expire (lifecycle transitions). Store
extensions: OpenWindow/ActivateWindow/RevokeWindow/ExpireWindow/
ListWindows/GetWindow/GetAuditLog — all call the REAL x/window/types
Window.Activate/Revoke/Expire methods (not reimplementation). Revoke on
Expired is a no-op (v0.2 terminal-state contract). AuditEntry appended on
each transition. 3 Window templates. window_test.go: lifecycle
correctness (asserts real methods invoked), idempotent revoke, revoke-on-
expired no-op, G-026 error lexicon checks. Coverage: store 99.2%,
handlers 87.7%.

---ci---
project: oy
phase: 3
milestone: v0.6
status: execute
---/ci---
2026-08-18 18:58:25 +00:00

122 lines
3.9 KiB
Go

// Package handlers holds the HTTP handlers for the OpenYield web UI screens.
//
// Each screen (Reach signup, Stash dashboard, Window authorization, Standing
// progress, Bloom accrual) gets its own handler file. handlers/server.go wires
// routes into the mux from web/server.go. Handlers render html/template
// templates against the mock store (web/store). Lexicon-clean by construction
// (REQ-012 / REQ-045): the lexicon_meta_web firewall scans these files.
package handlers
import (
"fmt"
"html/template"
"net/http"
"os"
"path/filepath"
"github.com/oy/openyield/web/store"
windowtypes "github.com/oy/openyield/x/window/types"
)
// Server bundles the mock store + per-page templates + route registration.
// Each screen handler is a method on Server so it shares the store + tmpl.
//
// Template loading: base.html is parsed once, then each page template is
// parsed in a CLONE of the base set so the per-page "content" block does not
// collide across pages (Go html/template shares the block namespace within
// one set; cloning per page isolates each page's content block). This is the
// standard Go template pattern for layouts + pages.
type Server struct {
Store *store.Store
Pages map[string]*template.Template
}
// New constructs a Server with the given store + per-page templates loaded
// from templatesDir (the absolute or relative path to web/templates/).
func New(s *store.Store, templatesDir string) (*Server, error) {
funcs := template.FuncMap{
"divGrain": func(grain, unit int64) int64 {
if unit == 0 {
return 0
}
return grain / unit
},
"string": func(v any) string {
switch t := v.(type) {
case string:
return t
case windowtypes.WindowStatus:
return string(t)
default:
return ""
}
},
}
basePath := filepath.Join(templatesDir, "base.html")
base, err := template.New("base.html").Funcs(funcs).ParseFiles(basePath)
if err != nil {
return nil, fmt.Errorf("parse base: %w", err)
}
pages := map[string]*template.Template{}
pageGlob := filepath.Join(templatesDir, "*.html")
matches, err := filepath.Glob(pageGlob)
if err != nil {
return nil, fmt.Errorf("glob pages: %w", err)
}
for _, p := range matches {
name := filepath.Base(p)
if name == "base.html" {
continue
}
clone, cerr := base.Clone()
if cerr != nil {
return nil, fmt.Errorf("clone for %s: %w", name, cerr)
}
pt, perr := clone.ParseFiles(p)
if perr != nil {
return nil, fmt.Errorf("parse %s: %w", name, perr)
}
pages[name] = pt
}
return &Server{Store: s, Pages: pages}, nil
}
// Register wires all screen routes into the given mux (Go 1.22 method
// patterns). Called by web/server.go after constructing the Server.
func (s *Server) Register(mux *http.ServeMux) {
s.registerReach(mux)
s.registerStash(mux)
s.registerWindow(mux)
// P4..P5 register their own routes (standing, bloom).
}
// render executes the named page template with the given data, writing HTML
// to w. The page template invokes base.html and overrides the "content" block.
func (s *Server) render(w http.ResponseWriter, name string, data any) {
tmpl, ok := s.Pages[name]
if !ok {
http.Error(w, "template not found: "+name, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := tmpl.ExecuteTemplate(w, "base.html", data); err != nil {
http.Error(w, "render error", http.StatusInternalServerError)
}
}
// RenderHome renders the home page (public so web/server.go can call it for
// the "/" route which lives outside handlers.Register).
func (s *Server) RenderHome(w http.ResponseWriter, data any) {
s.render(w, "home.html", data)
}
// templatesDir returns the default web/templates directory relative to the
// working directory. Used by web/server.go when constructing via New().
func DefaultTemplatesDir() string {
dir, _ := os.Getwd()
if filepath.Base(dir) == "web" || filepath.Base(dir) == "handlers" {
return filepath.Join(dir, "templates")
}
return "web/templates"
}