56db37a463
web/handlers/bloom.go: GET /bloom/{stashID} renders BloomRecord
(AccruedGrain, RateBasisPoints as %, LastAccrualBlock) + the 4.5%
target rate read from x/bloom/types.TargetBloomRateBasisPoints (D-073
code-constant source-of-truth, NOT hardcoded) + the 4.0%-5.0% band
(Min/MaxBloomRateBasisPoints) + Mission Lock note. Store extensions:
GetBloomRecord/ListBloomRecords. Seed: holder-alia at target rate (450
bps), holder-bryn at 420 bps (within band). bloom_test.go: D-073
target-rate source-of-truth regression guard (asserts TargetBloomRate-
BasisPoints=450, Min=400, Max=500 from code constants; would fail if
hardcoded), G-026 error lexicon check, highest-risk screen for banned-
term drift (uses "Bloom"/"real production"/"accrual" labels only).
Coverage: store 98.1%, handlers 89.2%.
---ci---
project: oy
phase: 5
milestone: v0.6
status: execute
---/ci---
126 lines
4.0 KiB
Go
126 lines
4.0 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"
|
|
standingtypes "github.com/oy/openyield/x/standing/types"
|
|
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)
|
|
case standingtypes.StandingBucket:
|
|
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)
|
|
s.registerStanding(mux)
|
|
s.registerBloom(mux)
|
|
}
|
|
|
|
// 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"
|
|
}
|