Compare commits

..

No commits in common. "4830cec2f8cb8f9a9a7ce9cd2e60ecca706e176f" and "7c158be73bbea8b39704cd139d36610760dea33f" have entirely different histories.

9 changed files with 27 additions and 78 deletions

View file

@ -111,39 +111,33 @@
var visible = null; // { folders, files } while filtering, else null var visible = null; // { folders, files } while filtering, else null
function computeVisible() { function computeVisible() {
var c = window.app.modules.classify; var c = window.app.modules.classify;
var folders = Object.create(null), files = Object.create(null), counts = Object.create(null); var folders = Object.create(null), files = Object.create(null);
var nf = filterActive(); var nf = filterActive();
function walk(folder, ancMatched) { function walk(folder, ancMatched) {
var selfMatch = nf && nameHit(folder.path || folder.name); var selfMatch = nf && nameHit(folder.path || folder.name);
var matched = ancMatched || selfMatch; var matched = ancMatched || selfMatch;
var show = false, hasFile = false, descMatch = false; var show = false, hasFile = false, descMatch = false;
// Post-filter counts for the row's "direct+total" badge: direct =
// immediate visible children/files, total = visible across the subtree.
var dDir = 0, tDir = 0, dFile = 0, tFile = 0;
(folder.children || []).forEach(function (ch) { (folder.children || []).forEach(function (ch) {
var r = walk(ch, matched); var r = walk(ch, matched);
if (r.show) { show = true; dDir++; tDir += 1 + r.tDir; } if (r.show) show = true;
if (r.hasFile) hasFile = true; if (r.hasFile) hasFile = true;
if (r.subtreeMatch) descMatch = true; // a child leads to a match if (r.subtreeMatch) descMatch = true; // a child leads to a match
tFile += r.tFile;
}); });
(folder.files || []).forEach(function (f) { (folder.files || []).forEach(function (f) {
hasFile = true; hasFile = true;
if (!classifyAllows(f)) return; if (!classifyAllows(f)) return;
var fileMatch = nf && nameHit(c.srcKeyForFile(f)); var fileMatch = nf && nameHit(c.srcKeyForFile(f));
if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; dFile++; } if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; }
if (fileMatch) descMatch = true; // a match sits directly in this folder if (fileMatch) descMatch = true; // a match sits directly in this folder
}); });
tFile += dFile;
if (matched) show = true; if (matched) show = true;
// "Show Empty" off → hide folders whose whole subtree holds no files. // "Show Empty" off → hide folders whose whole subtree holds no files.
if (!hasFile && !showEmpty && !matched) show = false; if (!hasFile && !showEmpty && !matched) show = false;
if (show) folders[folder.path] = true; if (show) folders[folder.path] = true;
counts[folder.path] = { dDir: dDir, tDir: tDir, dFile: dFile, tFile: tFile }; return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch };
return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch, tDir: tDir, tFile: tFile };
} }
(window.app.folderTree || []).forEach(function (root) { walk(root, false); }); (window.app.folderTree || []).forEach(function (root) { walk(root, false); });
return { folders: folders, files: files, counts: counts }; return { folders: folders, files: files };
} }
function folderShown(folder) { return !visible || !!visible.folders[folder.path]; } function folderShown(folder) { return !visible || !!visible.folders[folder.path]; }
function fileShown(file) { function fileShown(file) {
@ -217,13 +211,8 @@
const done = st === 'done'; const done = st === 'done';
// When fully scanned both numbers are blue; .done turns the labels blue too. // When fully scanned both numbers are blue; .done turns the labels blue too.
if (done) el.classList.add('done'); if (done) el.classList.add('done');
// While a filter (autofilter or a Show checkbox) is narrowing the tree, const dDir = folder.subdirCount || 0, tDir = folder.runDirs || 0;
// the badge counts what's VISIBLE; otherwise the raw scanned totals. const dFile = folder.fileCount || 0, tFile = folder.runFiles || 0;
const vc = visible && visible.counts && visible.counts[folder.path];
const dDir = vc ? vc.dDir : (folder.subdirCount || 0);
const tDir = vc ? vc.tDir : (folder.runDirs || 0);
const dFile = vc ? vc.dFile : (folder.fileCount || 0);
const tFile = vc ? vc.tFile : (folder.runFiles || 0);
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode('(')); frag.appendChild(document.createTextNode('('));

View file

@ -849,35 +849,6 @@ test('filter does not open collapsed branches; non-matching siblings hide', asyn
expect(r.files).toEqual([]); expect(r.files).toEqual([]);
}); });
test('folder count badge shows post-filter totals', async ({ page }) => {
await page.click('#modeClassifyBtn');
const r = await page.evaluate(() => {
window.app.folderTree = [{
name: 'Root', path: 'Root', expanded: true, scanState: 'done',
subdirCount: 2, runDirs: 2, fileCount: 0, runFiles: 3, files: [], children: [
{ name: 'A', path: 'Root/A', expanded: true, scanState: 'done', subdirCount: 0, runDirs: 0, fileCount: 2, runFiles: 2, children: [], files: [
{ originalFilename: 'alpha report', extension: 'pdf', folderPath: 'Root/A' },
{ originalFilename: 'beta memo', extension: 'pdf', folderPath: 'Root/A' },
] },
{ name: 'B', path: 'Root/B', expanded: true, scanState: 'done', subdirCount: 0, runDirs: 0, fileCount: 1, runFiles: 1, children: [], files: [
{ originalFilename: 'gamma note', extension: 'pdf', folderPath: 'Root/B' },
] },
],
}];
const tree = window.app.modules.tree;
const rootCount = () => { const e = document.querySelector('#folderTree .folder-item .folder-count'); return e ? e.textContent : null; };
tree.render();
const before = rootCount(); // no filter → raw scan totals
tree.setNameFilter('alpha'); // matches one file, in folder A only
const after = rootCount();
return { before, after };
});
expect(r.before).toContain('2 folders'); // raw: 2 subfolders…
expect(r.before).toContain('0+3 files'); // …3 files in the subtree
expect(r.after).toContain('1 folder'); // filtered: only A is visible
expect(r.after).toContain('0+1 file'); // …holding the single matching file
});
test('snapshot: a scanned zip subtree round-trips with its virtual members', async ({ page }) => { test('snapshot: a scanned zip subtree round-trips with its virtual members', async ({ page }) => {
const r = await page.evaluate(() => { const r = await page.evaluate(() => {
const sc = window.app.modules.scanner; const sc = window.app.modules.scanner;

View file

@ -2717,7 +2717,7 @@ td[data-field="trackingNumber"] {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span> <span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data"></button>

View file

@ -2824,7 +2824,7 @@ li.CodeMirror-hint-active {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Browse</span> <span class="app-header__title">ZDDC Browse</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh listing" aria-label="Refresh listing"></button>

View file

@ -2421,7 +2421,7 @@ input.tfile__name:focus { border-color: var(--primary); background: var(--bg); o
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span> <span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
</div> </div>
<button id="addDirectoryBtn" class="btn btn-primary">Use Local Directory</button> <button id="addDirectoryBtn" class="btn btn-primary">Use 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> <button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory" aria-label="Refresh" style="font-size:1.1rem;"></button>
@ -10083,39 +10083,33 @@ X.B(E,Y);return E}return J}())
var visible = null; // { folders, files } while filtering, else null var visible = null; // { folders, files } while filtering, else null
function computeVisible() { function computeVisible() {
var c = window.app.modules.classify; var c = window.app.modules.classify;
var folders = Object.create(null), files = Object.create(null), counts = Object.create(null); var folders = Object.create(null), files = Object.create(null);
var nf = filterActive(); var nf = filterActive();
function walk(folder, ancMatched) { function walk(folder, ancMatched) {
var selfMatch = nf && nameHit(folder.path || folder.name); var selfMatch = nf && nameHit(folder.path || folder.name);
var matched = ancMatched || selfMatch; var matched = ancMatched || selfMatch;
var show = false, hasFile = false, descMatch = false; var show = false, hasFile = false, descMatch = false;
// Post-filter counts for the row's "direct+total" badge: direct =
// immediate visible children/files, total = visible across the subtree.
var dDir = 0, tDir = 0, dFile = 0, tFile = 0;
(folder.children || []).forEach(function (ch) { (folder.children || []).forEach(function (ch) {
var r = walk(ch, matched); var r = walk(ch, matched);
if (r.show) { show = true; dDir++; tDir += 1 + r.tDir; } if (r.show) show = true;
if (r.hasFile) hasFile = true; if (r.hasFile) hasFile = true;
if (r.subtreeMatch) descMatch = true; // a child leads to a match if (r.subtreeMatch) descMatch = true; // a child leads to a match
tFile += r.tFile;
}); });
(folder.files || []).forEach(function (f) { (folder.files || []).forEach(function (f) {
hasFile = true; hasFile = true;
if (!classifyAllows(f)) return; if (!classifyAllows(f)) return;
var fileMatch = nf && nameHit(c.srcKeyForFile(f)); var fileMatch = nf && nameHit(c.srcKeyForFile(f));
if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; dFile++; } if (!nf || matched || fileMatch) { files[c.srcKeyForFile(f)] = true; show = true; }
if (fileMatch) descMatch = true; // a match sits directly in this folder if (fileMatch) descMatch = true; // a match sits directly in this folder
}); });
tFile += dFile;
if (matched) show = true; if (matched) show = true;
// "Show Empty" off → hide folders whose whole subtree holds no files. // "Show Empty" off → hide folders whose whole subtree holds no files.
if (!hasFile && !showEmpty && !matched) show = false; if (!hasFile && !showEmpty && !matched) show = false;
if (show) folders[folder.path] = true; if (show) folders[folder.path] = true;
counts[folder.path] = { dDir: dDir, tDir: tDir, dFile: dFile, tFile: tFile }; return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch };
return { show: show, hasFile: hasFile, subtreeMatch: descMatch || selfMatch, tDir: tDir, tFile: tFile };
} }
(window.app.folderTree || []).forEach(function (root) { walk(root, false); }); (window.app.folderTree || []).forEach(function (root) { walk(root, false); });
return { folders: folders, files: files, counts: counts }; return { folders: folders, files: files };
} }
function folderShown(folder) { return !visible || !!visible.folders[folder.path]; } function folderShown(folder) { return !visible || !!visible.folders[folder.path]; }
function fileShown(file) { function fileShown(file) {
@ -10189,13 +10183,8 @@ X.B(E,Y);return E}return J}())
const done = st === 'done'; const done = st === 'done';
// When fully scanned both numbers are blue; .done turns the labels blue too. // When fully scanned both numbers are blue; .done turns the labels blue too.
if (done) el.classList.add('done'); if (done) el.classList.add('done');
// While a filter (autofilter or a Show checkbox) is narrowing the tree, const dDir = folder.subdirCount || 0, tDir = folder.runDirs || 0;
// the badge counts what's VISIBLE; otherwise the raw scanned totals. const dFile = folder.fileCount || 0, tFile = folder.runFiles || 0;
const vc = visible && visible.counts && visible.counts[folder.path];
const dDir = vc ? vc.dDir : (folder.subdirCount || 0);
const tDir = vc ? vc.tDir : (folder.runDirs || 0);
const dFile = vc ? vc.dFile : (folder.fileCount || 0);
const tFile = vc ? vc.tFile : (folder.runFiles || 0);
const frag = document.createDocumentFragment(); const frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode('(')); frag.appendChild(document.createTextNode('('));

View file

@ -1793,7 +1793,7 @@ body {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC</span> <span class="app-header__title">ZDDC</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">

View file

@ -2770,7 +2770,7 @@ dialog.modal--narrow {
</svg> </svg>
<div class="header-title-group"> <div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span> <span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:57:52 · 8473ed3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
</div> </div>
<span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span> <span id="no-js-notice" class="text-gray-400 text-xs italic">JavaScript not available</span>
<!-- Publish split-button (Transmittal-specific primary action; <!-- Publish split-button (Transmittal-specific primary action;

View file

@ -1,8 +1,8 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line. # Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3 archive=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
transmittal=v0.0.27-beta · 2026-06-15 14:57:52 · 8473ed3 transmittal=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
classifier=v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3 classifier=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
landing=v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3 landing=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
form=v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3 form=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
tables=v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3 tables=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a
browse=v0.0.27-beta · 2026-06-15 14:57:53 · 8473ed3 browse=v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a

View file

@ -1770,7 +1770,7 @@ body.is-elevated::after {
</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.27-beta · 2026-06-15 14:57:53 · 8473ed3</span></span> <span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.27-beta · 2026-06-15 14:32:01 · 0d8125a</span></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">