From 2f93fc1854dace968642ff9566aec8ab123001ee Mon Sep 17 00:00:00 2001 From: ZDDC Date: Sun, 10 May 2026 06:44:35 -0500 Subject: [PATCH] feat(zddc-server): reviewing/ slash form serves browse, no-slash serves mdedit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same slash-vs-no-slash convention to reviewing/ that already governs the other three canonical project folders: //reviewing → mdedit (default tool, via DefaultAppAt) //reviewing/ → browse (HTML) — shows the aggregator's virtual / entries as a tree //reviewing/?json → aggregator JSON (handler.ServeReviewing) Browse fetches the JSON listing for the URL it was loaded from, so loading browse.html at //reviewing/ triggers a JSON request back through the dispatcher → ServeReviewing → aggregator output. Browse then renders the virtual / entries as clickable folders. Clicking a tracking folder navigates to the per-submittal view; clicking received/ or staged/ exits the virtual subtree into canonical archive/ or staging/ paths via the polyfill's explicit-url support. The HTML branch in the reviewing dispatcher block was previously calling appsSrv.Serve(..., "mdedit", ...) for trailing-slash URLs; now it falls through to the canonical-folder block which routes to ServeDirectory's HTML default (embedded browse.html). Test: TestDispatchEmptyCanonicalProjectFolders extended with the slash/ → browse subtests, mirroring the no-slash → default app set. All four canonical folders now have symmetric coverage of both shapes. Co-Authored-By: Claude Opus 4.7 (1M context) --- zddc/cmd/zddc-server/main.go | 33 ++++++++++++++----------------- zddc/cmd/zddc-server/main_test.go | 19 ++++++++++++++++++ 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/zddc/cmd/zddc-server/main.go b/zddc/cmd/zddc-server/main.go index a19c142..207f9fb 100644 --- a/zddc/cmd/zddc-server/main.go +++ b/zddc/cmd/zddc-server/main.go @@ -842,15 +842,16 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps } } // Reviewing aggregator. /reviewing/[/] is - // a virtual view. With trailing slash: - // - JSON request → aggregator listing (handler.ServeReviewing) - // - HTML request → mdedit, rooted at the reviewing/ path. - // mdedit's polyfill then fetches the JSON - // listing on its own. - // Without trailing slash, depth-3 (reviewing/) 301s - // to the slash form; depth-2 (reviewing) falls through to the - // canonical-folder block below where DefaultAppAt routes to - // mdedit and the no-slash branch serves it directly. + // a virtual view. The shape rule mirrors the other canonical + // folders (slash → browse, no-slash → default tool): + // - JSON request, any depth → aggregator listing (handler.ServeReviewing) + // - HTML, no slash → mdedit (default tool, via DefaultAppAt) + // - HTML, with slash → browse.html (via ServeDirectory). + // browse fetches JSON which routes back + // through here to ServeReviewing. + // Depth-3 no-slash (reviewing/) 301s to the slash form. + // Depth-2 no-slash (reviewing) falls through to the canonical- + // folder block below where DefaultAppAt routes to mdedit. if r.Method == http.MethodGet || r.Method == http.MethodHead { if proj, tracking, ok := handler.IsReviewingPath(urlPath); ok { if !strings.HasSuffix(urlPath, "/") { @@ -859,21 +860,17 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps return } // Depth-2 no-slash falls through to canonical-folder block. - } else { + } else if strings.Contains(r.Header.Get("Accept"), "application/json") { chain, _ := zddc.EffectivePolicy(cfg.Root, filepath.Join(cfg.Root, proj)) if allowed, _ := policy.AllowFromChain(r.Context(), handler.DeciderFromContext(r), chain, email, urlPath); !allowed { http.Error(w, "Forbidden", http.StatusForbidden) return } - if strings.Contains(r.Header.Get("Accept"), "application/json") { - handler.ServeReviewing(cfg, w, r, proj, tracking) - return - } - if appsSrv != nil { - appsSrv.Serve(w, r, "mdedit", chain, absPath) - return - } + handler.ServeReviewing(cfg, w, r, proj, tracking) + return } + // HTML trailing-slash falls through to canonical-folder + // block → ServeDirectory → embedded browse.html. } } // Canonical project-root folder fallback. /{archive, diff --git a/zddc/cmd/zddc-server/main_test.go b/zddc/cmd/zddc-server/main_test.go index 532deb8..981a59a 100644 --- a/zddc/cmd/zddc-server/main_test.go +++ b/zddc/cmd/zddc-server/main_test.go @@ -548,6 +548,25 @@ func TestDispatchEmptyCanonicalProjectFolders(t *testing.T) { }) } + // Trailing-slash form on a canonical folder serves the browse + // app for HTML requests — same convention as the existing IsDir + // branch. The slash-vs-no-slash distinction is the user's signal: + // "show me the directory contents" vs "open the default tool". + for _, stage := range []string{"working", "staging", "archive", "reviewing"} { + t.Run("slash/"+stage+" → browse", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/Project/"+stage+"/", nil) + req.Header.Set("Accept", "text/html") + rec := httptest.NewRecorder() + dispatch(cfg, idx, ring, appsSrv, nil, rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status=%d, want 200; body=%s", rec.Code, rec.Body.String()) + } + if !strings.Contains(rec.Body.String(), "ZDDC Browse") { + t.Errorf("%s/ HTML response missing 'ZDDC Browse'", stage) + } + }) + } + // Non-canonical missing folder still 404s (the fallback is // scoped to the four canonical names, not a blanket "missing → // empty" rule).