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() {
|
||||
var party = mdlSelect.value;
|
||||
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);
|
||||
}
|
||||
mdlOpenBtn.addEventListener('click', openSelectedMdl);
|
||||
|
|
|
|||
|
|
@ -447,10 +447,29 @@ cmd_build() {
|
|||
|
||||
for party in $parties; do
|
||||
party_dir="$proj_dir/archive/$party"
|
||||
mkdir -p "$party_dir/received" "$party_dir/issued"
|
||||
chmod 0777 "$party_dir" "$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" "$party_dir/incoming"
|
||||
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
|
||||
while [ "$i" -lt "$per_party" ]; do
|
||||
i=$((i + 1))
|
||||
|
|
|
|||
|
|
@ -884,15 +884,16 @@ func dispatch(cfg config.Config, idx *archive.Index, ring *handler.LogRing, apps
|
|||
// block → ServeDirectory → embedded browse.html.
|
||||
}
|
||||
}
|
||||
// Default-MDL virtual directory at archive/<party>/mdl[/].
|
||||
// Shape rule mirrors the other canonical folders:
|
||||
// - no slash → tables app (default tool for mdl/)
|
||||
// - slash → browse (ServeDirectory → empty listing for
|
||||
// the not-yet-materialised folder)
|
||||
// The dispatcher works without the on-disk dir existing
|
||||
// thanks to fs.ListDirectory's empty-listing fallback +
|
||||
// RecognizeTableRequest's default-MDL fallback.
|
||||
// Per-party canonical folders at archive/<party>/{mdl,incoming,
|
||||
// received,issued}[/]. The on-disk folder may not exist yet —
|
||||
// dispatch lands on a usable view regardless:
|
||||
// - no-slash mdl → tables app (default-MDL fallback)
|
||||
// - no-slash else → not yet a registered tool → falls
|
||||
// through to 302 (slash form)
|
||||
// - slash, all → browse (ServeDirectory → fs.ListDirectory
|
||||
// empty-listing fallback)
|
||||
if r.Method == http.MethodGet || r.Method == http.MethodHead {
|
||||
rel := strings.Trim(strings.TrimPrefix(urlPath, "/"), "/")
|
||||
if !strings.HasSuffix(urlPath, "/") {
|
||||
synth := urlPath + "/table.html"
|
||||
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)
|
||||
return
|
||||
}
|
||||
} else if zddc.IsArchivePartyMdlDir(
|
||||
strings.Trim(strings.TrimPrefix(urlPath, "/"), "/")) {
|
||||
// No-slash forms for non-mdl party folders 302 to
|
||||
// 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)
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ func ListDirectory(ctx context.Context, decider policy.Decider, fsRoot, dirPath,
|
|||
// virtualUserHomeEntry below still fires for working/ so the
|
||||
// user sees their own home placeholder.
|
||||
if os.IsNotExist(err) &&
|
||||
(zddc.IsProjectRootFolder(dirPath) || zddc.IsArchivePartyMdlDir(dirPath)) {
|
||||
(zddc.IsProjectRootFolder(dirPath) || zddc.IsArchivePartyFolder(dirPath)) {
|
||||
entries = nil
|
||||
} else {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -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 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 class="header-right">
|
||||
|
|
|
|||
|
|
@ -49,6 +49,36 @@ var AutoOwnCanonicalNames = []string{"working", "staging", "incoming"}
|
|||
// MkdirAll for it.
|
||||
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-
|
||||
// slash-separated) names the default-MDL pattern at exactly depth 4:
|
||||
// <project>/archive/<party>/mdl. Match is case-insensitive on the
|
||||
|
|
|
|||
Loading…
Reference in a new issue