The scan is the expensive part (minutes on cloud mounts), so a workspace is now
portable between browsers/machines without re-scanning:
- Per-workspace "Export" downloads the snapshot + classify map as one
<name>.zddc-workspace.json (the source-directory handle is omitted — it can't
be serialized across browsers).
- Landing-page "Import" recreates the workspace from that JSON; the user clicks
"Connect directory" once on the new browser to re-attach the folder (no
re-scan — the snapshot carries the 2-hour walk). A classification-dataset JSON
is rejected with a pointer to the in-app Import.
Also fix the welcome screen clipping its top on short viewports: the base
.empty-state centers with align-items:center, which overflows symmetrically and
puts the card's top out of scroll reach. Center the inner card with auto margins
instead — they collapse when it's taller than the viewport, keeping the top
reachable.
Test: workspace import recreates a transferable record (snapshot + map, no handle); 46 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a "Show Empty" checkbox (classify mode) — when off, folders whose whole
subtree contains no files are hidden, decluttering messy scans.
- Move the Show Unassigned/Assigned/Excluded/Empty filters out of the cramped
pane header into a dedicated "Show …" toolbar row beneath it (wraps cleanly).
- Drop the "X folders selected" text from the folder-tree header (selection
still works; updateSelectedCount guards the now-absent element).
Test: Show Empty off hides file-less folders (classify.spec.js -> 41).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an autofilter input above the source tree and above each target tree.
Typing substring-matches (ANDing space-separated terms) against the full file
path/name (and folder/node names) and reveals every match with the folder
hierarchy leading to it — non-matching branches collapse out, matching branches
auto-expand. So you can type "master deliverables list" and jump straight to it.
- Source tree (tree.js): one-pass visible-set over path+name; composes with the
Show Unassigned/Assigned/Excluded toggles; auto-expands to reveal hits.
- Target trees (target-tree.js): tracking + transmittal nodes are filter-aware
(match node names + each placed file's original/derived name); one shared
query mirrored across both tab inputs.
Tests: source-tree path reveal + tracking-tree node filter (classify.spec.js -> 36).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Classify & Copy interaction pass (replaces the single "Hide Assigned" toggle):
- Source-tree filters: three "Show Unassigned / Show Assigned / Show Excluded"
checkboxes (classify mode only) with live per-tab counts; "Hide Compliant" is
now rename-mode only. Folders with nothing visible collapse out.
- Target tree: ctrl/cmd-click a toggle to expand/collapse the whole subtree.
- Tracking drop-to-any-level: dropping on a node that isn't already a complete
leaf prompts for the remaining levels (e.g. "0001_0 (IFU)"), which are parsed
and nested under the drop target. Dropping on a finished leaf assigns directly.
- Placed-file rows: click to preview; the derived filename is now an inline
input — edit it (full "TRACKING_REV (STATUS) - Title.ext") and the item is
re-filed onto the parsed tracking path (created if needed) + title override.
New classify helpers: trackingNodeComplete, trackingPathLabel. tree.setShowFilters
replaces setHideAssigned. Tests updated/added (classify.spec.js -> 33 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the Classify & Copy add-folder work:
- Add-folder now parses each (brace-expanded) name into the nested tracking
levels it represents — split on "-", then the FINAL "_" splits the leaf
revision. "CPO-0001_0 (IFU)" → CPO / 0001 / 0 (IFU); a braced pattern nests
every expansion and shares common ancestors. New classify.parseFolderLevels
+ addTrackingPath (ensure-path with name reuse).
- Node add/edit/delete controls moved back to the RIGHT of the level name and
revealed on hover (was left + always-visible).
Tests: parseFolderLevels cases + a nested-chain/shared-ancestor test; updated
the "+ Root folder" test for the new nesting (classify.spec.js -> 31 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Classify & Copy polish — in either target tab the goal is to assign or exclude
every left-pane file until nothing remains:
- Hide Assigned checkbox (classify mode, in the folder-tree pane header):
collapses the source tree to only what's left on the ACTIVE axis — hides
files already assigned in the current tab (or excluded) and any folder whose
scanned subtree is thereby empty. Re-renders on tab switch; target-tree
exposes activeAxis().
- Node add/edit/delete controls moved to the LEFT of the level name and made
always-visible (was right-aligned + hover-only), so building/pruning the
tracking and transmittal trees is one click.
- Brace expansion in the add-folder box: "BMB-187023-{PM,EL,EM}-MOM-
{0001-0002,0005}_A (IFR)" creates all 9 folders — {a,b} alternation +
{N-M} zero-padded numeric ranges, cartesian product across groups; a
multi-create is confirmed first. New classify.expandFolderPattern().
Tests: expandFolderPattern unit cases + a Hide-Assigned DOM test
(classify.spec.js → 29 passed; classifier.spec.js → 4 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Welcome: drop the 'absorbed into Browse' notice; bigger, inviting intro with
a two-method tutorial (Classify & copy — recommended/non-destructive; Rename
in place — edits files) and a OneDrive 'keep on device' tip.
- Resumable scan: the snapshot now records per-folder scan state, the workspace
record is created up front, and the partial snapshot is persisted every 5s
during the (slow) scan. scanner.resumeScan() resolves handles for only the
still-pending folders and drains them — so an interrupted scan picks up where
it left off instead of starting over.
- Reconnect on restore: opening a workspace no longer assumes the source is
connected; a header 'Connect directory' button (and a prompt) re-grants the
persisted handle in one click or lets you re-pick it. Until connected you can
still edit the data model; connecting also resumes any pending scan.
- Tests: resume-scan via mock root handle (31 classify/classifier green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The classifier re-scanned the source on every session; on cloud-backed mounts
(OneDrive/Samba) that's minutes of per-op latency. Workspaces fix it: scan a
folder ONCE, snapshot the completed tree, and resume instantly — all
classification runs on the data model; the filesystem is only touched at copy.
- persist.js v2: multi-workspace IndexedDB (tiny 'index' store for the welcome
list + 'data' store holding the source handle, tree snapshot, and map). DB v2.
- scanner.js: snapshotTree()/loadSnapshot() (compact, handle-less, marked done,
totals recomputed) + lazy resolveFileHandle/resolveDirHandle from the root.
- workspace.js: welcome manager (new/open/rename/delete), debounced autosave of
the active workspace, 'Refresh from disk' (re-scan → re-snapshot, path-keyed
map carries over). New workspace = the one slow full scan; reopen = instant.
- copy.js: resolves snapshot files' handles from the workspace root with a
one-click read permission re-grant; missing-on-disk files surface as errors.
- app.js: enterAppShell() shared by rename/workspace flows; exposes setMode;
classify.js decoupled from persistence.
- template/css: welcome workspace list + header 'Workspaces' button.
- tests: snapshot round-trip, persist CRUD + classify-only-preserves-tree,
copy-from-snapshot via mock root handle (28 classify/classifier tests green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Each source file row shows a classification state dot (unassigned →
has-tracking/transmittal → done), and each folder shows an aggregate dot
over its subtree.
- Right-click a file or folder to Exclude/Include from the copy (folder applies
to its whole subtree) or clear an axis; excluded files are struck through and
never copied.
- Cross-tree find is bidirectional: click a placed file in the target pane to
reveal+flash it in the source tree (expanding its folders); click a source
file to switch the target pane to its placed axis and flash the node.
- Target pane now reverse-looks-up over ALL scanned files (the left tree), not
the selection-scoped grid, with placements grouped in one pass per render.
- classify.getAssignment() read-only accessor; 5 new tests (18 total green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In Classify & Copy mode the left tree now lists each folder's files as
draggable rows (with a classification state dot), and folder rows are
draggable for a group-drag of the whole subtree. Target-tree nodes are drop
zones: a tracking folder (any node) or a transmittal bin; dropping assigns the
dragged source key(s) along that axis via classify.place().
- dnd.js: drag-payload bus (keys held in a module var since dataTransfer can't
be read during dragover; carries a marker for the copy cursor).
- tree.js: createFileElement + group-drag dragstart; classify-mode file rows.
- target-tree.js: setupDropZone with dragover highlight + drop assignment
(tracking = any node, transmittal = bins only).
- app.js: source tree re-renders on classify state change.
- 2 DnD drop-handler tests (14 total green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Header gets a Rename / Classify & Copy switch. In Classify & Copy mode the
spreadsheet pane is replaced by a tabbed target pane (By tracking number /
By transmittal), while the source tree stays on the left.
- target-tree.js: renders both trees from classify state; tracking-folder
create/rename/delete (leaf folders styled as the revision); party CRUD +
per-slot inline transmittal-bin form (date + TRN/SUB + seq + optional
status/title); shows the derived filename + a validation badge for each
placed file; live header stats (done / in progress / unassigned / excluded).
- app.js setMode(): swaps panes, toggles classify mode, re-renders both trees.
- 3 UI smoke tests added to classify.spec.js (12 total green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The black-completed vs grey-flashing distinction was too subtle. Completed
numbers (the direct count, always; the +total once final) now render in
var(--primary) — theme-aware blue in both light and dark. While a subtree
is still scanning its +total stays muted grey + pulses, so blue = done,
grey = in progress. Once both numbers are blue the row's folders/files
labels turn blue too (.folder-count.done .ct-label).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The live "Scanning… N folders · M files — <path>" status now lives in a
persistent page footer bar instead of under the folder-tree header.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Counts now read "direct+total" — e.g. "(2+10 folders, 15+300 files)". The
direct number (immediate children) shows as soon as a folder's own directory
is read; the total (whole-subtree) is accumulated progressively and flashes
grey until the subtree is fully scanned, then goes solid. The "+total" is
omitted once done and there's nothing deeper.
- Scan errors (permission denied, network hiccups on a share) now surface as a
toast (de-duped per path) instead of only console noise; a failed folder/zip
is marked done-empty so it doesn't wedge the walk.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the full depth-first "scan everything, then render once + expandAll +
selectAll" walk (which looked stalled and was a render bomb on a large network
drive) with a progressive, breadth-first scan:
- Walks level-by-level behind a bounded worker pool (6), rendering as it goes —
the top folder levels appear immediately, deeper levels fill in the
background. Workers await between directories so the UI stays responsive.
- Live status line under the tree header: "Scanning… N folders · M files —
<current path>", ending "Scanned … in Ts."
- Per-folder state machine (pending → scanning → children → done) with
immediate subfolder/file counts; the row is greyed (with a faint pulse) until
its whole subtree is scanned, then turns solid — the at-a-glance signal.
- Opening a folder jumps its subtree to the front of the scan (ensureScanned),
so an opened folder always shows complete contents; idempotent vs the
background walk.
- No more auto-expand/auto-select-all (that loaded the entire drive up front);
the root is selected so the grid shows its files immediately.
- ZIPs stay expandable, scanned inline into virtual nodes (already in memory
once read); whole zip subtree marked done at once.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three tools (archive, browse, classifier) independently implemented
an empty-state pattern with three different CSS class naming
conventions and slightly different rules:
archive: .empty-state + .empty-state-content (BEM-less)
browse: .empty-state + .empty-state__inner (BEM)
classifier: .empty-state + .empty-state-content (BEM-less)
Same visual intent ("nothing's loaded yet — here's a welcome card
with instructions"), implemented three times with subtly different
spacing, no shared body styling for h2/p/ul/li, and incompatible
class names that prevented a future tool from copy-pasting the
pattern.
Promote a single consolidated rule set to shared/base.css using
BEM naming throughout:
.empty-state — base (flex centered, padding)
.empty-state--overlay — modifier: position absolute,
top 50px to clear app-header,
z-index 10. Used by archive
and classifier (their empty
states sit OVER the main
layout).
.empty-state__inner — content card (left-aligned,
text-muted, max-width 640)
.empty-state__inner--centered — modifier: tighter max-width
500, centered text, 2rem
padding. Used by tools whose
welcome screen reads as a
centered card.
.empty-state__inner h2/p/ul/ol/li — typography defaults
.empty-state__inner .note — italic small-print
.welcome-list — bullet list with left-aligned
text + auto margins; safe to
nest inside a centered card.
Per-tool changes:
- archive/template.html, archive/js/app.js: rename
.empty-state-content → .empty-state__inner empty-state__inner--centered;
add .empty-state--overlay to the outer .empty-state container.
Also the runtime-injected unsupported-browser markup in
showUnsupportedBrowserMessage() and the showHttpErrorState
selector.
- classifier/template.html: same renames.
- archive/css/layout.css + components.css: delete .empty-state*
and .welcome-list rules (now in shared).
- classifier/css/layout.css: same. Keep .empty-state.drag-over
locally — classifier is the only tool whose empty state acts
as a drop target.
- browse/css/base.css: delete .empty-state* (shared covers it).
browse's template was already using .empty-state__inner so no
template change needed.
LOC: shared/base.css gains ~70 lines; per-tool overrides lose ~85
combined. Net -15. More importantly, future tools can reuse the
pattern by adding two divs and (optionally) the --centered or
--overlay modifiers; no copy-paste required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five tools (browse, transmittal, landing, form, tables) were rendering
their headers as vertically-stacked blocks — the .app-header flex
container correctly laid out its left/right groups, but those groups
themselves had no display:flex rule, so their children (logo, title,
build label, action button) defaulted to block-level stacking.
Three tools (archive, mdedit, classifier) hid the bug because they
each carried their own copy of the .header-left/.header-right flex
rule in tool-local CSS. Same intent, slightly different gap values:
archive: left gap 0.75rem, right gap 0.5rem
mdedit: both 0.75rem
classifier: both 0.5rem
Promote the rule to shared/base.css alongside .app-header (where the
class is used in every template anyway): left 0.75rem, right 0.5rem
(matching archive — the layout the user pointed at as the reference).
Delete the three local duplicates.
Now all eight tools use the same header chrome contract: logo + title
group + primary action laid out horizontally with consistent gaps,
icon buttons grouped tighter on the right.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.
See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.