Previously every .zip was auto-expanded into a folder of members (and was even
double-represented as both a file and a folder node). Now a .zip is one
classifiable file by default; right-click it → "Expand as folder" to pull its
members into the fileset, and right-click an expanded archive (or a member) →
"Collapse to single file" to go back. The toggle sits with Exclude in the
context menu.
- scanner: stop creating zip-root nodes during the scan; expandZipAsFolder /
collapseZipToFile mutate the tree in place (re-reading members from the live
handle or, for a restored workspace, lazily from the root) and recompute
subtree totals. Mode is encoded by the tree shape, so it persists in the
snapshot as-is.
- classify.dropAssignments clears the assignments that cease to exist when a zip
flips mode (the single-file key on expand; the member keys on collapse).
- copy already handles both: a zip-as-file copies whole; members extract from
the archive.
Also: a folder whose entire subtree is excluded now renders its name struck
through, mirroring the excluded-file style.
Tests: collapse restores the single .zip + drops member assignments; a
fully-excluded folder gets the struck-through class (48 green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zip members were live-only: expandable while the source was connected, but the
workspace snapshot dropped the archive (.zip became a plain file), so a
classification made inside one vanished on reopen — and copy couldn't extract it
anyway (it tried to walk the archive path as a real directory).
Now zips are first-class:
- snapshotTree/loadSnapshot persist the scanned archive subtree — zip-root +
virtual folders + members carry isVirtual/zipPath/zipEntryPath, so the tree
rebuilds on reopen and assignments inside an archive survive. An archive that
was never opened persists as a lazy 'zip' node that reopens on demand.
- scanner.ensureZipLoaded(rootHandle, zipPath) reloads an archive from the
workspace root when the in-memory cache is cold (post-restore); scanZipNode
falls back to it when a restored zip node has no live file object.
- copy.js reads a member via scanner.extractZipMember (Blob from the archive)
instead of a non-existent file handle; preview.js reloads the archive for a
restored member before opening it.
This also reconciles export/import with the snapshot: both now keep zip members,
so a round-trip no longer leaves dangling in-archive assignments.
Tests: zip subtree snapshot round-trip; copy extracts a member to the output (45).
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>
The scan is I/O-bound on cloud-sync / network mounts (OneDrive, Samba) where
each directory read is a high-latency round-trip. More in-flight reads hide
that latency on the many-folders case. (A single large folder is still
enumerated one entry at a time by the File System Access API and can't be
parallelized.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scan concurrency: the scan is I/O-bound — each directory read is a network
round-trip to the share, so the lever is parallel in-flight reads, not CPU
threads. Replace the per-level BFS barrier (which idled workers waiting on
the slowest dir in each level) with a continuous shared-queue pool that
keeps up to SCAN_CONCURRENCY (16, up from 6) reads in flight at once,
pulling newly discovered child dirs as they land. Still roughly
breadth-first (FIFO), so top levels surface first. ensureScanned reuses it.
Error messages: translate File System Access DOMExceptions into accurate,
actionable text keyed on err.name (not the cryptic raw message, which reads
like a permission problem when it isn't). e.g. InvalidStateError now reads
'the folder changed on disk since it was first read … rescan' instead of
'an operation that depends on state cached in an interface object …'. The
raw name+message is appended in parens for copy-paste troubleshooting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The scan was slow because it OPENED every file (getFile() for size/lastModified
— which the grid doesn't even display) and read every ZIP inline. On a network
share that's a round-trip per file. Now:
- createFileObject builds rows from the directory entry name alone, no
getFile(); size/lastModified load on demand (preview/SHA/rename already call
getFile() themselves). The scan is now a pure directory listing.
- ZIPs are lazy: a .zip is an expandable node read only when opened
(scanZipNode), not during the walk.
- Footer shows live elapsed time (ticks every second), and a success toast
fires at completion with totals: "Scan complete — N folders, M files in Ts."
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>
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.