diff --git a/browse/js/init.js b/browse/js/init.js index e58b4b8..0d76df5 100644 --- a/browse/js/init.js +++ b/browse/js/init.js @@ -50,6 +50,11 @@ // Single shared popup window for file preview (across // multiple file clicks). Same pattern as archive's preview. - previewWindow: null + previewWindow: null, + + // Cascade-resolved scope flags, refreshed on each listing + // fetch from response headers. + // scopeDropTarget: cascade's drop_target at currentPath + scopeDropTarget: false }; })(); diff --git a/browse/js/loader.js b/browse/js/loader.js index 5925e09..61d24ef 100644 --- a/browse/js/loader.js +++ b/browse/js/loader.js @@ -85,6 +85,14 @@ headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }); + // Capture cascade-resolved scope flags from response headers + // before bailing on 404. zddc-server emits X-ZDDC-Drop-Target + // for directories the cascade marks as upload destinations + // (see zddc/internal/zddc/lookups.go DropTargetAt). The flag + // is leaf-only — it describes THIS path, not its descendants + // — so a rescope or popstate re-reads it from the new listing. + var dropTargetHdr = (resp.headers.get('X-ZDDC-Drop-Target') || '').toLowerCase(); + window.app.state.scopeDropTarget = dropTargetHdr === 'true'; if (resp.status === 404) { return []; } diff --git a/browse/js/upload.js b/browse/js/upload.js index 9444733..aab51a0 100644 --- a/browse/js/upload.js +++ b/browse/js/upload.js @@ -1,10 +1,16 @@ // upload.js — drag-drop file upload into the current scope. // -// Active only in server mode and only at paths where uploads make -// sense (any segment named working / staging / incoming, case- -// insensitive). At other scopes the handlers stay armed but ignore -// drops silently — there is no visible drop-zone overlay outside an -// upload-eligible context. +// Active only in server mode and only at paths where the cascade +// declares drop_target: true (see zddc/internal/zddc/lookups.go +// DropTargetAt + defaults.zddc.yaml). The loader captures the +// X-ZDDC-Drop-Target response header on every directory listing +// fetch and stamps state.scopeDropTarget; this module just reads it. +// +// At scopes where drop_target is false (or unset), the handlers +// stay armed but ignore drops silently — no visible drop-zone +// overlay. An operator can flip working/staging/incoming on or +// extend the cascade to mark additional dirs as drop targets via +// .zddc; the client follows automatically without code change. // // Wire model: // - dragenter on the document raises a counter; first-enter shows @@ -30,11 +36,6 @@ if (!window.app || !window.app.modules) return; var UPLOAD_MAX_BYTES = 256 * 1024 * 1024; // 256 MiB per file - // Path segments where uploads are allowed. Matches the current - // hardcoded surface (working / staging / incoming). Will become - // configurable when the folders: schema lands. - var UPLOAD_SCOPES = /\/(working|staging|incoming)(\/|$)/i; - var state = window.app.state; var enterCount = 0; var overlayEl = null; @@ -56,8 +57,12 @@ function currentScopeAllows() { if (!state || state.source !== 'server') return false; - var p = state.currentPath || ''; - return UPLOAD_SCOPES.test(p); + // state.scopeDropTarget is set by the loader on every listing + // fetch from the X-ZDDC-Drop-Target response header; it's a + // boolean read of the cascade's effective drop_target flag at + // the current path. Defaults to false when the header is + // absent (older server or non-server response). + return !!state.scopeDropTarget; } function showOverlay() { diff --git a/zddc/internal/handler/directory.go b/zddc/internal/handler/directory.go index ecb00e5..7680282 100644 --- a/zddc/internal/handler/directory.go +++ b/zddc/internal/handler/directory.go @@ -128,6 +128,15 @@ func ServeDirectory(cfg config.Config, appsSrv *apps.Server, w http.ResponseWrit // auto-detect (which fetches the same URL with Accept: JSON). w.Header().Set("Vary", "Accept") + // Surface cascade-resolved scope flags via response headers so + // the browse SPA can render scope-aware UI (drop-zone overlay, + // future affordances) without re-implementing the cascade + // client-side. Keep the header surface tight — only routing- + // shape booleans go here; ACL details stay server-side. + if zddc.DropTargetAt(cfg.Root, absDir) { + w.Header().Set("X-ZDDC-Drop-Target", "true") + } + if strings.Contains(accept, "application/json") { // Content-hash ETag on the listing payload. Re-fetched on every // request (the cascade is walked, ACL filter applied, JSON diff --git a/zddc/internal/handler/tables.html b/zddc/internal/handler/tables.html index 3ea1503..acb2549 100644 --- a/zddc/internal/handler/tables.html +++ b/zddc/internal/handler/tables.html @@ -1300,7 +1300,7 @@ body.help-open .app-header {