chore(embedded): cut v0.0.17-beta

This commit is contained in:
ZDDC 2026-05-10 07:57:40 -05:00
parent 315d039880
commit 4cd39998aa
8 changed files with 358 additions and 16 deletions

View file

@ -2267,7 +2267,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · log-citrus-hazel</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -969,7 +969,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · log-citrus-hazel</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>

View file

@ -1486,7 +1486,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · log-citrus-hazel</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>

View file

@ -995,6 +995,128 @@ body {
color: var(--text-muted);
}
/* ── Project mode ──────────────────────────────────────────────────────── */
/* Activated when location.pathname is a single project segment (e.g.
/Project-1). Picker UI is hidden; this block surfaces the four
lifecycle-stage cards and MDL editing instructions. */
.project-title {
font-size: 1.6rem;
margin: 0 0 0.25rem;
font-weight: 600;
}
.project-title__subtle {
color: var(--text-muted);
font-weight: normal;
font-size: 0.9rem;
}
.lead {
color: var(--text-muted);
margin: 0.25rem 0 1.5rem;
}
.stages {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.85rem;
margin: 1rem 0 1.5rem;
}
.stage-card {
display: block;
padding: 1rem 1.1rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
text-decoration: none;
color: var(--text);
transition: border-color 0.15s, box-shadow 0.15s, transform 0.05s;
cursor: pointer;
}
.stage-card:hover {
border-color: var(--primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.stage-card:active {
transform: translateY(1px);
}
.stage-card h3 {
margin: 0 0 0.3rem;
font-size: 1rem;
color: var(--primary);
font-weight: 600;
}
.stage-card p {
margin: 0;
color: var(--text-muted);
font-size: 0.875rem;
}
.browse-link {
display: inline-block;
margin-top: 0.25rem;
color: var(--primary);
text-decoration: none;
cursor: pointer;
}
.browse-link:hover {
text-decoration: underline;
}
#projectView ol {
padding-left: 1.5rem;
}
#projectView ol li {
margin-bottom: 0.4rem;
}
#projectView code {
font-family: var(--font-mono);
background: var(--bg-secondary);
padding: 0.1em 0.35em;
border-radius: 3px;
font-size: 0.86em;
}
#projectView h2 {
font-size: 1.1rem;
margin: 2.25rem 0 0.5rem;
padding-bottom: 0.3rem;
border-bottom: 1px solid var(--border);
font-weight: 600;
}
.party-list {
padding-left: 1.5rem;
margin: 0.4rem 0 1rem;
}
.party-list li {
margin-bottom: 0.25rem;
}
.party-list a {
color: var(--primary);
text-decoration: none;
}
.party-list a:hover {
text-decoration: underline;
}
.party-list-none-yet {
color: var(--text-muted);
font-style: italic;
}
</style>
</head>
<body>
@ -1010,7 +1132,7 @@ body {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · log-citrus-hazel</span></span>
</div>
</div>
<div class="header-right">
@ -1019,7 +1141,9 @@ body {
</div>
</header>
<main class="landing-main">
<main id="landingMain" class="landing-main">
<!-- Picker mode (deployment root /). Project picker + groups. -->
<div id="pickerView">
<!-- Welcome / hero -->
<section class="landing-hero">
<h1>Welcome to the ZDDC Archive</h1>
@ -1078,6 +1202,67 @@ body {
<div class="project-list-loading">Loading projects…</div>
</div>
</div>
</div><!-- /pickerView -->
<!-- Project mode (/<project>). Stage cards + MDL section. Shown
by landing.js when location.pathname is a single segment. -->
<div id="projectView" class="hidden">
<h1 id="projectTitle" class="project-title">
<span id="projectName"></span>
<span class="project-title__subtle">— project workspace</span>
</h1>
<p class="lead">Pick a lifecycle stage, or browse all files.</p>
<div class="stages">
<a class="stage-card" id="stageArchive">
<h3>Archive</h3>
<p>Permanent record of issued and received transmittals, organized by counterparty.</p>
</a>
<a class="stage-card" id="stageWorking">
<h3>Working</h3>
<p>Per-user drafting workspace. Your folder is private by default; you can grant access by editing its <code>.zddc</code> file.</p>
</a>
<a class="stage-card" id="stageStaging">
<h3>Staging</h3>
<p>Outbound transmittals being prepared for issue.</p>
</a>
<a class="stage-card" id="stageReviewing">
<h3>Reviewing</h3>
<p>Pending review responses — inbound submittals paired with their in-progress drafts.</p>
</a>
</div>
<p><a id="browseAllLink" class="browse-link">Browse all files →</a></p>
<h2>Master Deliverables List (MDL)</h2>
<p>Each counterparty in the archive has an MDL — an editable
table of expected deliverables. The default columns mirror
the ZDDC tracking-number components (<code>originator</code>,
<code>phase</code>, <code>project</code>, <code>area</code>,
<code>discipline</code>, <code>type</code>,
<code>sequence</code>, <code>suffix</code>) plus
<code>title</code>, <code>plannedRevision</code>,
<code>plannedDate</code>, <code>status</code>, and
<code>owner</code>.</p>
<p><strong>To edit the MDL for any party:</strong></p>
<ol>
<li>Open the project archive: <a id="archiveBrowseLink"></a></li>
<li>Click into a party's folder (e.g. <code>PartyA</code>)</li>
<li>Click <code>mdl</code> inside the party folder</li>
</ol>
<div id="partyListSection">
<!-- Populated by JS when archive/ enumeration succeeds.
Either a "direct links" block with <ul.party-list> or a
"no parties yet" fallback. -->
</div>
<p>To customize the columns or schema for a specific party, drop
a <code>table.yaml</code> and <code>form.yaml</code> into
<code>archive/&lt;party&gt;/mdl/</code>. Operator-supplied
files override the embedded defaults entirely.</p>
</div><!-- /projectView -->
</main>
<!-- Help Panel -->
@ -2707,9 +2892,163 @@ body {
catch (e) { /* private mode / quota */ }
}
// ── Project mode ─────────────────────────────────────────────────────────
//
// The same landing tool serves at /<project> as the project-workspace
// page. Mode is determined from location.pathname:
//
// / → 'picker' (existing behavior)
// /<single-segment> → 'project'
// /index.html → 'picker' (file:// + standalone-served root)
// anything else → 'picker' (best-effort fallback)
//
// Project mode shows the four canonical lifecycle-stage cards, a
// "browse all files" link, and a Master Deliverables List section
// with direct links to any parties currently in archive/. The party
// list is fetched from <project>/<archive>/?json=1; failures fall
// back to the static "no parties yet" copy.
function detectMode() {
if (typeof location === 'undefined') return 'picker';
var path = location.pathname || '/';
// Strip any trailing /index.html so the deployment-root case
// matches even on file:// or behind some servers.
var trimmed = path.replace(/\/index\.html$/, '/');
if (trimmed === '' || trimmed === '/') return 'picker';
// Single non-slash, non-dot segment → project root.
var parts = trimmed.split('/').filter(Boolean);
if (parts.length === 1 && parts[0].indexOf('.') === -1) {
return 'project';
}
return 'picker';
}
function projectFromPath() {
var parts = (location.pathname || '/').split('/').filter(Boolean);
return parts[0] || '';
}
// Render the project-workspace view: title, four stage links, MDL
// section. Stage hrefs use the no-trailing-slash form so the server
// routes them to each canonical default tool (mdedit for working/,
// transmittal for staging/, etc.). Browse-all and the archive deep
// link use the slash form to land on the directory listing.
async function renderProjectMode() {
var project = projectFromPath();
if (!project) return;
// Hide picker, show project view.
var picker = document.getElementById('pickerView');
var projectView = document.getElementById('projectView');
if (picker) picker.classList.add('hidden');
if (projectView) projectView.classList.remove('hidden');
document.title = project + ' — ZDDC';
var titleEl = document.getElementById('projectName');
if (titleEl) titleEl.textContent = project;
var p = encodeURIComponent(project);
var stages = [
{ id: 'stageArchive', href: '/' + p + '/archive' },
{ id: 'stageWorking', href: '/' + p + '/working' },
{ id: 'stageStaging', href: '/' + p + '/staging' },
{ id: 'stageReviewing', href: '/' + p + '/reviewing' },
];
for (var i = 0; i < stages.length; i++) {
var a = document.getElementById(stages[i].id);
if (a) a.setAttribute('href', stages[i].href);
}
var browseAll = document.getElementById('browseAllLink');
if (browseAll) {
browseAll.setAttribute('href', '/' + p + '/');
browseAll.textContent = 'Browse all files →';
}
var archiveBrowse = document.getElementById('archiveBrowseLink');
if (archiveBrowse) {
archiveBrowse.setAttribute('href', '/' + p + '/archive/');
archiveBrowse.innerHTML = '<code>/' + escapeHtml(project) + '/archive/</code>';
}
// Fetch party list. Best-effort — failures render the
// no-parties-yet fallback. We try /<project>/archive/ — the
// server returns the listing in either lowercase or PascalCase
// form; either yields the same JSON shape via case-insensitive
// URL canonicalization.
var partySection = document.getElementById('partyListSection');
if (!partySection) return;
var parties = await fetchParties(p);
if (parties == null) {
// Network error or unauthenticated — show neither list nor
// explicit "none" message. The page is still usable.
partySection.innerHTML = '';
return;
}
if (parties.length === 0) {
partySection.innerHTML =
'<p class="party-list-none-yet">No party folders yet. The MDL view auto-renders at any '
+ '<code>archive/&lt;party&gt;/mdl/</code> URL, even when the folder doesn\'t exist on '
+ 'disk — so you can start editing an MDL before any transmittals have been exchanged.</p>';
return;
}
var html = '<p><strong>Direct links — parties currently in <code>archive/</code>:</strong></p>'
+ '<ul class="party-list">';
for (var j = 0; j < parties.length; j++) {
var name = parties[j].name;
var url = parties[j].url; // server-provided absolute URL
html += '<li><a href="' + url + 'mdl/">' + escapeHtml(name) + ' MDL →</a></li>';
}
html += '</ul>';
partySection.innerHTML = html;
}
// Returns an array of {name, url} for each party folder in the
// project's archive/, sorted by name. Returns null if the listing
// can't be fetched (offline, 4xx, or non-JSON response). Returns
// [] if the listing succeeds but archive/ is empty / has no
// visible party folders.
async function fetchParties(projectURL) {
try {
var resp = await fetch('/' + projectURL + '/archive/', {
headers: { 'Accept': 'application/json' },
cache: 'no-cache',
credentials: 'same-origin'
});
if (!resp.ok) return null;
var ctype = resp.headers.get('Content-Type') || '';
if (!ctype.toLowerCase().includes('json')) return null;
var data = await resp.json();
if (!Array.isArray(data)) return null;
// Server emits directories with trailing "/" on the name.
// Filter to dirs only, strip the slash for display.
var out = [];
for (var i = 0; i < data.length; i++) {
var e = data[i];
if (!e.is_dir) continue;
var nm = String(e.name || '').replace(/\/$/, '');
if (!nm) continue;
if (nm.charAt(0) === '.' || nm.charAt(0) === '_') continue;
out.push({ name: nm, url: e.url || ('/' + projectURL + '/archive/' + encodeURIComponent(nm) + '/') });
}
out.sort(function (a, b) { return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; });
return out;
} catch (e) {
return null;
}
}
// ── Bootstrap ────────────────────────────────────────────────────────────
async function init() {
if (detectMode() === 'project') {
await renderProjectMode();
return;
}
await initPicker();
}
async function initPicker() {
loadGroups();
urlRestore();
@ -2764,6 +3103,9 @@ body {
saveGroup: saveGroup,
openSelectedVisible: openSelectedVisible,
dismissWarning: dismissWarning,
// Project-mode entry points (also tested directly).
detectMode: detectMode,
renderProjectMode: renderProjectMode,
// Test-only: override the navigation function (avoids the messy
// browser-locked-down state of window.location).
_setNavigate: function(fn) { navigate = fn; }

View file

@ -1925,7 +1925,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · log-citrus-hazel</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary" title="Add a local directory">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh directory" aria-label="Refresh"></button>

View file

@ -2285,7 +2285,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · log-citrus-hazel</span></span>
</div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action;

View file

@ -1,9 +1,9 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
transmittal=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
classifier=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
mdedit=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
landing=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
form=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
tables=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
browse=v0.0.17-beta · 2026-05-10 · iron-ribbon-crystal
archive=v0.0.17-beta · 2026-05-10 · log-citrus-hazel
transmittal=v0.0.17-beta · 2026-05-10 · log-citrus-hazel
classifier=v0.0.17-beta · 2026-05-10 · log-citrus-hazel
mdedit=v0.0.17-beta · 2026-05-10 · log-citrus-hazel
landing=v0.0.17-beta · 2026-05-10 · log-citrus-hazel
form=v0.0.17-beta · 2026-05-10 · log-citrus-hazel
tables=v0.0.17-beta · 2026-05-10 · log-citrus-hazel
browse=v0.0.17-beta · 2026-05-10 · log-citrus-hazel

View file

@ -1092,7 +1092,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-beta · 2026-05-10 · iron-ribbon-crystal</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.17-beta · 2026-05-10 · log-citrus-hazel</span></span>
</div>
</div>
<div class="header-right">