Artifact 362612b5bdf209964a14e65140872b58ae1489bae17122a2b7ce7c056b7ec6a8:
- File
internal/web/web.go
— part of check-in
[bf969e9d1a]
at
2026-08-15 02:10:44
on branch trunk
— phase 0 exit: serve, HTML page via op, harness loop, provenance gate
API router + web page as registry consumers; disposability loop over all surfaces; third_party ledger enforced by arch test. (user: dmestas, size: 3434) [annotate] [blame] [check-ins using]
// Package web is the HTML consumer of the registry (0001 §9). Pages are // server-rendered html/template over op results — the same ops the API // serves, so every page capability is API-visible by construction. This // first page exists to prove the seam end to end (0003 phase 0); real // pages land with the 0002 §4 extraction order. package web import ( "context" "encoding/json" "fmt" "html/template" "net/http" "net/url" "reflect" "craftdesign.group/quarry/internal/ops" ) // base is the layout every page renders through. Both themes come from // prefers-color-scheme; no JavaScript anywhere (0001 Principle 3 — the // no-JS baseline is the baseline). var base = template.Must(template.New("base").Parse(`<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{.Title}} — quarry</title> <style> :root { --fg: #1a1a1a; --bg: #ffffff; --muted: #6a6a6a; } @media (prefers-color-scheme: dark) { :root { --fg: #e8e8e8; --bg: #14161a; --muted: #9a9a9a; } } body { color: var(--fg); background: var(--bg); font: 16px/1.5 system-ui, sans-serif; max-width: 60rem; margin: 0 auto; padding: 1rem; } footer { color: var(--muted); border-top: 1px solid var(--muted); margin-top: 2rem; padding-top: 0.5rem; font-size: 0.85rem; } </style> </head> <body> <main> <h1>{{.Title}}</h1> {{block "content" .}}{{end}} </main> <footer>quarry {{.Version}}</footer> </body> </html> `)) var homePage = template.Must(template.Must(base.Clone()).Parse( `{{define "content"}}<p>A forge for zit repositories. No repositories are being served yet — ingest arrives with phase 1.</p>{{end}}`)) type pageData struct { Title string Version string } // Handler mounts every HTML page over the registry. func Handler(r *ops.Registry) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, req *http.Request) { home(r, w, req) }) return mux } func home(r *ops.Registry, w http.ResponseWriter, req *http.Request) { out, err := invoke(req.Context(), r, "meta.version", nil) if err != nil { http.Error(w, "version op failed", http.StatusInternalServerError) return } var v struct { Version string `json:"version"` } remarshal(out, &v) w.Header().Set("Content-Type", "text/html; charset=utf-8") homePage.Execute(w, pageData{Title: "quarry", Version: v.Version}) } // invoke runs an op through its descriptor with form-shaped input — the // same decode path the API router uses, so HTML pages and API calls hit // identical op semantics. Results come back as opaque values; pages pick // the fields they render via remarshal. func invoke(ctx context.Context, r *ops.Registry, name string, form url.Values) (any, error) { d, ok := r.Lookup(name) if !ok { return nil, fmt.Errorf("web: unknown op %q", name) } inPtr := d.NewIn() if err := ops.DecodeValues(form, inPtr); err != nil { return nil, err } in := reflect.ValueOf(inPtr).Elem().Interface() if err := ops.Validate(in); err != nil { return nil, err } return r.Invoke(ctx, name, in) } // remarshal projects an op result onto the fields a page renders — the // JSON round-trip keeps web coupled to op output *shapes* (the API // contract) rather than to concrete Go types in other packages. func remarshal(from any, to any) { b, err := json.Marshal(from) if err == nil { json.Unmarshal(b, to) } }