diff --git a/zddc/internal/apps/embedded/archive.html b/zddc/internal/apps/embedded/archive.html index 3ea1ead..f8fbcf8 100644 --- a/zddc/internal/apps/embedded/archive.html +++ b/zddc/internal/apps/embedded/archive.html @@ -2582,7 +2582,7 @@ td[data-field="trackingNumber"] {
ZDDC Archive - v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b + v0.0.27-beta · 2026-06-03 17:46:04 · 2918248
diff --git a/zddc/internal/apps/embedded/browse.html b/zddc/internal/apps/embedded/browse.html index 54a7d42..a0b16e3 100644 --- a/zddc/internal/apps/embedded/browse.html +++ b/zddc/internal/apps/embedded/browse.html @@ -2484,7 +2484,7 @@ body {
ZDDC Browse - v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b + v0.0.27-beta · 2026-06-03 17:46:04 · 2918248
@@ -11549,10 +11549,10 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr return; } - // Derive the party from the path: archive//incoming//. + // Derive the party from the path: incoming///. var parts = url.replace(/^\/+|\/+$/g, '').split('/'); - var partyIdx = parts.indexOf('archive'); - var party = (partyIdx >= 0 && parts[partyIdx + 1]) ? parts[partyIdx + 1] : ''; + var incIdx = parts.indexOf('incoming'); + var party = (incIdx >= 0 && parts[incIdx + 1]) ? parts[incIdx + 1] : ''; var classification = classifyChildren(node, parsedFolder.trackingNumber); @@ -11607,22 +11607,19 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr // stage.js — Stage and Unstage workflow modals. // -// After the layout reshape, working/ and staging/ live INSIDE each -// party folder: archive//working// and -// archive//staging//. Stage and Unstage are now -// per-party — the destination batch is always inside the SAME -// party's staging slot. The party context is read from the source -// file's path. +// In the flat-peer layout working/ and staging/ are top-level peers, +// each partitioned by party: working// and +// staging///. Stage and Unstage are per-party — the +// destination batch is always inside the SAME party's staging peer. The +// party context is read from the source file's path. // -// Stage: move a file from archive//working/<…> into a -// transmittal folder under archive//staging/<…>. Modal lists -// existing transmittal folders in the party's staging/ plus a "New -// transmittal folder…" option that prompts for a ZDDC-conforming -// name and mkdirs it before the move. +// Stage: move a file from working//<…> into a transmittal folder +// under staging//<…>. Modal lists existing transmittal folders in +// the party's staging/ plus a "New transmittal folder…" option that +// prompts for a ZDDC-conforming name and mkdirs it before the move. // -// Unstage: move a file from archive//staging// -// back to the user's archive//working// home -// (overridable). +// Unstage: move a file from staging/// back to +// working// (overridable). // // Both reuse the existing X-ZDDC-Op: move primitive — no new composite // endpoint is needed; the client just orchestrates one POST per file @@ -11643,19 +11640,21 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr // ── Scope detection: path-shape, not cascade-content ────────────── // A file is stageable if its path matches - // //archive//working/<…>. Unstageable if it - // matches //archive//staging//<…>. - // Both are path-shape queries — content/ACL is enforced server- - // side. + // //working//<…>. Unstageable if it matches + // //staging///<…>. Both are path-shape + // queries — content/ACL is enforced server-side. - // projectPartySlot returns { project, party, slot, rest } when - // path matches //archive///, or + var WORKSPACE_PEERS = { working: 1, staging: 1, reviewing: 1, incoming: 1 }; + + // projectPartySlot returns { project, party, slot, rest } when path + // matches //// for a workspace peer, or // null on non-match. function projectPartySlot(path) { var rel = path.replace(/^\/+|\/+$/g, '').split('/'); - if (rel.length < 4) return null; - if (rel[1].toLowerCase() !== 'archive') return null; - return { project: rel[0], party: rel[2], slot: rel[3], rest: rel.slice(4) }; + if (rel.length < 3) return null; + var slot = rel[1].toLowerCase(); + if (!WORKSPACE_PEERS[slot]) return null; + return { project: rel[0], slot: slot, party: rel[2], rest: rel.slice(3) }; } function isStageableFile(node) { @@ -11692,7 +11691,7 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr async function fetchStagingFolders(project, party) { var entries = await listDir( - '/' + project + '/archive/' + encodeURIComponent(party) + '/staging/'); + '/' + project + '/staging/' + encodeURIComponent(party) + '/'); return entries .filter(function (e) { return e && e.isDir; }) .map(function (e) { return e.name; }); @@ -11880,11 +11879,11 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr var srcUrl = tree.pathFor(node); var info = projectPartySlot(srcUrl); if (!info || info.slot !== 'working') { - status('Stage applies only to files under archive//working/.', 'error'); + status('Stage applies only to files under working//.', 'error'); return; } - var stagingBase = '/' + info.project + '/archive/' + - encodeURIComponent(info.party) + '/staging/'; + var stagingBase = '/' + info.project + '/staging/' + + encodeURIComponent(info.party) + '/'; var folders; try { folders = await fetchStagingFolders(info.project, info.party); } catch (e) { @@ -11922,12 +11921,11 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr var srcUrl = tree.pathFor(node); var info = projectPartySlot(srcUrl); if (!info || info.slot !== 'staging') { - status('Unstage applies only to files under archive//staging/.', 'error'); + status('Unstage applies only to files under staging//.', 'error'); return; } - var email = await fetchSelfEmail(); - var defaultTarget = '/' + info.project + '/archive/' + - encodeURIComponent(info.party) + '/working/' + (email || '') + '/'; + var defaultTarget = '/' + info.project + '/working/' + + encodeURIComponent(info.party) + '/'; var choice; try { choice = await openUnstagePicker({ fileCount: 1, defaultTarget: defaultTarget }); @@ -13168,49 +13166,36 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr // (^[A-Za-z0-9][A-Za-z0-9.-]*$). function validPartyName(s) { return /^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(s || ''); } - // The project-level folder-nav aggregators. These have no physical - // presence: // lists the parties whose - // archive/// has content. Creating something here means - // creating it under a party — see createInAggregator. - var FOLDER_NAV_SLOTS = { working: 1, staging: 1, reviewing: 1 }; + // The party-partitioned workspace peers. Each is a physical top-level + // directory // whose children are / folders. + // Creating something at a peer root means choosing a party — see + // createInAggregator. (mdl/rsk rows are created via the tables tool; + // archive is the WORM record; ssr is the flat registry — none of those + // use this picker.) + var PARTY_PEERS = { incoming: 1, working: 1, staging: 1, reviewing: 1 }; - // aggregatorRoot returns { project, slot } when parentDir is a - // project-level folder-nav aggregator root (server mode only), else - // null. parentDir is a "///" URL. + // aggregatorRoot returns { project, slot } when parentDir is a party- + // partitioned peer root (server mode only), else null. parentDir is a + // "///" URL. function aggregatorRoot(parentDir) { if (state.source !== 'server') return null; var segs = (parentDir || '').replace(/^\/+|\/+$/g, '').split('/'); if (segs.length !== 2 || !segs[0]) return null; - var slot = segs[1].toLowerCase(); - return FOLDER_NAV_SLOTS[slot] ? { project: segs[0], slot: slot } : null; + var peer = segs[1].toLowerCase(); + return PARTY_PEERS[peer] ? { project: segs[0], slot: peer } : null; } - // rewriteAggregatorPath maps a path UNDER a folder-nav aggregator - // (a party already chosen — ///[/]) to its - // canonical archive path //archive//[/], - // mirroring the server's folder-nav redirect. Returns null when - // parentDir isn't under such an aggregator (root case is handled by - // aggregatorRoot + the picker). Covers right-clicking a party row - // shown in an aggregator listing so "New folder" doesn't 409. - function rewriteAggregatorPath(parentDir) { - if (state.source !== 'server') return null; - var segs = (parentDir || '').replace(/^\/+|\/+$/g, '').split('/'); - if (segs.length < 3 || !segs[0]) return null; - var slot = segs[1].toLowerCase(); - if (!FOLDER_NAV_SLOTS[slot]) return null; - var p = '/' + segs[0] + '/archive/' + segs[2] + '/' + slot + '/'; - var rest = segs.slice(3); - if (rest.length) p += rest.join('/') + '/'; - return p; - } - - // List the parties under a project's archive/ (folder names), sorted. + // List the registered parties for a project — one ssr/.yaml per + // party (the authoritative registry). A party "exists" iff its ssr row + // exists, so this is the canonical source for the picker. Returns [] + // on error. async function fetchParties(project) { try { - var entries = await loader.fetchServerChildren('/' + project + '/archive/'); + var entries = await loader.fetchServerChildren('/' + project + '/ssr/'); return entries - .filter(function (e) { return e.isDir; }) - .map(function (e) { return e.name; }) + .filter(function (e) { return !e.isDir && /\.yaml$/i.test(e.name); }) + .map(function (e) { return e.name.replace(/\.yaml$/i, ''); }) + .filter(function (n) { return n !== 'table' && n !== 'form'; }) .sort(function (a, b) { return a.localeCompare(b); }); } catch (_e) { return []; } } @@ -13236,8 +13221,8 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr box.innerHTML = '

New ' + kindWord + ' in ' + escapeHtml(opts.slot) + '/

' + '

' + - escapeHtml(opts.slot) + '/ aggregates each party’s work, so it has no folder of its own. ' + - 'Pick the party this ' + kindWord + ' belongs to — it lands under archive/<party>/' + escapeHtml(opts.slot) + '/.' + + escapeHtml(opts.slot) + '/ is partitioned by party. ' + + 'Pick the party this ' + kindWord + ' belongs to — it lands under ' + escapeHtml(opts.slot) + '/<party>/.' + '

' + '
' + (partyList || 'No parties yet — create one below.') + @@ -13288,13 +13273,15 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr var nv = validateName(box.querySelector('#pp-name').value); if (!nv.ok) { statusError(nv.msg); return; } close(); - resolve({ party: party, name: nv.name }); + resolve({ party: party, name: nv.name, isNew: sel.value === '__new__' }); }); }); } - // createInAggregator routes a New folder/file in a virtual aggregator - // root to archive/// after prompting for the party. + // createInAggregator routes a New folder/file at a party-peer root to + // the physical /// after prompting for the + // party. A brand-new party is registered first by creating its + // ssr/.yaml row (the authoritative registry; party_source: ssr). async function createInAggregator(agg, kind) { var up = window.app.modules.upload; if (!up) return; @@ -13303,42 +13290,45 @@ var e=function(t,n){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Arr if (!choice) return; // Party names are validated to a URL-safe charset, so no encoding // needed for the party segment; makeDir/makeFile encode the leaf. - var targetDir = '/' + agg.project + '/archive/' + choice.party + '/' + agg.slot + '/'; + var targetDir = '/' + agg.project + '/' + agg.slot + '/' + choice.party + '/'; try { + if (choice.isNew) { + // Register the party: its existence is ssr/.yaml. + await up.makeFile('/' + agg.project + '/ssr/', choice.party + '.yaml', + 'kind: SSR\n', 'application/yaml; charset=utf-8'); + } if (kind === 'folder') { await up.makeDir(targetDir, choice.name); - statusInfo('Created ' + choice.party + '/' + agg.slot + '/' + choice.name); + statusInfo('Created ' + choice.party + '/' + choice.name + ' in ' + agg.slot + '/'); } else { var name = /\.(md|markdown)$/i.test(choice.name) ? choice.name : choice.name + '.md'; var template = '# ' + name.replace(/\.(md|markdown)$/i, '') + '\n\n'; await up.makeFile(targetDir, name, template, 'text/markdown; charset=utf-8'); - statusInfo('Created ' + choice.party + '/' + agg.slot + '/' + name); + statusInfo('Created ' + choice.party + '/' + name + ' in ' + agg.slot + '/'); } } catch (e) { var msg = (e && e.message) || String(e); if (/\b403\b/.test(msg)) { - statusError('Not allowed — creating a new party requires the document-controller role.'); + statusError('Not allowed — registering a new party requires the document-controller role.'); + } else if (/\b409\b/.test(msg)) { + statusError('Unknown party — register it first (document controller).'); } else { statusError('Create failed: ' + msg); } return; } - // Refresh the aggregator view — the party now appears if it had no - // content before. await reloadDir('/' + agg.project + '/' + agg.slot + '/'); } async function createInDir(parentDir, kind) { var up = window.app.modules.upload; if (!up) return; - // A project-level folder-nav aggregator (working/staging/reviewing) - // has no physical home — route through the party picker instead of - // erroring on an unplaceable mkdir/PUT. + // At a party-peer root (incoming/working/staging/reviewing) the + // create needs a party — route through the picker. Deeper paths + // (a party already chosen, e.g. working//…) are physical and + // created directly. var agg = aggregatorRoot(parentDir); if (agg) return createInAggregator(agg, kind); - // A party already chosen inside an aggregator view → canonical path. - var rewritten = rewriteAggregatorPath(parentDir); - if (rewritten) parentDir = rewritten; var promptMsg = kind === 'folder' ? 'New folder name (under ' + parentDir + '):' : 'New markdown filename (under ' + parentDir + '):'; diff --git a/zddc/internal/apps/embedded/classifier.html b/zddc/internal/apps/embedded/classifier.html index 071ecc4..e11be3d 100644 --- a/zddc/internal/apps/embedded/classifier.html +++ b/zddc/internal/apps/embedded/classifier.html @@ -1793,7 +1793,7 @@ body.is-elevated::after {
ZDDC Classifier - v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b + v0.0.27-beta · 2026-06-03 17:46:04 · 2918248
diff --git a/zddc/internal/apps/embedded/index.html b/zddc/internal/apps/embedded/index.html index f4783cd..1192262 100644 --- a/zddc/internal/apps/embedded/index.html +++ b/zddc/internal/apps/embedded/index.html @@ -1536,7 +1536,7 @@ body {
ZDDC - v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b + v0.0.27-beta · 2026-06-03 17:46:04 · 2918248
diff --git a/zddc/internal/apps/embedded/transmittal.html b/zddc/internal/apps/embedded/transmittal.html index 05e1003..dce4ac7 100644 --- a/zddc/internal/apps/embedded/transmittal.html +++ b/zddc/internal/apps/embedded/transmittal.html @@ -2635,7 +2635,7 @@ dialog.modal--narrow {
ZDDC Transmittal - v0.0.27-beta · 2026-06-03 13:55:32 · 3e8737b + v0.0.27-beta · 2026-06-03 17:46:04 · 2918248
JavaScript not available