feat: virtual fallback for archive/<party>/* folders + incoming fixture data
Three coupled fixes:
1. landing MDL card: Open button now navigates to /<project>/archive/
<party>/mdl (no trailing slash) so the tables tool loads. The
slash form would route to browse instead, which is not what users
want when they click "Open MDL".
2. zddc-server canonical-folder fallback extended to
archive/<party>/{mdl,incoming,received,issued}. New
zddc.IsArchivePartyFolder() recognises any of the four party
folders at depth 4. fs.ListDirectory returns [] for missing
on-disk variants (mirroring the project-root behavior added in
commit 3fc3717); the dispatcher routes slash forms to
ServeDirectory and the no-slash mdl form to ServeTable, with
non-mdl no-slash forms 302'ing to the slash form.
So /Project-N/archive/<party>/incoming/ now lands on an empty
browse listing rather than 404 when nobody has dropped files yet.
3. Fixture seeded with 3 files per party under incoming/ — naming
intentionally NOT in transmittal-envelope form, so classifier
(loaded automatically by browse's grid mode at /incoming/
per the URL-driven view convention) has something to rename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ab44d75d03
commit
5debd552ae
6 changed files with 75 additions and 15 deletions
|
|
@ -704,7 +704,11 @@
|
||||||
function openSelectedMdl() {
|
function openSelectedMdl() {
|
||||||
var party = mdlSelect.value;
|
var party = mdlSelect.value;
|
||||||
if (!party) return;
|
if (!party) return;
|
||||||
var url = '/' + p + '/archive/' + encodeURIComponent(party) + '/mdl/';
|
// No trailing slash: per the convention, the no-slash form
|
||||||
|
// serves the tables tool with the MDL view. The slash form
|
||||||
|
// would serve browse, which is not what the user wants when
|
||||||
|
// they click "Open MDL".
|
||||||
|
var url = '/' + p + '/archive/' + encodeURIComponent(party) + '/mdl';
|
||||||
window.location.assign(url);
|
window.location.assign(url);
|
||||||
}
|
}
|
||||||
mdlOpenBtn.addEventListener('click', openSelectedMdl);
|
mdlOpenBtn.addEventListener('click', openSelectedMdl);
|
||||||
|
|
|
||||||
|
|
@ -447,10 +447,29 @@ cmd_build() {
|
||||||
|
|
||||||
for party in $parties; do
|
for party in $parties; do
|
||||||
party_dir="$proj_dir/archive/$party"
|
party_dir="$proj_dir/archive/$party"
|
||||||
mkdir -p "$party_dir/received" "$party_dir/issued"
|
mkdir -p "$party_dir/received" "$party_dir/issued" "$party_dir/incoming"
|
||||||
chmod 0777 "$party_dir" "$party_dir/received" "$party_dir/issued"
|
chmod 0777 "$party_dir" "$party_dir/received" "$party_dir/issued" "$party_dir/incoming"
|
||||||
write_zddc_config "$party_dir/.zddc" party
|
write_zddc_config "$party_dir/.zddc" party
|
||||||
|
|
||||||
|
# Seed incoming/ with a couple of unclassified files (no
|
||||||
|
# transmittal envelope yet) so the grid view has data to
|
||||||
|
# exercise. Stays small — incoming is the staging surface
|
||||||
|
# for files dropped by counterparties; classifier turns
|
||||||
|
# them into named transmittal folders.
|
||||||
|
incoming_seed_count=3
|
||||||
|
j=0
|
||||||
|
while [ "$j" -lt "$incoming_seed_count" ]; do
|
||||||
|
j=$((j + 1))
|
||||||
|
seed_ext=$(pick_word "$EXTENSIONS_WEIGHTED")
|
||||||
|
seed_title="$(random_title)"
|
||||||
|
seed_name="incoming-${j}-${seed_title}.${seed_ext}"
|
||||||
|
seed_path="$party_dir/incoming/$seed_name"
|
||||||
|
# Use the per-transmittal renderer with dummy
|
||||||
|
# tracking/rev/status. classifier reads the bytes,
|
||||||
|
# not the envelope, when renaming.
|
||||||
|
render_file "$seed_ext" "drop-${j}" "_A" "(IFR)" "$seed_title" "$seed_path"
|
||||||
|
done
|
||||||
|
|
||||||
i=0
|
i=0
|
||||||
while [ "$i" -lt "$per_party" ]; do
|
while [ "$i" -lt "$per_party" ]; do
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
|
|
|
||||||
|
|
@ -884,15 +884,16 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
|
||||||
// block → ServeDirectory → embedded browse.html.
|
// block → ServeDirectory → embedded browse.html.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Default-MDL virtual directory at archive/<party>/mdl[/].
|
// Per-party canonical folders at archive/<party>/{mdl,incoming,
|
||||||
// Shape rule mirrors the other canonical folders:
|
// received,issued}[/]. The on-disk folder may not exist yet —
|
||||||
// - no slash → tables app (default tool for mdl/)
|
// dispatch lands on a usable view regardless:
|
||||||
// - slash → browse (ServeDirectory → empty listing for
|
// - no-slash mdl → tables app (default-MDL fallback)
|
||||||
// the not-yet-materialised folder)
|
// - no-slash else → not yet a registered tool → falls
|
||||||
// The dispatcher works without the on-disk dir existing
|
// through to 302 (slash form)
|
||||||
// thanks to fs.ListDirectory's empty-listing fallback +
|
// - slash, all → browse (ServeDirectory → fs.ListDirectory
|
||||||
// RecognizeTableRequest's default-MDL fallback.
|
// empty-listing fallback)
|
||||||
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
||||||
|
rel := strings.Trim(strings.TrimPrefix(urlPath, "/"), "/")
|
||||||
if !strings.HasSuffix(urlPath, "/") {
|
if !strings.HasSuffix(urlPath, "/") {
|
||||||
synth := urlPath + "/table.html"
|
synth := urlPath + "/table.html"
|
||||||
if tr := handler.RecognizeTableRequest(cfg.Root, http.MethodGet, synth); tr != nil {
|
if tr := handler.RecognizeTableRequest(cfg.Root, http.MethodGet, synth); tr != nil {
|
||||||
|
|
@ -904,8 +905,14 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
|
||||||
handler.ServeTable(cfg, tr, w, r)
|
handler.ServeTable(cfg, tr, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if zddc.IsArchivePartyMdlDir(
|
// No-slash forms for non-mdl party folders 302 to
|
||||||
strings.Trim(strings.TrimPrefix(urlPath, "/"), "/")) {
|
// the slash form so they consistently land on the
|
||||||
|
// browse listing.
|
||||||
|
if zddc.IsArchivePartyFolder(rel) {
|
||||||
|
http.Redirect(w, r, urlPath+"/", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if zddc.IsArchivePartyFolder(rel) {
|
||||||
handler.ServeDirectory(cfg, appsSrv, w, r)
|
handler.ServeDirectory(cfg, appsSrv, w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
||||||
// virtualUserHomeEntry below still fires for working/ so the
|
// virtualUserHomeEntry below still fires for working/ so the
|
||||||
// user sees their own home placeholder.
|
// user sees their own home placeholder.
|
||||||
if os.IsNotExist(err) &&
|
if os.IsNotExist(err) &&
|
||||||
(zddc.IsProjectRootFolder(dirPath) || zddc.IsArchivePartyMdlDir(dirPath)) {
|
(zddc.IsProjectRootFolder(dirPath) || zddc.IsArchivePartyFolder(dirPath)) {
|
||||||
entries = nil
|
entries = nil
|
||||||
} else {
|
} else {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -1300,7 +1300,7 @@ body.help-open .app-header {
|
||||||
</svg>
|
</svg>
|
||||||
<div class="header-title-group">
|
<div class="header-title-group">
|
||||||
<span class="app-header__title" id="table-title">ZDDC Table</span>
|
<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 18:10:39 · e85d5fc-dirty</span></span>
|
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-alpha · 2026-05-11 18:30:39 · ab44d75-dirty</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,36 @@ var AutoOwnCanonicalNames = []string{"working", "staging", "incoming"}
|
||||||
// MkdirAll for it.
|
// MkdirAll for it.
|
||||||
var VirtualOnlyCanonicalNames = []string{"reviewing"}
|
var VirtualOnlyCanonicalNames = []string{"reviewing"}
|
||||||
|
|
||||||
|
// IsArchivePartyFolder reports whether dirPath (relative, forward-
|
||||||
|
// slash-separated) names a canonical per-party folder at exactly
|
||||||
|
// depth 4: <project>/archive/<party>/<folder>, where <folder> is one
|
||||||
|
// of mdl/incoming/received/issued (case-insensitive). The party
|
||||||
|
// segment is verbatim.
|
||||||
|
//
|
||||||
|
// Used by listing + dispatch fallbacks so a fresh party that hasn't
|
||||||
|
// yet had files written to each subfolder still lands on a usable
|
||||||
|
// empty browse view rather than 404.
|
||||||
|
func IsArchivePartyFolder(dirPath string) bool {
|
||||||
|
clean := strings.Trim(filepath.ToSlash(dirPath), "/")
|
||||||
|
if clean == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := strings.Split(clean, "/")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(parts[1], "archive") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
leaf := strings.ToLower(parts[3])
|
||||||
|
for _, name := range PartyFolders {
|
||||||
|
if leaf == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// IsArchivePartyMdlDir reports whether dirPath (relative, forward-
|
// IsArchivePartyMdlDir reports whether dirPath (relative, forward-
|
||||||
// slash-separated) names the default-MDL pattern at exactly depth 4:
|
// slash-separated) names the default-MDL pattern at exactly depth 4:
|
||||||
// <project>/archive/<party>/mdl. Match is case-insensitive on the
|
// <project>/archive/<party>/mdl. Match is case-insensitive on the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue