diff --git a/zddc/cmd/zddc-server/main.go b/zddc/cmd/zddc-server/main.go
index 29407ac..e7b60c9 100644
--- a/zddc/cmd/zddc-server/main.go
+++ b/zddc/cmd/zddc-server/main.go
@@ -905,7 +905,7 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
http.Redirect(w, r, urlPath+"/", http.StatusFound)
return
}
- handler.ServeDirectory(cfg, w, r)
+ handler.ServeDirectory(cfg, appsSrv, w, r)
return
}
http.Error(w, "Not Found", http.StatusNotFound)
@@ -980,7 +980,7 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
http.Redirect(w, r, urlPath+"/", http.StatusFound)
return
}
- handler.ServeDirectory(cfg, w, r)
+ handler.ServeDirectory(cfg, appsSrv, w, r)
return
}
diff --git a/zddc/internal/handler/directory.go b/zddc/internal/handler/directory.go
index 5c3f4f4..ecb00e5 100644
--- a/zddc/internal/handler/directory.go
+++ b/zddc/internal/handler/directory.go
@@ -39,7 +39,15 @@ func safeJoin(fsRoot, relPath string) (string, bool) {
// ServeDirectory handles a request for a directory path.
// If Accept: application/json → return Caddy-compatible JSON listing.
// Otherwise → return minimal HTML.
-func ServeDirectory(cfg config.Config, w http.ResponseWriter, r *http.Request) {
+//
+// appsSrv is optional: when non-nil, HTML responses resolve the
+// `browse` tool through the apps subsystem so a `.zddc apps:` cascade
+// entry can override the embedded bytes (handy for live alpha-dev
+// iteration: point apps.browse: at a path source and every ./build is
+// served from disk without recompiling the binary). When nil, the
+// embedded copy is served directly — same behavior as before the
+// cascade hook was added.
+func ServeDirectory(cfg config.Config, appsSrv *apps.Server, w http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
if !strings.HasSuffix(urlPath, "/") {
http.Redirect(w, r, urlPath+"/", http.StatusFound)
@@ -150,12 +158,17 @@ func ServeDirectory(cfg config.Config, w http.ResponseWriter, r *http.Request) {
return
}
- // Browser HTML fallback: serve the embedded `browse` tool. It's a
- // single-file SPA whose autoDetectServerMode loads the JSON listing
- // for the current directory and renders it as a sortable, filterable
- // tree. Same bytes that get served at /
/browse.html — but at
- // the bare directory URL too, so any zddc-served folder presents a
- // usable file browser to anyone who navigates to it.
+ // Browser HTML fallback: serve the `browse` tool. By default it's
+ // the embedded copy (single-file SPA whose autoDetectServerMode
+ // loads the JSON listing for the current directory and renders it
+ // as a sortable, filterable tree). A `.zddc apps: browse:` entry
+ // up the chain can override with a path or URL source — when
+ // appsSrv is wired up, delegate to it so cascade entries are
+ // honored at directory URLs too (not just //browse.html).
+ if appsSrv != nil {
+ appsSrv.Serve(w, r, "browse", chain, absDir)
+ return
+ }
body := apps.EmbeddedBytes("browse")
if len(body) == 0 {
// Bootstrap state: a fresh build hasn't populated browse.html
diff --git a/zddc/internal/handler/directory_test.go b/zddc/internal/handler/directory_test.go
index 11400df..a5d53c4 100644
--- a/zddc/internal/handler/directory_test.go
+++ b/zddc/internal/handler/directory_test.go
@@ -57,7 +57,7 @@ func TestServeDirectoryRootIsPublic(t *testing.T) {
// Anonymous: empty email in context.
req = req.WithContext(context.WithValue(req.Context(), EmailKey, ""))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200 (root is public); body = %s",
@@ -70,7 +70,7 @@ func TestServeDirectoryRootIsPublic(t *testing.T) {
req.Header.Set("Accept", "application/json")
req = req.WithContext(context.WithValue(req.Context(), EmailKey, ""))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body = %s", rec.Code, rec.Body.String())
@@ -100,7 +100,7 @@ func TestServeDirectoryRootIsPublic(t *testing.T) {
req.Header.Set("Accept", "application/json")
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "admin@example.com"))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("admin status = %d, want 200", rec.Code)
@@ -120,7 +120,7 @@ func TestServeDirectoryRootIsPublic(t *testing.T) {
req.Header.Set("Accept", "application/json")
req = req.WithContext(context.WithValue(req.Context(), EmailKey, ""))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code != http.StatusForbidden {
t.Errorf("private subdir for anonymous: status = %d, want 403", rec.Code)
@@ -162,7 +162,7 @@ func TestServeDirectoryRedirectsTableRowsDir(t *testing.T) {
req.Header.Set("Accept", "text/html")
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "casey@example.com"))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status = %d, want 302; body = %s", rec.Code, rec.Body.String())
@@ -177,7 +177,7 @@ func TestServeDirectoryRedirectsTableRowsDir(t *testing.T) {
req.Header.Set("Accept", "application/json")
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "casey@example.com"))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body = %s", rec.Code, rec.Body.String())
@@ -196,7 +196,7 @@ func TestServeDirectoryRedirectsTableRowsDir(t *testing.T) {
req.Header.Set("Accept", "text/html")
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "casey@example.com"))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code == http.StatusFound {
t.Fatalf("got 302 to %q for non-table dir", rec.Header().Get("Location"))
@@ -225,7 +225,7 @@ func TestServeDirectoryRedirectsDefaultMdl(t *testing.T) {
req.Header.Set("Accept", "text/html")
req = req.WithContext(context.WithValue(req.Context(), EmailKey, "casey@example.com"))
rec := httptest.NewRecorder()
- ServeDirectory(cfg, rec, req)
+ ServeDirectory(cfg, nil, rec, req)
if rec.Code != http.StatusFound {
t.Fatalf("status = %d, want 302; body = %s", rec.Code, rec.Body.String())
diff --git a/zddc/internal/handler/tables.html b/zddc/internal/handler/tables.html
index bfafbb9..b2ae7e3 100644
--- a/zddc/internal/handler/tables.html
+++ b/zddc/internal/handler/tables.html
@@ -1300,7 +1300,7 @@ body.help-open .app-header {