Markdown preview pane now surfaces YAML front-matter above the TOC as a
key/value list (definition list), so engineering documents with header
metadata (title, revision, status, etc.) show their identity at a glance
without opening the file in mdedit. Front-matter parsing handles both
scalar and array values; arrays render as comma-joined.
TOC pane is now resizable (4px col-resize handle on its left edge);
preserves the user's chosen width across re-renders inside a single
session.
mdedit welcome banner moved inside #welcome-screen so the "browse opens
md in this same editor" callout only shows when no file is open — it
was previously visible in every state which was noisy.
archive.spec.js: wait for #filePreviewToggle to be attached before
clicking, fixing a Playwright flake where the preview button hadn't
mounted yet.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape browse from "tree-as-table with popup preview" into a unified
file-experience tool with three layered behaviors:
Phase A — Two-pane shell
Phase B — Markdown plugin (Toast UI inline)
Phase C — Grid mode (classifier workflow)
Phase D — Deprecation banners on standalone classifier + mdedit
= Phase A: two-pane shell + lightweight preview plugins =
Browse's table view becomes a tree-pane on the left + preview-pane on
the right with a draggable resizer. Click a folder → expand inline.
Click a file → render in the right pane. The previous popup window
becomes an explicit "⤴ Pop out" button in the right-pane header for
users with a second monitor.
Preview rendering reuses shared/preview-lib.js (PDF iframe, image
<img>, TIFF, ZIP listing, text <pre>). Unknown types show a download
link. browse/js/preview.js refactored into renderInline (default) +
renderInPopup (Pop out button); both share the same plugin
dispatch logic.
Filter rows were already removed earlier this session. Sort columns
likewise — the tree is alphabetical by default; the underlying
setSort API still exists for future re-introduction.
= Phase B: markdown plugin =
New browse/js/preview-markdown.js: when a .md or .markdown file is
clicked, the right pane mounts a Toast UI editor (initial-value =
file contents) with a small toolbar containing Save + dirty indicator
+ status text. Save sends PUT through the file API for server-mode
files; non-server sources are read-only for now (deferred to a
follow-up that wires zddc-source.js writes too). Ctrl+S / Cmd+S
inside the editor saves.
Toast UI Editor (~700 KB JS + ~160 KB CSS) was previously bundled
only in mdedit/vendor/. Moved to shared/vendor/ so browse and mdedit
both pull from one location.
= Phase C: grid mode =
View-mode toggle [Browse | Grid] in the toolbar. Grid mode loads the
classifier tool as an iframe scoped to the current directory (server
mode at working/staging/incoming locations) — classifier's full
bulk-rename workflow without leaving browse. v1 implementation; a
future iteration could bundle classifier's modules directly into
browse for tighter integration. Hostile cases (file:// origin, paths
outside working/staging/incoming) show a friendly explanation
instead of a blank iframe.
new browse/js/grid.js handles the activation logic.
= Phase D: deprecation banners =
mdedit and classifier standalones gain a "this tool is being absorbed
into Browse" advisory banner. Both standalones remain fully
functional and continue to ship — they're useful for offline single-
file editing and air-gapped environments. The banner just points
users toward the unified browse experience.
= Files =
+ browse/js/preview-markdown.js (markdown plugin)
+ browse/js/grid.js (grid-mode plugin)
M browse/template.html (two-pane layout, view toggle, banners)
M browse/css/tree.css (two-pane CSS, replaces table styles)
M browse/js/init.js (state additions: selectedId, viewMode)
M browse/js/tree.js (rowHtml: <tr>+<td> → <div>)
M browse/js/preview.js (renderInline / renderInPopup split)
M browse/js/events.js (toggle wiring, resizer, click handlers
adapted from <table> to <div>)
M browse/build.sh (Toast UI vendor + new modules)
R mdedit/vendor/toastui-* → shared/vendor/ (one bundle, two tools)
M mdedit/build.sh (paths)
M mdedit/template.html (deprecation banner)
M classifier/template.html (deprecation banner)
M tests/browse.spec.js (selectors updated for new layout +
new "click file → preview" test)
Bundle sizes after this commit:
browse: ~1020 KB (was ~290 KB; added Toast UI ~700 KB)
classifier: ~1470 KB (unchanged from prior baseline)
mdedit: ~2140 KB (unchanged; vendor location moved but not added)
What's deferred:
- TOC + front-matter pane in browse's markdown plugin (mdedit has
these; browse v1 uses just the editor).
- FS-API writes from browse's markdown plugin (server PUT works).
- Classifier modules bundled directly into browse (v1 uses iframe).
- Sort UI in the new tree (model still supports it; no widget yet).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes every runtime CDN load. The "ship the record player with the
record" philosophy: a downloaded .html file works offline against any
file the user can open, with no network dependency at runtime.
Newly vendored under shared/vendor/:
- xlsx.full.min.js (SheetJS, 928 KB) — XLSX/XLS preview
- utif.min.js (UTIF, 57 KB) — TIFF preview
Already there but now used by mdedit too:
- jszip.min.js, docx-preview.min.js
Call sites updated to drop the `await loadLibrary(URL)` pattern —
since the vendor JS is concatenated into the inline <script> at build
time, window.XLSX / window.JSZip / window.UTIF / window.docx are
available synchronously from page load.
Per-tool changes:
- archive/build.sh: +xlsx, +utif
- classifier/build.sh: +xlsx, +utif
- transmittal/build.sh: +xlsx, +utif
- mdedit/build.sh: +jszip, +docx-preview, +xlsx, +utif
(mdedit was the only tool not yet
bundling any of the preview deps)
- browse/build.sh: +utif
- archive/js/table.js, classifier/js/preview.js,
transmittal/js/files-preview.js, mdedit/js/file-tree.js (×2):
drop the `await loadLibrary('…cdn…')` lines.
- shared/preview-lib.js:
drop the loadLibrary(UTIF) / loadLibrary(JSZip) wrappers; assume
window.UTIF and window.JSZip are present.
Net bundle-size delta after baking:
archive: +990 KB → ~1.47 MB
browse: +57 KB → ~292 KB
classifier: +990 KB → ~1.43 MB
mdedit: +1100 KB → ~2.09 MB
transmittal: +990 KB → ~1.63 MB
Docs (AGENTS.md, ARCHITECTURE.md) updated: removed the "runtime CDN
loading exception" paragraph and the table row that flagged xlsx as
CDN-loaded.
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>
The .app-header__logo SVG was decorative on every tool. Web's
strongest convention is "click logo → go home" — so users tapping
it expecting that fallback got nothing. Now the logo is wrapped in
an anchor whose href reflects the URL the page was loaded from:
file:// → no wrap (no server home to point at)
/ → wrap, href=/ (deployment root)
/index.html / /<tool>.html → wrap, href=/ (root, no project)
/<project>/... → wrap, href=/<project> (project landing)
The wrap happens client-side at DOMContentLoaded via shared/logo.js,
loaded by every tool's build.sh after toast/nav. Idempotent — a
template-supplied anchor or a second mount call is a no-op.
The companion shared/logo.css adds a subtle hover/focus affordance
(opacity 0.82, focus ring) so the logo reads as clickable without
otherwise altering its visual weight. Tools opt out by setting
window.zddc.logo.disabled = true before DOMContentLoaded (e.g. for
deployments that pin the logo to an external destination).
Five Playwright tests (tests/logo.spec.js) lock the contract:
no-wrap on file://, href=/ at root, href=/<project> in project
subtree, aria-label matches target, idempotent re-mount.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mdedit's loadServerDirectory derived its file-tree root by stripping
back to the last "/" in window.location.href. For a no-trailing-slash
canonical URL (<project>/working) that gave the wrong root: the parent
project directory rather than working/ itself. Visible symptom on
zddc-server's auto-served URLs after the recent dispatcher changes —
mdedit at /Project/working would show the project's archive, working,
staging, etc. all together instead of being sandboxed to working/.
Detect the directory-shaped URL by checking whether the last path
segment contains "." (file marker). No dot = directory; treat the
whole path as the root and append the trailing slash internally.
/Project/working → root /Project/working/ (was /Project/)
/Project/working/x/y/ → root /Project/working/x/y/ (unchanged)
/Project/x/mdedit.html → root /Project/x/ (file URL; unchanged)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a thin nav strip directly under the app-header showing the four
canonical lifecycle stages from the transmittal-workflow spec:
archive · working · staging · reviewing. Each is a link to that
stage's directory under the current project. Current stage is
highlighted (bold + primary color, aria-current="page"). Strip
mounts as a sibling of .app-header on DOMContentLoaded — no
template changes needed in any tool.
Render rules (shared/nav.js shouldRender):
- location.protocol must be http: or https: (file:// has no project
structure to navigate within)
- a project segment must be detectable as the first path segment
(when it isn't a tool HTML file like /index.html or
/archive.html?projects=A,B). Multi-project view at the deployment
root therefore shows no strip.
Stage URL targets:
- Archive → <project>/archive.html (project-root archive view)
- Working → <project>/working/ (directory listing — mdedit auto-served)
- Staging → <project>/staging/ (directory listing — transmittal auto-served)
- Reviewing → <project>/reviewing/ (directory listing)
Convention-driven, not probed: if a deployment doesn't have one of
these folders the link returns 404. Operators on non-standard layouts
can opt out by setting window.zddc.nav.disabled = true before
DOMContentLoaded.
This pairs with the previous landing-tool change (single-project
click → <project>/archive.html). Together they give the user
both URL-bar manipulation AND visible navigation across the four
canonical project stages.
Five Playwright tests in tests/nav.spec.js exercise:
- non-render at deployment root
- render + active stage on <project>/archive.html
- render + active stage deep inside <project>/working/foo/mdedit.html
- canonical link targets
- mount position is sibling of .app-header
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Promote classifier's local toast (classifier/css/base.css + showToast
in classifier/js/excel.js) into shared/toast.{js,css}. Every tool's
build.sh now concatenates them, so window.zddc.toast(msg, level, opts)
is callable from any tool.
API:
window.zddc.toast('Saved.', 'success');
window.zddc.toast('Could not load: ' + err.message, 'error');
window.zddc.toast('Note', 'info', { durationMs: 3000 });
Levels: info (default) | success | warning | error. Single-toast
policy — a second call replaces the first. Click anywhere on the
toast to dismiss. ARIA: error → role=alert/aria-live=assertive,
others → role=status/aria-live=polite.
Class prefix is .zddc-toast (BEM-ish) to avoid colliding with any
tool-local .toast rules. Classifier's existing showToast now
delegates to window.zddc.toast — call sites in excel.js +
selection.js are unchanged. Classifier's local .toast CSS block
deleted in favor of the shared one.
This commit only EXPOSES the API. Replacing the ~25 alert() call
sites scattered across archive/transmittal/mdedit/classifier with
toast calls is left as follow-up — each alert needs per-call review
to decide if it's truly non-blocking.
Five Playwright tests in tests/toast.spec.js lock the contract:
API exposure, level mapping, ARIA roles, single-toast replace,
click-to-dismiss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six inline style="" attributes in archive, browse, mdedit templates
moved into stylesheets:
- The font-size:1.1rem override on #refreshHeaderBtn (three tools) is
now a single rule in shared/base.css — the refresh ⟳ glyph genuinely
reads smaller than ◐ / ?, so the rule lives next to the existing
shared icon-button block.
- The flex-start justify-content + select-all margin on archive's
Revisions column header become a .th-content--start variant +
.select-all-checkbox class in archive/css/table.css.
- The 450px initial width + 200px min-width on mdedit's #file-nav move
into mdedit/css/base.css; the runtime resizer continues to override
via inline style.width when the user drags.
No visual change — the output is exactly equivalent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mdedit's toc.js exported updateToc / clearActiveTocItem / setActiveTocItem
as window.* globals, which was the only remaining violation of the
two-globals discipline (only window.app and window.zddc are intended).
Other tools either use IIFE + window.app.modules.X, or — like mdedit —
declare top-level functions that are reachable across concatenated files
without going through window.
Removed the three window.* exports and unqualified the four call sites
in events.js, editor.js (×3), and file-system.js. setActiveTocItem was
already called bare elsewhere; the change is just dropping a
window.foo && check chain that's now unnecessary.
No behavior change — TOC generation and click-to-scroll work as before.
mdedit's three Playwright tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the binary acl.allow/deny model with five permission verbs
(r/w/c/d/a) and first-class roles, and adds an authenticated file API
(PUT/DELETE/POST move/mkdir) so the HTML tools can edit-in-place over
HTTP. Closes the AC-3(7) and AC-6 federal-readiness gaps.
File API (zddc/internal/handler/fileapi.go)
- PUT <new> → action c
- PUT <existing> → action w
- PUT <.zddc> → action a (CanEditZddc strict-ancestor rule)
- DELETE → action d
- POST mkdir → action c (auto-writes creator-owned .zddc when the
parent is Incoming/Working/Staging)
- POST move → action w on src + c on dst, atomic via os.Rename
- Optional If-Match for optimistic concurrency, --max-write-bytes cap,
audit log emits a structured file_write event per operation.
Permission model (zddc/internal/zddc/{acl,file,roles,cascade_mode}.go)
- acl.permissions: { principal → verb-set } map; principals are email
patterns or role names. Empty verb set is an explicit deny.
- roles: { name → members } definitions, available at the level they
declare and all descendants. Closer-to-leaf shadows ancestor.
- Legacy acl.allow/deny still work; they fold into permissions at
parse time (allow → "rwcd", deny → "").
- Cascade walks leaf→root; first level with any matching entry wins;
the union of matching verb sets at that level decides.
- --cascade-mode=strict adds a root→leaf ancestor-deny pre-pass so an
ancestor explicit-deny is absolute (NIST AC-6). Default delegated
preserves the existing commercial behavior.
Special folders (zddc/internal/zddc/special.go)
- Incoming / Working / Staging: mkdir auto-writes a .zddc into the new
subdir granting created_by + that email rwcda directly. Same form
operators write by hand; creator can edit it later to add others.
- Issued / Received: server-enforced WORM split. Cascade grants
inherited from above the WORM folder are masked to r only; grants
placed at-or-below the WORM folder retain r,c. Operators grant
write-once (cr) to the doc controller via an explicit .zddc at the
Issued/Received folder. Admins exempt — only escape hatch.
Browser polyfill (shared/zddc-source.js)
- HttpDirectoryHandle + HttpFileHandle implement the FS Access API
surface (values, getFileHandle, createWritable, removeEntry,
queryPermission/requestPermission) over zddc-server's listing JSON
and file API. Existing tools written against showDirectoryPicker
work unchanged.
- detectServerRoot() returns { handle, status }: tools auto-load on
HTTP, surface a clear "no permission to list" message on 403, and
fall back to the welcome screen on 0.
- classifier renames take the atomic POST move path on HTTP-backed
handles; mdedit and transmittal route reads/writes through the
polyfill so prior FS-API code paths cover both modes.
Tests
- zddc/internal/zddc/{cascade_mode,roles,special,acl}_test.go cover
delegated vs strict, role membership / shadowing / legacy fallback,
WORM split semantics, verb-set parser round-trip.
- zddc/internal/handler/fileapi_test.go now also covers role-based
vendor scenarios, WORM blocking vendor & doc controller writes,
explicit Issued .zddc unlocking the cr drop-box, admin bypass,
auto-ownership on mkdir, and strict-mode lockouts.
Docs
- ARCHITECTURE.md + zddc/README.md document the verb model, role
syntax, special-folder behaviors, cascade-mode flag, and full file
API surface. Federal-readiness gap analysis strikes AC-3(7) and
AC-6.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The button was renamed from 'Select Directory' to 'Add Local Directory'
in 22c142e (chore(headers): standardize across all 7 tools). The
template, in-tool welcome hint, and in-tool help text all updated;
mdedit/README.md was missed.
Investigated the underlying bug report ('Select Directory button isn't
showing'): the button itself is fine — present in template.html with
btn-primary class, never hidden, only visually demoted to btn--subtle
once a directory is loaded. The user's report was new-label confusion
from the rename. Fixing the README to match the rendered label closes
the discrepancy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mdedit was the only tool whose dist/<tool>.html was force-tracked
(via `git add -f` in the build's stable-cut path). Inconsistent with
every other tool in the repo, where dist/ is fully gitignored. The
build regenerates mdedit/dist/mdedit.html the same way it regenerates
the others, so there's no reason to track it.
Drop the `git add -f` line in build:735 and `git rm --cached` the
file. The on-disk artifact stays put for the dev iteration loop;
only the index entry goes away.
Bake into the dev binary:
- ETag + max-age=0 on embedded HTML (304s on repeat loads)
- gzip compression middleware (~75% wire-size reduction)
- vendored jszip + docx-preview in archive/transmittal/classifier
- tee'd file-based access log via --access-log
Bake the standardized headers + archive bugfix + browse refactor
into the dev binary. Triggers notify-chart-dev → bumps tnd-zddc-chart
develop with appVersion=0.0.16-beta-<sha>.
Bring every tool's header in line with archive's pattern:
[logo] [title] [version] [Add Local Directory] [⟳] ............... [◐] [?]
------------- header-left --------------- ----- header-right -
Changes per tool:
* browse: rename "Select Directory" → "Add Local Directory"; add the
red-non-stable wrap to the build label (was missing); add a help
panel + bundle shared/help.js.
* classifier: rename selectDirectoryBtn → addDirectoryBtn,
refreshBtn → refreshHeaderBtn for consistency. Update all JS
callers and welcome-screen copy to the new label.
* mdedit: same id rename. Move the previously-in-pane refresh
button into the header. Stop renaming the dir button to
"Directory: <name>" once a folder is loaded — instead use the
shared btn--subtle variant to de-emphasize while keeping the
standard label.
* transmittal: convert non-standard <div class="app-header"> with
spacer/icons containers to <header class="app-header"> with the
canonical header-left/header-right pair. Move the publish split-
button into header-left (Transmittal-specific primary action).
Remove dead .app-header__spacer/__icons/header-icon-btn CSS now
that nothing references those classes.
* landing, form: add help-btn + help-panel + bundle shared/help.js.
Each panel is tool-specific (project picker docs for landing,
schema-driven form docs for form).
Cross-cutting:
* shared/base.css: promote .btn--subtle from browse/css/tree.css
so any tool with an online mode can de-emphasize Add Local
Directory consistently.
Verified all 7 tools in headless Chromium: header structure correct,
build label red on non-stable cuts, help panel opens + closes via
button + Esc.
Bake the latest dev cut of all six tools into zddc/internal/apps/embedded/
so the dev image (built from main) ships the new browse filter UI +
vendored JSZip. Triggers notify-chart-dev which bumps the chart's
develop branch with appVersion=v0.0.16-beta-<sha>.
The zddc-server-v0.0.9 (and sibling) tags previously pointed at a
commit whose embedded versions.txt + tool HTMLs still carried
alpha-dirty labels — the cut process regenerated these in the
working tree but never folded them into the tagged commit. The
binary built from that tag (used by tnd-zddc-chart's prod
Dockerfile) embedded the alpha labels.
This commit folds the stable-labeled artifacts in. The seven
v0.0.9 tags are force-moved to point here so future binary builds
from `ZDDC_REF=stable` get clean stable bytes baked in. The
old commit (a02a26d) remains in history; just no tag references
it anymore.
Sustainable fix to ./build's release flow (commit before tag,
skip embedded mutation on plain dev/alpha cuts) is a separate
follow-up — this commit only fixes the in-flight state.
Schema-driven form renderer plus zddc-server endpoints that turn any
<name>.form.yaml into a working data-collection form at <path>/<name>.form.html.
Submissions land in <path>/<name>/<YYYY-MM-DD>-<email-sanitized>.yaml,
ACL-gated by the existing .zddc cascade. The form posts back to its own URL;
the server strips ".html" and routes by what's underneath, so create and
update use the same client-side code path.
Form spec dialect: JSON Schema 2020-12 + RJSF-style ui:* hints, written in
YAML. Chosen for LLM authorability — it's the canonical structured-output
target for OpenAI/Anthropic, and the ui:* convention is the most-trained UI
hint vocabulary. Supported subset for v0: type (string/number/integer/boolean/
array/object), enum, min/max, minLength/maxLength, required, additionalProperties:
false, properties, items, format (date, email). Round-trip mode is form-as-truth:
submission YAML is regenerated each save, comments are not preserved (the v1
file-as-truth mode for hand-edited files like .zddc itself is deferred).
New components:
* form/ — sixth single-file HTML tool, vanilla JS renderer (~760 LoC)
* zddc/internal/jsonschema/ — focused JSON Schema validator covering only
the v0 keyword subset. Match-implementation-cost-to-surface-used: a full
library brings 70%+ surface we don't use; revisit when v1 adds $ref +
oneOf + if/then/else.
* zddc/internal/handler/formhandler.go — RecognizeFormRequest / ServeForm,
capability-URL re-edit, atomic submission writes via the new
zddc.WriteAtomic helper extracted from writer.go.
* dispatch() in zddc-server/main.go now intercepts *.form.html and
*.yaml.html before the static-file path; spec existence is the trigger.
Build pipeline: form joins ZDDC_RELEASE_TOOLS in lockstep, gets its own
embedded copy in handler/form.html (separate from the apps cascade —
the form renderer is fixed, not subject to per-folder version overrides).
Tests: 5 new Playwright specs (form-safety) + 14 new Go tests across the
validator and handler. All 172 Playwright tests + 10 Go packages green.
End-to-end manual verification: GET empty → POST 201 + capability URL →
GET re-edit (pre-filled) → POST update → 200, raw YAML browsable, ACL
deny → 403.
Docs: form/ section added to AGENTS.md and ARCHITECTURE.md. AGENTS.md
also documents the implementation-vs-dependency policy. CLAUDE.md repo-shape
list extended.
Deferred (v1+): .zddc editor migration onto this system, file-as-truth
lossless YAML round-trip, ui:show-when conditional visibility, oneOf/anyOf,
apps-cascade preview hook, cascade-fetched form definitions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Migrates from in-repo orphan `website` branch + LFS to a two-repo +
deploy-host model so source editing is fully decoupled from live state.
- Source code stays here (codeberg.org/VARASYS/ZDDC).
- Hand-edited website content moves to a separate Codeberg repo
(codeberg.org/VARASYS/ZDDC-website, cloned at ~/src/zddc-website/).
- Live site is /srv/zddc/ on the deploy host (Caddy bind-mount),
populated by ./deploy from this repo's dist/release-output/ plus
~/src/zddc-website/.
- Releases are no longer in any git history — reproducible from
<tool>-vX.Y.Z tags via `./build release X.Y.Z`. No LFS, no
Codeberg release assets.
Build/deploy split:
- ./build (no arg) is source-only; nothing in dist/release-output/
or /srv/zddc/ is touched.
- ./build alpha|beta|release seeds dist/release-output/ from
/srv/zddc/releases/ (preserving symlinks), then mutates the
channel(s) being cut on top. The bundle is always a complete
intended-live snapshot, so the verifier sees a complete world
and ./deploy --releases (rsync --delete-after) replaces live
state cleanly.
- New ./deploy wraps the rsync flow with --content / --releases
subcommands.
Docs updated to reflect the new model: CLAUDE.md, AGENTS.md,
ARCHITECTURE.md, zddc/README.md, README.md, .gitignore, shared/
build-lib.sh comments, deprecated zddc/release.sh message.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reverts the prior CLI simplification. ./build (no arg) now does source
work only — tool dist/ + cross-compiled zddc-server binaries — and
leaves the website worktree alone. Channel/release cuts are explicit:
./build dev build (source only, no deploy)
./build alpha cut alpha (cascades nothing)
./build beta cut beta (cascades alpha → beta)
./build release [X.Y.Z] cut stable (cascades all)
Rationale: editing source shouldn't have a side-effect on the live
site. The website worktree at ~/src/zddc-website/ is what Caddy serves
in real time, so any write to it is a deploy. Treating dev iteration
as alpha-publish was confusing — the user wanted source builds and
deploys to be distinct verbs.
Mechanically: a `dev` (default) branch is added to the case statement;
the post-build matrix-index regen + channel-link verifier are
conditional on RELEASE_CHANNEL being set; dev builds skip them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames build.sh → build and replaces the --release flag form with
subcommands:
./build cut alpha (default; active dev iteration)
./build beta cut beta (cascades alpha → beta)
./build release cut stable (coordinated next version)
./build release X.Y.Z cut stable at explicit version
./build help
The contract shift: there's no longer a "plain dev build that doesn't
touch channels" at the top level. Every full-stack build is a publish
action — running ./build IS active dev iteration, which is what alpha
already meant. To iterate on one tool without writing to the website
worktree, use the per-tool sh tool/build.sh (unchanged).
Output continues to land in ${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}
and nothing is pushed automatically. Commit + push the website branch
yourself when you want to publish. Stable cuts still tag locally on
main; tags push separately too.
Behind the scenes: the export of ZDDC_DEPLOY_RELEASES_DIR is moved
above the per-tool build.sh invocations so children inherit it. The
prior "if RELEASE_CHANNEL else write_zddc_server_stubs_all" branch is
collapsed since RELEASE_CHANNEL is always set under the new CLI.
Docs (CLAUDE.md, AGENTS.md, ARCHITECTURE.md, zddc/README.md) updated
to reference ./build everywhere; the per-tool sh tool/build.sh refs
stay (they're a separate, narrower entry point).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves website source + release artifacts off `main` and into a new
orphan branch named `website` in this same Codeberg repo. A `git worktree`
of that branch — typically at ~/src/zddc-website/ — is what the system
Caddy now bind-mounts and serves at zddc.varasys.io. Decoupling source
from the live site means editing source can no longer accidentally
affect what's published.
Layout going forward:
- ~/src/zddc/ — main worktree (this branch, source only).
- ~/src/zddc-website/ — git worktree of the `website` branch:
hand-edited content + LFS-tracked release
artifacts (server binaries) + regular-git
HTML tool releases + symlinks.
- Caddy bind-mount swapped: ~/src/zddc/website → ~/src/zddc-website
(quadlet at /etc/containers/systemd/caddy.container, restarted).
Build pipeline now writes releases to
${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}.
- build.sh: RELEASES_DIR points at the env var
- shared/build-lib.sh: promote_release honors the env var, falls
back to the legacy in-repo path so any
standalone single-tool release on a checkout
that still has website/ keeps working
- freshen-channel: passes ZDDC_DEPLOY_RELEASES_DIR through to
the worktree-based build
Docs (CLAUDE.md, AGENTS.md, ARCHITECTURE.md, .gitignore) updated for
the new layout. The 51 MB of website/ blobs stays in main's history
(no force-push); over time Codeberg's GC will pack them down.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructures the version picker on the releases page so channel
mirrors are first-class, selectable options:
Channels (mutable URLs)
stable — currently v0.0.8 ← default selection
beta — tracks stable
alpha — tracks stable
Pinned versions (immutable URLs)
v0.0.8
v0.0.2
v0.0.1
Picking "stable" now rewires every download link on the page to the
*_stable.html (HTML tools) or zddc-server_stable_<plat> (binary)
channel-mirror URL — the kind a user wants to copy + bookmark for a
"latest stable" reference. Same for beta and alpha. Picking a pinned
vX.Y.Z still rewires to immutable per-version URLs.
Removed the separate "Or pick a channel" chip-pill row that previously
sat under the picker. The dropdown is now the single control; chips
duplicated functionality and added visual noise. The .channel-chips
CSS rules in website/css/style.css come out with them.
Static defaults (without JS) now use the stable channel mirror URLs
too, so both copy-from-source and JS-rewire produce the same outcome
for users who want stable. The page works end-to-end with JS off.
Embedded snapshots refreshed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues from one session:
* File tree: ZDDC-conforming filenames render as a single line
even though the JS already produced two-div markup (filename-main +
filename-secondary). Cause: .tree-row__label was display:flex
(row-direction), so the two divs laid out side-by-side. Fix: wrap
each label's text in a new .tree-row__name span styled
flex-direction:column. Both file and folder code paths use the
same wrapper now; non-ZDDC entries collapse to a single
.filename-main line so typography stays consistent across the tree.
Tested by injecting a ZDDC filename into a mock directory and
asserting filename-secondary's bounding-box top is below
filename-main's bottom.
* Toast UI Editor was unreadable in dark mode. Toast UI ships with
light-only chrome; its .toastui-editor-md-container has color #222
on a transparent bg, so when mdedit's dark theme rendered the
surrounding pane in #1e1e1e the editor text fell on near-black
background → effectively invisible. Fix: add CSS overrides in
mdedit/css/editor.css that target the editor's load-bearing
surfaces (md-container, md-preview, ww-container, ProseMirror,
toolbar, mode-switch tabs, popups) and apply var(--bg) /
var(--text). Toolbar icons get a filter:invert(0.85) hue-rotate
to flip the sprite-baked dark glyphs. Both manual override
(data-theme="dark") and OS-pref auto fallback (prefers-color-scheme)
are covered. Tested by computing contrast ratios on every editor
surface in dark mode — all came in at 10:1+ (well above WCAG AA's
4.5:1).
Embedded snapshots refreshed to current main HEAD's dev build label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds shared/preview-lib.js with two cross-tool renderers:
- renderTiff (UTIF.js, lazy-loaded from CDN; PDF-style toolbar with
page nav, zoom, fit-width/fit-page; multi-page TIFFs decode lazily)
- renderZipListing (JSZip; sortable name/size/modified table, sticky
header, host-grouped paths)
Wired into the four tools that have a preview surface (archive, classifier,
mdedit, transmittal). Cross-document compatible so the same renderer works
for popup-window tools (archive/classifier/transmittal) and inline tools
(mdedit). Archive previously had no image branch at all — now previews
JPG/PNG/GIF/WebP/BMP/SVG natively, plus TIFF via UTIF, plus the ZIP listing.
Adds the dark-blue rounded-square favicon to each app's header (left of
the title) and to the website navigation. Single inline SVG, sized via
.app-header__logo (in shared/base.css) for tools and .brand-logo (in
website/css/style.css) for the website. Self-contained — the SVG carries
its own background, no wrapper styling needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the codeberg.org/varasys/zddc-server registry image, which had
no remaining consumer outside this shop. The two chart Dockerfiles
(tnd-zddc-chart) now compile zddc-server from source at build time,
fetching the right tag from a Codeberg release. release-image.sh,
zddc/Containerfile, and zddc/podman-compose.yaml are gone.
Build artifacts (HTML tools + zddc-server binaries) move from
website/releases/ in this repo to Codeberg release assets attached to
git tags. The website at zddc.varasys.io serves them by reverse-
proxying /releases/<tag>/<asset> to the corresponding Codeberg URL,
so consumers (zddc-use, level-2 bootstrap stubs, the chart
Dockerfiles) only ever talk to zddc.varasys.io.
Releases page becomes server-rendered static HTML regenerated on each
build via a single Codeberg API call. A small website/releases/manifest.json
maps <tool>-<channel> → tag for runtime channel resolution by zddc-use
and the level-2 stubs.
Files added:
- shared/publish-codeberg-release.sh — POSIX-sh helper that creates a
Codeberg release for a tag (sets prerelease flag from tag suffix)
and uploads/replaces release assets idempotently. Sourced by
build-lib.sh and zddc/release.sh.
- zddc/release.sh — replaces release-image.sh. Tags + cross-compiles
binaries via native Go (no podman needed; install Go) + uploads to
Codeberg release assets. No image build, no registry push.
Files modified:
- shared/build-lib.sh — promote_release tags + uploads via the helper
for stable AND alpha/beta now (alpha/beta were untagged before).
update_alpha removed; per-tool build.sh files no longer mirror to
website/releases/<tool>_alpha.html on plain dev builds.
- build.sh — prefers native go build over the old podman-based
cross-compile (which is gone with Containerfile). build_releases_index
queries the Codeberg API once and writes static HTML + manifest.json,
with graceful fallback when the API is unreachable.
- bootstrap/level2.html.tmpl — fetches manifest.json to resolve
channel → tag, then fetches the asset from /releases/<tag>/<asset>
(Caddy proxy). Replaces the old /releases/<tool>_<channel>.html flat
URL pattern. Operators with curl'd level-2 stubs need to re-issue
them — this is a breaking change.
- AGENTS.md, CLAUDE.md — rewritten to describe the new flow.
- .gitignore — releases/ artifacts now expected to be on Codeberg, not
committed locally.
NOT in this commit (deferred until $CODEBERG_TOKEN is provisioned):
- Backfilling existing tags as Codeberg releases.
- Cleanup commit: git rm-ing the existing artifacts in website/releases/.
Until backfill happens, those files are how operators with old
bootstrap stubs still get content. Once Codeberg has the assets,
drop them.
- The Caddy reverse-proxy config on zddc.varasys.io.
Operator-side changes (not in this repo):
- tnd-zddc-chart Dockerfile.prod and Dockerfile (dev) need updating
to compile from source rather than `FROM codeberg.org/...:stable`.
Done in a separate commit on that repo.
- Caddyfile rule for the /releases/<tag>/<asset> reverse-proxy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles a stretch of in-progress work across the SPA tools so the
tree returns to a coherent shippable state ahead of cutting a new
zddc-server stable image:
- landing: substantial rework of the project picker (sortable/filterable
table, presets refactor, ?projects= filter, ?v= channel propagation,
loading/error states)
- archive: presets cleanup, source.js refactor, filtering/url-state
alignment with the landing page
- mdedit: file-system module split, resizer, file-tree improvements,
base/toc styling tweaks
- transmittal/classifier: small template touch-ups for shared chrome
- shared: build-lib.sh helpers, new favicon.svg
- bootstrap, build.sh: pick up the channel-aware install/track zip
generation
- tests: new landing.spec.js, expanded archive/mdedit/build-label specs
- docs: CLAUDE.md picks up the zddc-server section and freshens the
alpha-build exception note
- regenerated artifacts: install.zip, track-{alpha,beta,stable}.zip,
*_alpha.html — these are produced by `sh build.sh` and per project
convention are committed alongside the source changes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every plain `sh tool/build.sh` invocation now reasserts a relative symlink
website/releases/<tool>_alpha.html → ../../<tool>/dist/<tool>.html so the
alpha hyperlinks always serve whatever dist currently holds. Idempotent — git
sees no churn on rebuild. `--release alpha` still wins by overwriting the
symlink with a real "alpha · <date> · <sha>" file; the next plain build
re-symlinks it.
Five existing alpha files become typechanges (regular file → symlink) — the
one-time migration cost. The reassertion survives deployment because the
website is served directly from the working tree.
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.