Adds the risk register as a sibling of MDL under archive/<party>/, and
three project-level virtual aggregations at <project>/{ssr,mdl,rsk}:
- SSR aggregates archive/<party>/ssr.yaml; "+ Add row" materializes a
new party folder (mkdir + auto-own .zddc + ssr.yaml). Renames go
through X-ZDDC-Op: ssr-rename, which os.Rename's the party
directory so every row inside follows. Party name doubles as the
folder name (no opaque IDs) and is path-derived on read.
- MDL/RSK rollups list every deliverable / every risk across all
parties with a derived `party` column; "+ Add row" is suppressed
because party affiliation is ambiguous in the aggregate view.
All four virtual roots are declared `virtual: true` in
defaults.zddc.yaml. Spec/form bytes come from six new embedded
defaults (default-rsk.*, default-ssr.*, default-project-{mdl,rsk}.*)
served via a generalized IsDefaultSpec/IsDefaultSpecAbs that replaces
the MDL-only recognizer. Listing synthesis lives in fs/tree.go;
ACL on each synthetic row evaluates against the canonical
archive/<party>/ chain so non-owners see rows read-only. PUT/DELETE
through virtual URLs rewrite to canonical paths in fileapi.go via
sibling-shape blocks that don't touch the ACL gate. SSR row DELETE
returns 405 (delete the party folder via the archive view).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
101 lines
3.2 KiB
Go
101 lines
3.2 KiB
Go
// Package handler — virtualviewhandler.go: GET dispatch for SSR row +
|
|
// MDL/RSK rollup row URLs.
|
|
//
|
|
// These URLs live in the project-level virtual folders (<project>/ssr,
|
|
// <project>/mdl, <project>/rsk) and rewrite to canonical files inside
|
|
// <project>/archive/<party>/. 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: <party>` 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: <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/<party>/ssr.yaml bytes
|
|
// with `name: <party>` injected. If no canonical file exists yet,
|
|
// returns `name: <party>\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: <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)
|
|
}
|