Replaces the zip option. Copy now offers two real destinations, both filing
under <party>/<received|issued>/<transmittal>/<name>:
- Server archive — PUTs each file into a zddc-server over HTTP via the
zddc-source HttpDirectoryHandle (mkdir as needed). Offered only over http(s);
prompts for the archive URL (guessed from the current path's archive/ segment).
- Local folder — the File System Access picker (choose archive/ to file directly).
Both reuse the one copy engine and are efficiently resumable: copyOne probes the
target with a cheap stat/HEAD and SKIPS anything already present — no source
read, no hashing — so a re-run after an interruption only does what's left.
Canonical ZDDC names + the WORM archive mean an existing name is the same
document, so we never overwrite (the old content-diff path is dropped).
Tests: re-run skips an existing local target; PUT into a server-style handle then
resume-skips it (52 green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy now opens a small dialog: copy the canonical files into a folder (pick your
archive/ to file them straight into <party>/<received|issued>/<transmittal>/),
or download a single .zip with the same layout (unzips into place). The zip path
reuses readSource + the derived outRel, so the bytes and folder structure match
the directory copy exactly; conflicts are still skipped beforehand.
Test: the zip is built with one entry at ClientCorp/received/<transmittal>/
ACME-MECH-0001_A (IFR) - foundation.pdf (52 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>
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 Copy button (enabled once >=1 file is fully classified) copies the mapped
files into a user-chosen output directory under their canonical names/layout
<party>/{received,issued}/<transmittal>/<filename> — reading the source, never
writing it.
- copy.js: plan() (complete, non-excluded files) → conflict scan (two sources
→ same output path are reported + skipped) → copyTo() engine on the generic
FS-Access shape (ensureDir + getFileHandle + createWritable). Per-file dedup:
identical target (sha256) is skipped; existing-but-different is left
untouched and reported; live footer progress; completion toast.
- app.js: restores the saved map on launch (keyed by source-relative path, so
it re-attaches when the same directory is re-opened) and persists the source
handle on open; Copy button wired.
- target-tree.js: enables/labels the Copy button from the done count.
- 2 copy-engine tests with mock FS handles (copy/skip/differ + conflict);
24 classify+classifier tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>