feat(zddc): Phase 4b — grid mode driven by cascade default_tool

The /incoming/ path regex in browse/js/grid.js was the second-most
visible client-side hardcode of the canonical convention. Migrating
it to the cascade:

  Header surface:
    X-ZDDC-Default-Tool: <name>   The cascade-resolved default tool
                                  for the listing's directory. Empty
                                  header = no default declared.

  Client wiring:
    loader.fetchServerChildren reads the header into
    state.scopeDefaultTool on every listing fetch (initial mount,
    rescope on dblclick, popstate). grid.classifierAvailableHere
    now returns scopeDefaultTool === 'classifier' instead of
    regex-matching the URL.

  Effect:
    Grid mode auto-activates wherever the cascade picks classifier
    as the default — currently archive/<party>/incoming per
    defaults.zddc.yaml. An operator who sets default_tool: classifier
    on a custom directory gets grid mode there too, no code change.
    An operator who removes the default at incoming sees grid mode
    stop auto-activating there.

  Bootstrap timing fix:
    The initial events.init() runs applyResolvedViewMode before the
    detection fetch completes, so state.scopeDefaultTool is empty
    at that point and grid never auto-activates on first paint.
    app.js bootstrap now re-applies the resolved view mode after
    autoDetectServerMode returns, so a fresh /incoming URL lands
    on grid mode immediately.

The /incoming/ regex is gone. Two client hardcodes remaining
(archive source heuristics, shared/nav stage strip) — Phase 4c/d.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-11 16:15:25 -05:00
parent 4b04f61e4b
commit d90975662f
6 changed files with 34 additions and 14 deletions

View file

@ -34,6 +34,12 @@
events.statusInfo('Loaded ' + detected.entries.length + ' item'
+ (detected.entries.length === 1 ? '' : 's')
+ ' from ' + detected.path);
// The initial events.init() applied view mode before the
// cascade headers were available (no fetch yet). Now that
// state.scopeDefaultTool is set from the detection
// response, re-resolve so an /incoming URL auto-activates
// grid mode.
if (events.applyResolvedViewMode) events.applyResolvedViewMode();
}
// Else: empty state stays visible; user can click Select Directory.

View file

@ -2,11 +2,11 @@
// as an iframe scoped to the current directory so users get classifier's
// full bulk-rename workflow without leaving browse.
//
// Availability: only inside an `incoming/` subtree (case-insensitive).
// Working/staging support the classifier tool at the URL level, but
// they're file-staging contexts in normal use, not rename surfaces —
// the Grid toggle is for the inbound side. Outside an incoming/ path,
// the Grid button is hidden entirely (no explanatory empty state).
// Availability: the cascade decides. Grid auto-activates wherever the
// .zddc cascade resolves default_tool=classifier (defaults.zddc.yaml
// declares this for archive/<party>/incoming/). Operators can extend
// — e.g. setting default_tool=classifier on a custom dir activates
// grid mode there too — without touching this code.
//
// Iframe src resolution: <currentDirURL>/classifier.html. Iframe
// embedding only works in server mode; file:// pages don't get the
@ -18,10 +18,11 @@
var mounted = false;
function classifierAvailableHere() {
// Grid is the classifier-embedded view. Only meaningful in
// incoming/ — that's where bulk-rename actually happens.
var path = (window.location && window.location.pathname) || '';
return /\/incoming(\/|$)/i.test(path);
// state.scopeDefaultTool is set by the loader from the
// X-ZDDC-Default-Tool response header on every listing fetch.
// Grid mode is meaningful exactly where the cascade picks
// classifier as the default — no client-side path matching.
return state.scopeDefaultTool === 'classifier';
}
function activate() {

View file

@ -55,6 +55,9 @@
// Cascade-resolved scope flags, refreshed on each listing
// fetch from response headers.
// scopeDropTarget: cascade's drop_target at currentPath
scopeDropTarget: false
// scopeDefaultTool: cascade's default_tool at currentPath
// (empty when no default declared)
scopeDropTarget: false,
scopeDefaultTool: ''
};
})();

View file

@ -93,6 +93,12 @@
// — 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';
// X-ZDDC-Default-Tool surfaces the cascade-resolved default
// tool name for the current path. Browse uses it to decide
// grid-mode auto-activation (when default_tool==classifier)
// without re-implementing the cascade client-side.
window.app.state.scopeDefaultTool =
(resp.headers.get('X-ZDDC-Default-Tool') || '').toLowerCase();
if (resp.status === 404) {
return [];
}

View file

@ -130,12 +130,16 @@ func ServeDirectory(cfg config.Config, appsSrv *apps.Server, w http.ResponseWrit
// 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.
// grid-mode auto-activation, future affordances) without
// re-implementing the cascade client-side. Keep the header
// surface tight — only routing-shape signals go here; ACL
// details stay server-side.
if zddc.DropTargetAt(cfg.Root, absDir) {
w.Header().Set("X-ZDDC-Drop-Target", "true")
}
if dt := zddc.DefaultToolAt(cfg.Root, absDir); dt != "" {
w.Header().Set("X-ZDDC-Default-Tool", dt)
}
if strings.Contains(accept, "application/json") {
// Content-hash ETag on the listing payload. Re-fetched on every

View file

@ -1300,7 +1300,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title" id="table-title">ZDDC Table</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 21:11:16 · 6310afa-dirty</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 21:14:28 · 4b04f61-dirty</span></span>
</div>
</div>
<div class="header-right">