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).