May 2026 reshape. archive/ is now the only physical project-root
directory; working/, staging/, reviewing/ move from the project root
into each archive/<party>/ folder. Six top-level URLs become virtual
aggregators served via the cascade rather than disk:
ssr/mdl/rsk tables rollups across parties with a
synthesised $party source-party column
working/staging/ browse folder-nav listings of parties with
reviewing non-empty content in the slot; per-party
URLs 302-redirect to archive/<party>/<slot>/
Mkdir at the project root is restricted to `archive` and `_`/`.`-
prefixed system names — virtual aggregator names and ad-hoc folders
return 409.
Plan Review hardcodes the scaffold convention (archive/<party>/
{reviewing,staging}/<tracking>/); the pre-reshape
on_plan_review.{reviewing_root,staging_root} cascade keys are dropped.
document_controller is now subtree-admin of every archive/<party>/
(not of project-root working/staging/ as before), so per-party
lifecycle slots inherit admin authority through the cascade.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
107 lines
3.6 KiB
Go
107 lines
3.6 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. (Identity
|
|
// of an SSR row is the party folder name, so the field is named
|
|
// plainly rather than sigil-prefixed.)
|
|
// - MDL / RSK rollup rows get `$party: <party>` so the rollup table
|
|
// can show which package each row came from. The `$` sigil marks
|
|
// the field as system-synthesised: tables tool renders it read-
|
|
// only and the form client strips it before submit, so a user-
|
|
// defined `party` field on a deliverable row never collides with
|
|
// the synthetic source-party column.
|
|
//
|
|
// 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)
|
|
}
|