The top-level toggle is a tool choice, not the two classification axes (those
are the By-tracking / By-transmittal tabs inside Classify & Copy). Default to
the Classify & copy workflow and relabel the toggle 'Classify & copy' /
'Rename in place' so its purpose is clear; the in-place spreadsheet stays one
click away. 'Use Local Directory' now opens in Classify mode too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A folder with files but no subfolders got no expand toggle, so in Classify &
Copy mode its files (the drag source) could never be revealed — and leaf
folders full of files are exactly where the work is. Make a folder expandable
when it has files in classify mode; expanding lists the draggable file rows.
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>
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>
- 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>
Foundation for the non-destructive map+copy workflow: source stays read-only,
files are mapped onto two orthogonal target trees, a later step copies renamed
copies to a separate output dir.
- classify.js: the single source of truth. assignments map keyed by
source-relative path (survives re-pick); tracking tree (positional: ancestors
joined '-' = tracking number, immediate parent 'REV (STATUS)' leaf = rev+status,
title from original name) and transmittal tree (<party>/{received,issued}/<bin>).
deriveTarget() computes filename + output path + validation purely; pub/sub +
debounced autosave; node CRUD with dangling-placement cleanup.
- persist.js: IndexedDB store of the serialized map + the source
FileSystemDirectoryHandle, with queryPermission/requestPermission re-grant on
reload and a re-pick fallback.
- tests/classify.spec.js: 9 in-page unit tests for the derive/assignment logic
(no FS Access needed) — tracking join, leaf REV (STATUS) parse incl. invalid
status, title derivation/override, transmittal path composition, exclude,
cascade delete.
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>
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>
Toasts now collect in a bottom-right stack. Error/warning toasts are STICKY —
they stay until dismissed so the user can read, select, and copy them while
troubleshooting (e.g. classifier scan errors); info/success still auto-dismiss
(opts.durationMs:0 forces sticky for any level). Each toast has a × to dismiss
it, a "Clear all" appears when 2+ are stacked, and the message text is
selectable/copyable. alert() maps to a sticky error toast.
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>
A no-auth virtual folder so anyone can grab a tool and run it against their own
local filesystem: GET /_apps/ is an index (Download / Open links); GET
/_apps/<tool>.html serves that tool's HTML (?download forces a save). Prefers
the site .zddc.zip bundle member (freshest), falls back to the binary's
embedded copy; tables/form come from the embedded tables bundle. Carries no
data, so it's served before the ACL/cascade and the reserved-prefix guard;
`_`-prefixed + virtual means no collision with content.
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>
Regression guard: mkdir and PUT under working/<party>/ keep the requested
basename case verbatim (MixedCaseDir, UPPER-Name.MD), confirming the server
does not normalize filename case — tracking numbers and the like must stay as
typed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a new record's identity is composed from required fields that aren't
table columns (e.g. the risk register's project/discipline/sequence
tracking-number components), those can't be supplied by typing into the grid.
"+ Add row" now navigates to the compose form (<dir>/form.html) for such
tables — the form composes the tracking number and the server creates the
record. Simple tables (all required fields are columns) keep the inline
draft-row flow. Server mode only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client-side required check was enforcing every field in the row schema's
`required` list — including ones with no table column (e.g. the risk register's
project/discipline/sequence tracking-number components, which are composed
server-side / set via the add-row form, not inline-editable). That blocked
saves naming fields the user can't fill in the grid ("Can't save — required:
party, project, discipline, sequence").
Now requiredFields() intersects `required` with the visible columns, so the
client only blocks on required fields the table can actually fill; non-column
required fields are left to the server (it composes them or returns a 422).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A table column can declare `options_source: <peer>` and the server fills its
`enum` from the live entries under <project>/<peer>/ — so the row editor renders
a dropdown of the current registry instead of free text. Generic + configurable
in the spec; no hardcoding.
- Server (tablehandler.go): resolveDynamicEnums + registryEntries resolve the
peer directory (its *.yaml basenames + subfolders, sorted, dot/spec entries
skipped) into the column enum at ServeTable time, before the context inject.
- Default risk register: add a `package` column with `options_source: ssr`
(dropdown of the project's SSR packages) + the matching form property. The
spec comment documents the key so operators can source other registries.
- Test covering the resolver (entries, skips, untouched columns).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Header: required columns (from the row schema's `required` list) get a red
`*` marker so mandatory fields are obvious.
- Save: before the PUT/POST, a client-side required-field check marks the empty
cells and blocks the save with a clear reason; the server's 422 remains the
authority and its messages surface the same way.
- Inline error row: when a row can't be saved (required-missing, 422 validation,
409 duplicate, 403 permission, network, HTTP error), the reason now shows in a
full-width message row directly beneath the offending row — not just a hover
tooltip or the far-off status bar. Cleared on a successful save / reload.
- Editor row-indexing counts data rows only (tr[data-row-id]) so the inserted
error rows never offset cell navigation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These three are virtual party-aggregator folders — the trailing slash serves
their dir_tool (the browse folder-nav listing of parties INSIDE the folder),
while the no-slash form served the browse tool scoped at the project level.
Land the user inside the folder. archive/ keeps the no-slash form (its
default_tool is the archive tool, which is the intended landing there).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Overflow: the preview pane's child (the markdown shell) was a flex item with
the default min-width:auto, so the editor's wide internal min-content pushed
the whole pane past the viewport's right edge. Add min-width:0 on
.preview-pane__body and its children so the editor shrinks (and its own +
the grid's minmax(0) scrolling takes over) — the pane never overhangs.
- Pop out: editor-type files (markdown, yaml/.zddc, code text) were popped into
the lightweight preview window, which can't host the bundled editor — so
markdown showed as raw <pre>. Now they open the FULL browse app deep-linked
to the file (<dir>?file=<name>) in a new window, loading the real editor.
HTML keeps its rendered popup; images/pdf/office unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renderable types (.html/.htm) now show RENDERED by default again — but in a
sandboxed iframe (no scripts, no same-origin) so arbitrary HTML is safe — and a
header toggle flips to the CodeMirror source view (and back). Non-renderable
text files open straight in CodeMirror as before; PDF stays an iframe; markdown
keeps its own rendered/source toggle.
- RENDERABLE map + per-node view mode (rendered default; remembers only the
last-toggled node, so switching files resets). Toggle button sits by Pop out.
- The same-node render guard now allows a deliberate toggle through (prompting
to discard if the source editor is dirty); clearPreview hides the toggle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Editor routing: the CodeMirror editor is now the general editor for editable
text files that aren't markdown — yaml/.zddc keep schema lint + completion +
hover; txt/csv/tsv/json/xml/html/css/js/log/ini/conf/… open as a plaintext
code editor (line numbers, find, save) instead of a read-only <pre>. HTML now
edits its source (was an iframe render); PDF stays an iframe. Mode is yaml-only
(the vendored CM mode); others plaintext, lint gutter suppressed.
- Abandon the .zddc schema FORM (preview-zddc-form.js deleted): .zddc opens in
CodeMirror. Guided dialogs (Manage access, …) are the front door for common
tasks; CodeMirror is the full/raw surface. One fewer half-baked middle layer.
- Manage Access dialog laid out cleanly: grid rows (who fills + shrinks, level
sizes to content) so long emails/levels never overflow; short level labels
with tooltips + a one-line legend; box-sizing fixed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First of the intent-driven actions that replace raw-YAML editing as the front
door. Right-click a folder (or the pane) → "Manage access…" → a dialog of
people + friendly levels; the raw .zddc editor is demoted to "Edit raw policy
(.zddc)…" as the advanced escape. Both gated by the same admin authority.
- browse/js/manage-access.js: reads the folder's on-disk .zddc, presents
principals as View / Contribute / Edit / Manage (admins: membership), plus an
"inherit from parent" toggle (uncheck = make private). Save maps levels back
to verbs (r / rc / rwc / admins:), merges ONLY the access bits into the doc
(every other key preserved), and PUTs. Unrecognised verb strings show as
"Custom" and are preserved untouched.
- Menu: "Manage access…" (guided) is now primary; "Edit raw policy (.zddc)…"
is the escape hatch.
- Self-contained modal + CSS; refreshes the listing on save.
Sets the pattern for the remaining operator tasks (role members, display label,
project convert vars, register a party).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The .zddc lint used a hand-kept TOP_KEYS/ROLE_KEYS/ACL_KEYS/CONVERT_KEYS list
with bespoke type tags — a parallel grammar that drifted from the Go structs
(we'd already had to patch in party_source/history/etc.). Replace it with one
schema-driven validator over the same JSON Schema that now drives completion +
hover, so lint/completion/hover/form all share ONE grammar.
- Bake zddc.schema.json into the bundle at build time (window.__ZDDC_SCHEMA__),
the exact file the server serves at /.api/zddc-schema. Synchronous + works
offline (file://), where that endpoint is unreachable — drops the runtime
fetch entirely.
- validateZddc() now walks the parsed doc against the schema: type, enum,
pattern, properties, additionalProperties (false|schema), patternProperties,
items, and the recursive $ref:"#" (paths:). Same {keyPath,severity,message}
shape, so findLine + the CM lint helper are unchanged.
- Delete TOP_KEYS/ALLOWED_TOOLS/ROLE_KEYS/ACL_KEYS/CONVERT_KEYS + walkObject/
checkValue/addTypeErr. preview-yaml completion now reads getZddcSchema too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalise the front-matter completion into a reusable, provider-based helper
(browse/js/yaml-complete.js) and wire BOTH YAML editors through it. Still fully
deterministic — every candidate and doc string comes from a schema, no AI.
- yaml-complete.js: shared CodeMirror plumbing (indent→key-path, sibling scan,
show-hint, debounced hover tooltip) + two providers:
· flatProvider — a fixed field list (front matter), with an exclude set.
· schemaProvider — a JSON Schema walker that resolves nested key-paths
through properties / additionalProperties / patternProperties and the
recursive $ref:"#" .zddc uses for paths:; keys from object properties,
values from enum / boolean, hover docs from `description`.
- .zddc editor (preview-yaml.js): fetch /.api/zddc-schema once and attach the
schemaProvider on .zddc files — nested-key completion at every level, enum
values (default_tool, dir_tool, views.*.tool), booleans, and hover docs.
Plain .yaml stays lint+highlight only.
- Front-matter editor (preview-markdown.js): refactored to delegate to the
shared helper via flatProvider (excluding the filename-driven identity keys);
the bespoke frontMatterHints is gone — one implementation now.
- Hover-doc tooltip styling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Vendor CodeMirror's show-hint add-on and wire deterministic, schema-driven
completion into the markdown front-matter pane — NO heuristics, no AI; every
candidate comes from the converter's own field list.
- Vendor codemirror-show-hint.min.{js,css} (CM 5.65.x add-on); concat in
browse/build.sh after the core CM bundle so it extends window.CodeMirror.
- Server: add a structured `values` enum to convert.FrontMatterField (doctype →
report/letter/specification, numbering → true/false), exposed via the existing
/.api/frontmatter JSON. Tracks the template set; keeps the server as the
single source of truth instead of parsing the hint prose.
- Client: frontMatterHints() completes recognised KEYS at line start (excluding
the filename-driven identity keys and keys already present) and enum VALUES
after `key:`. Picking an enum key auto-opens its value list. Triggered on
Ctrl-Space and automatically as you type (completeSingle:false — always a
menu, never an auto-guess). Themed dropdown for dark mode.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes to the markdown editor's identity handling:
- Sync-on-open now only RECONCILES front-matter identity keys the author
already wrote (correcting a stale title/revision/…); it never ADDS them. A
blank or new file opens blank instead of getting an auto-generated title at
the top. The converter derives identity from the filename regardless, so an
empty front matter needs nothing baked in. (Cancel/revert likewise only
touches existing keys.)
- The "Rename file & reopen" flow force-writes the buffer (no If-Match) instead
of routing through save(), which raised the conflict-resolution modal on a
(spurious) 412. The rename is a deliberate user action and the identity edit
that triggered it is consumed by the new filename, so we commit the buffer
rather than interrupting with a merge dialog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the raw <textarea> front-matter editor with a CodeMirror 5 instance —
the same editor family the .zddc previewer already uses (and already bundled in
browse, so no added weight). Gains: YAML syntax highlighting, line numbers, and
the shared js-yaml lint gutter; one consistent editing feel across the two YAML
surfaces.
- Mount fmCM (mode:yaml, lineNumbers, lint gutter, readOnly when not writable)
into a host div; refresh on the next frame so it measures correctly.
- Route every front-matter read/write through fmCM.getValue()/setValue() and
the change event (sync-on-open, dirty tracking, the rename cue, Cancel, save).
- The old textarea placeholder (recognised front-matter keys) becomes a greyed
caption under the header whose tooltip carries the full key list — CodeMirror
5 has no built-in placeholder. Arbitrary keys remain free either way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a Cancel button alongside "Rename file & reopen" that discards the manual
identity-field edits and restores the filename-derived values (leaving the rest
of the front matter + body untouched), then recomputes dirty + the cue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rename-cue div used an inline display:flex, which outranks the `hidden`
attribute's [hidden]{display:none} — so fmWarn.hidden=true never hid it and an
empty yellow box showed whenever the cue had nothing to say. Control visibility
via style.display ('none'/'flex') instead of the hidden attribute.
Also surface a status line when sync-on-open rewrites the front matter to match
the filename, so the change isn't silent ("Front matter synced to filename —
review and save").
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The four identity fields (tracking_number/title/revision/status) come from the
filename — the single source of truth that the register/WORM/ACL key off, never
the front matter. But they must stay in the front matter for the converter's
title block. Resolve the long-standing "front matter disagrees with filename"
nag without coupling the system to ZDDC naming:
- Sync-on-open: when the filename is ZDDC-parseable, mirror its identity into
the front matter on open; if that corrects anything the buffer opens dirty so
a save bakes it in. No-op for non-ZDDC names — the editor stays fully usable
on arbitrary directories, where the front matter is the sole source.
- A manual edit to an identity field is treated as a cue to RENAME the file
(the filename owns identity), not a value to keep: the old "filename wins,
ignored" warning is replaced by an explicit "Rename file & reopen" button
that saves, renames to the implied ZDDC name, and reopens it (server mode via
the ?file deep-link; FS-Access via the moved handle).
- Reword the RecognizedFrontMatter hints from "the filename wins on mismatch"
to "mirrors the filename — rename the file to change it".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
partySourceGate ran on every PUT/move at party-depth-or-below and rejected
with 409 whenever the party lacked a registry row — including edits of files
already filed under working/<party>/…. The gate is an ONBOARDING guard (don't
let a typo'd/unregistered party folder be introduced), not a write gate: once
the party directory exists on disk the party is established, so editing within
it must succeed. Allow when <project>/<peer>/<party>/ already exists; keep the
409 only for introducing a brand-new unregistered party.
This was surfaced by the browse markdown editor 409ing on save for an existing
file under a party folder whose ssr/ row was missing or differently-cased.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three .zddc previewer fixes reported against the browse YAML editor:
- Lint no longer flags valid keys. browse/js/preview-yaml.js TOP_KEYS had
drifted from the Go decoder (zddc/internal/zddc/file.go): party_source,
history, history_globs, records, auto_own_roles, received_path,
planned_response_date, planned_review_date, field_codes were all reported
as "unknown key". Add them with appropriate type tags plus an 'object'
case in checkValue for the free-form maps (records, field_codes).
- The ".zddc schema" pill is now clickable (↗) — opens the canonical JSON
Schema the lint mirrors at /.api/zddc-schema (no-auth, read-only).
- The synthetic virtual-.zddc header comment named an internal source path
(internal/zddc/defaults/'s paths: tree) that an operator can't act on. It
now names the operator-facing artifact: the built-in defaults bundle,
exportable/overridable as a root .zddc.zip via `zddc-server show-defaults`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The intro <p class="help"> was a 2–3 sentence paragraph; in a narrower
preview pane it wrapped to ~9 lines (~170px tall), pushing the Title field
far down. It was also redundant with the read-only "Structure & advanced"
section + the "Edit raw YAML" button. Tighten it to one concise line
("Project options. Structural keys are read-only — use Edit raw YAML."):
now 20px wide / 41px on a narrow pane (was up to ~170px).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tables tool referenced --spacing-sm/md/lg (14×) but they were never
defined and used no fallbacks, so every padding/margin/gap collapsed to 0 —
table cells had no vertical padding and the table sat flush to the viewport
edges. Define --spacing-sm/md/lg (+ alias the --color-*/--radius-sm names the
tool uses) in shared/base.css, and give .table-main a clear left/right gutter
(padding: md lg). Fixes every tables view (profile/tokens/diagnostics/mdl).
Profile: clicking a project (or admin-subtree) row now opens that scope's
.zddc INFO FORM in the browse editor (via the ?file=.zddc deep link →
selects + previews the .zddc → schema-driven Title/Roles/Admins form),
instead of navigating into the project's files. Diagnostic rows still link
to their endpoints.
Validated in a containerized browser: 24px side gutters + padded rows;
clicking Proj → /Proj/?file=.zddc → the .zddc form editor. Full suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
grid.js was classifier-only; make it embed ANY embeddable full-page tool the
cascade resolves as default_tool — classifier (incoming/), transmittal
(staging/), archive (the index) — as an iframe scoped to the current dir
(<dir>/<tool>.html). This is the browse-as-shell bridge from the ADR: browse
stays the top-level app and the heavy tools open in-pane (the gridView), so
navigating to staging/ or archive/ inside browse shows transmittal/archive
without leaving the shell, with ?view=browse falling back to the folder
listing (and the standalone tools still served directly at the no-slash URL).
EMBEDDABLE = {classifier, transmittal, archive}; tables/forms embed in the
preview pane instead, landing/browse don't self-embed. resolveViewMode keys
off grid.availableHere() (now generic). Validated in a containerized browser:
each dir embeds its tool, ?view=browse overrides to the listing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The profile page links to /.profile/{config,logs,whoami}, which returned raw
JSON — so a browser click landed on raw JSON. Render them through the tables
engine instead (header chrome + sortable/filterable columns), content-
negotiated: browsers (Accept: text/html) get the table; scripts (Accept:
application/json) still get the unchanged JSON. New serveDiagTable helper +
kvRow/kvColumns: logs → time/level/message/detail rows (newest first);
config + whoami → Field/Value rows. Dropped the deep effective-policy row
from the profile table (kept JSON-only, not linked).
Extends api-actions.js with a `readOnly` context flag so a server-injected
read-only table (no apiActions) still hides the file-model toolbar buttons
(+ Add row / Save). Export CSV stays.
Completes the bespoke-server-page → tables-engine consolidation: tokens,
profile, and the three admin diagnostics now all render declaratively with
shared chrome; per-role gating stays server-side (diagnostics are elevated-
super-admin only). Full Go suite green; verified in a containerized browser.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Capture the mechanism the tokens + profile consolidation now rests on:
AGENTS.md gains a "Server-injected collections (apiActions)" section under
the Tables system (pre-assembled #table-context + the create/deleteRow/
rowNav layer, with server-side per-role gating), and the ARCHITECTURE ADR
marks step 2 done (/.tokens + /.profile render via the engine) and flags
that the remaining folds (archive/landing/transmittal) are feature-rich
PLUGIN migrations — not quick tables-fications.
Adds TestBuildTokensTableContext locking the contract: only the caller's
own tokens become rows, each row carries its id for the delete action, and
apiActions wires create (one-time secret) + per-row delete to /.api/tokens.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retire the bespoke profile page. /.profile now renders through the shared
tables engine (header chrome incl. the profile menu) from a server-injected
context: the caller's "Effective access" — projects + admin subtrees — as
clickable rows (rowNav opens each), identity in the description, and an
apiActions "+ New project" (name → POST /.profile/projects, gated on
can_create_project; roles are added afterward by editing the project's
.zddc, which is now standing-editable). Super-admin diagnostics
(config/logs/whoami/effective-policy) stay discoverable as rows linking to
their unchanged endpoints — gated on IsSuperAdmin so a non-admin's context
never even names them.
Dropped as redundant/niche: the in-page theme picker (the header has the
theme button), the localStorage inspector, and the "editable .zddc" links
(those files are now standing-editable in browse).
Extends the generic apiActions layer (tables/js/api-actions.js) with
`fixed` constant fields (e.g. parent="/"), `required` field validation, and
`rowNav` clickable rows (capture-phase, so it beats the editor's per-cell
handlers). Rewrote TestServeProfileHTMLLayered to the new model (per-role
context correctness: no admin leak; super-admin diagnostics present) and
dropped the now-dead stripTemplates helper. Validated in a containerized
browser; full Go suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Retire the bespoke, chrome-less /.tokens page. It now renders through the
shared tables engine — getting the standard header (logo, theme, profile
menu) + declarative columns/filters for free — from a server-injected,
pre-assembled #table-context built from the user's tokens (Store.List).
New, reusable "tables over an API collection" primitive (tables/js/
api-actions.js): when the injected context carries an `apiActions` block,
it drives create (a modal form → POST, surfacing the one-time secret) and
per-row delete (→ DELETE) against a REST endpoint, and hides the file-model
toolbar affordances (+ Add row / Save). It deliberately does NOT touch the
file-save/row-ops machinery (ETag/conflict/row-file writes), so the secrets
surface stays on the existing tested /.api/tokens endpoints.
Server: handler.injectTableContextObj injects an arbitrary pre-assembled
context; EmbeddedTablesHTML() exposes the renderer to sibling handlers;
ServeTokensPage builds the token context (+ apiActions for /.api/tokens)
and serves the tables HTML, falling back to the legacy skeleton only when
the store or the tables renderer is unavailable.
This is the first dynamic/virtual-record collection rendered by the same
declarative engine + chrome as on-disk tables — no bespoke page. Validated
end-to-end in a containerized browser (list + create→secret + revoke);
tests/tokens.spec.js updated to the new UI; full Go suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the agreed direction: browse becomes the single shell (header +
tree + preview pane), content tools become preview-pane plugins, and
server features (account menu, permissions) are progressive enhancement —
not a server-rendered header wrapping an iframed browse. Sketches the
plugin contract (handles/render/dispose + the ctx capability object that
abstracts server-vs-local read/write/verbs) and the incremental migration
path. Captures the model settled on with the user.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>