// Package handler — virtualviewhandler.go: GET dispatch for SSR row + // MDL/RSK rollup row URLs. // // These URLs live in the project-level virtual folders (/ssr, // /mdl, /rsk) and rewrite to canonical files inside // /archive//. The bytes returned to the client are // augmented with a single path-derived field that the canonical file // doesn't carry: // // - SSR rows get `name: ` so the table renderer has a column // to sort on and the form edit pre-fills the party name. // - MDL / RSK rollup rows get `party: ` so the rollup table // can show which package each row came from. // // Both fields are stripped before write-back (SSR via serveFormCreateSSR // strip; MDL/RSK rollup writes go through the generic serveFormUpdate, // where the path-derived `party:` is rejected by `additionalProperties: // false` in the underlying schema — so the client must strip it on // submit, which the tables/form JS already does for path-derived // fields). // // Listings: see fs/tree.go. package handler import ( "net/http" "os" "strconv" "codeberg.org/VARASYS/ZDDC/zddc/internal/zddc" "gopkg.in/yaml.v3" ) // ServeVirtualViewRow serves a GET (or HEAD) for one of the virtual // row URLs. Caller is expected to have already evaluated ACL against // vv.PartyArchive's chain. // // For SSR rows: returns the canonical archive//ssr.yaml bytes // with `name: ` injected. If no canonical file exists yet, // returns `name: \n` (an otherwise-empty row) — the SSR view // shows every party folder whether or not metadata has been written. // // For MDL / RSK rollup rows: returns the canonical bytes with // `party: ` injected. If the canonical file doesn't exist // (shouldn't happen — the listing only surfaces real files) returns // 404. func ServeVirtualViewRow(w http.ResponseWriter, r *http.Request, vv zddc.VirtualViewResolution) { if !vv.Resolved || !vv.Kind.IsRowKind() { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } raw, err := os.ReadFile(vv.CanonicalAbs) if err != nil { if !os.IsNotExist(err) { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // File doesn't exist yet. if vv.Kind != zddc.VirtualViewSSRRow { http.NotFound(w, r) return } raw = nil } var data map[string]any if len(raw) > 0 { if err := yaml.Unmarshal(raw, &data); err != nil { http.Error(w, "parse canonical yaml: "+err.Error(), http.StatusInternalServerError) return } } if data == nil { data = make(map[string]any) } switch vv.Kind { case zddc.VirtualViewSSRRow: data["name"] = vv.Party case zddc.VirtualViewMDLRow, zddc.VirtualViewRSKRow: data["party"] = vv.Party } out, err := yaml.Marshal(data) if err != nil { http.Error(w, "marshal virtual row: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/yaml; charset=utf-8") w.Header().Set("Cache-Control", "no-store") w.Header().Set("X-ZDDC-Source", "virtual-view-row") w.Header().Set("X-ZDDC-Resolved-Path", vv.CanonicalURL) w.Header().Set("Content-Length", strconv.Itoa(len(out))) if r.Method == http.MethodHead { w.WriteHeader(http.StatusOK) return } _, _ = w.Write(out) }