Artifact 1e46262ad5897b30614b61fc3df0921f01ccac110118567840b663363ff26e63:
- File
internal/web/web.go
— part of check-in
[83db19e8c7]
at
2026-08-15 10:52:57
on branch trunk
— phase 1 tracer (minus NATS): index, watcher, timeline op + page
SQLite fold via ncruces driver; ingest.Sync with cursor-loss rebuild; serve --repo; harness wipe is now a real rm -rf + re-ingest. (user: dmestas, size: 4808) [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; } table { border-collapse: collapse; width: 100%; } th, td { text-align: left; padding: 0.25rem 0.75rem 0.25rem 0; } th { border-bottom: 1px solid var(--muted); } a { color: inherit; } </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. See the <a href="/timeline">timeline</a>.</p>{{end}}`)) var timelinePage = template.Must(template.Must(base.Clone()).Parse(`{{define "content"}} {{if .Entries}}<table> <thead><tr><th>Check-in</th><th>Date</th><th>User</th><th>Comment</th></tr></thead> <tbody>{{range .Entries}} <tr><td><code>{{printf "%.12s" .Hash}}</code></td><td>{{.Date}}</td><td>{{.User}}</td><td>{{.Comment}}</td></tr> {{end}}</tbody> </table>{{else}}<p>No check-ins.</p>{{end}} {{end}}`)) type pageData struct { Title string Version string Entries []timelineEntry } type timelineEntry struct { Hash string `json:"hash"` Date string `json:"date"` User string `json:"user"` Comment string `json:"comment"` } // 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) { render(r, w, req, homePage, "quarry", nil) }) mux.HandleFunc("GET /timeline", func(w http.ResponseWriter, req *http.Request) { out, err := invoke(req.Context(), r, "timeline.list", req.URL.Query()) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } var tl struct { Entries []timelineEntry `json:"entries"` } remarshal(out, &tl) render(r, w, req, timelinePage, "timeline", tl.Entries) }) return mux } // render executes a page template with the version footer served by the // meta.version op — every page reports it (0001 Principle 1). func render(r *ops.Registry, w http.ResponseWriter, req *http.Request, page *template.Template, title string, entries []timelineEntry) { 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") page.Execute(w, pageData{Title: title, Version: v.Version, Entries: entries}) } // 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) } }