The 2dc9ad2 commit ("refactor: distribute via Codeberg release assets,
drop the upstream image") rewrote AGENTS.md and CLAUDE.md but left
several pre-existing references to the old write-to-website/releases
flow and the now-removed Containerfile / podman-compose / release-image.sh.
This sweeps the rest:
- CLAUDE.md
- drop "podman/podman-compose" from the zddc/ blurb (no Containerfile)
- drop the broken `podman build -t zddc-server zddc/` command
- rewrite the "Most-used commands" table so --release semantics match
actual behavior (tag + Codeberg upload, not file write)
- rewrite "Things that bite": replace "never write to website/releases/"
and the obsolete "alpha exception" bullet with the new rules
($CODEBERG_TOKEN required, dist files no longer force-tracked, etc.)
- rewrite the website/ description in "Repo shape" to reflect that
only index.html + manifest.json live there now
- ARCHITECTURE.md
- rewrite the website/ directory tree (no more <tool>_v*.html, _stable
symlinks, or _alpha/_beta files)
- rewrite "Channels" section: every cut now tags + uploads to Codeberg,
alpha/beta have .N counters and matching tags, no more in-place
overwrites
- rewrite the build-label table: dev builds carry the next-stable
target as a -alpha pre-release suffix with full timestamp + dirty
marker (was: "Built: <ts> BETA")
- update level-2 bootstrap description: resolves channel via
manifest.json, fetches /releases/<tag>/<asset>, not a flat URL
- update landing-tool description: ships only as Codeberg release
asset, not a committed website/releases/landing_v<X>.html
- AGENTS.md
- update website/ tree to the post-refactor layout
- replace the two-step podman build / podman-compose run blocks under
zddc-server with a Go build + go run quickstart (no container in
this repo)
- drop the "Containerfile uses a multi-stage build" note from the
"Notes" list (Containerfile is gone)
- drop the stale "landing/build.sh writes website/index.html" note —
website/index.html is now hand-edited, not produced by landing's
build
- README.md (top-level)
- tools table no longer links to /releases/<tool>_stable.html
(those URLs return 404 post-refactor); link to the releases page
once instead
- bootstrap/README.md
- update the "permanent pin" URL examples and CORS verification
snippet to use /releases/<tag>/<asset> URLs (Caddy → Codeberg)
instead of the old flat /releases/<tool>_<channel>.html pattern
- explain that channel resolution is via manifest.json now
- zddc/README.md
- rewrite Quick Start: download a release binary or build from source,
no `podman build`
- rewrite TLS examples to invoke ./zddc-server directly instead of
`podman run ... zddc-server` (image name no longer exists)
- mention ZDDC_INSECURE_DIRECT in the env-var table and the plain-HTTP
example — startup is refused without it on non-loopback binds
- replace the "Container image" section with "Distribution" (binaries
on Codeberg, no image) and the "Building" section with go build
instructions
- replace "Release Tagging" with documentation of zddc/release.sh
(the canonical replacement for release-image.sh, which is gone)
- shared/build-lib.sh
- fix the comment claiming "plain builds mirror to website/releases/"
— they don't anymore
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 KiB
AGENTS.md — ZDDC
Commands
# Build all tools (writes to dist/ only; also regenerates website/releases/{index.html,manifest.json})
sh build.sh
# Build single tool
sh tool/build.sh # archive | transmittal | classifier | mdedit | landing
# Cut a stable release (auto-increments patch version, tags <tool>-vX.Y.Z, uploads <tool>_vX.Y.Z.html to Codeberg)
sh tool/build.sh --release
sh tool/build.sh --release 1.2.0 # explicit version
# Cut an alpha/beta channel build (tags <tool>-vX.Y.Z-{alpha,beta}.N, uploads to Codeberg as a prerelease)
sh tool/build.sh --release alpha
sh tool/build.sh --release beta
# Release all tools at once
sh build.sh --release [version|alpha|beta]
# Test all tools
npm test
# Test single tool
npx playwright test tool # archive | transmittal | classifier | mdedit
# Dev server (cache-busting HTTP, on port 8000)
./dev-server start
./dev-server stop
No lint, typecheck, or format commands exist — the project is plain sh + vanilla JS.
Architecture
Five independent single-file HTML tools (archive, transmittal, classifier, mdedit, landing). Each compiles to one self-contained .html in dist/ with all CSS and JS inlined — the first four name their output dist/tool.html; landing writes dist/index.html (it's served at / by zddc-server). Tools share a small set of canonical helpers in shared/ (filename parsing, ZDDC filter UI, theme, help) — see "Shared modules" below.
tool/
css/ source stylesheets (concatenated in order)
js/ vanilla JS IIFEs (concatenated in order)
template.html placeholder markers: {{CSS_PLACEHOLDER}}, {{JS_PLACEHOLDER}}, {{BUILD_LABEL}}
build.sh assembles dist/tool.html
dist/tool.html generated output — committed with `git add -f`
shared/
base.css CSS tokens and primitives included first by every tool's CSS build
zddc.js canonical filename/folder/revision parsers, formatters, status validation
zddc-filter.js shared ZDDC project/status filter UI module
theme.js light/dark theme switcher
help.js shared help dialog module
build-lib.sh POSIX sh helpers (ensure_exists, concat_files, build_timestamp)
sourced by every tool's build.sh via: . "$root_dir/../shared/build-lib.sh"
website/
index.html hand-edited intro page (root URL)
releases/
index.html versions index, regenerated by build.sh from the Codeberg release list
manifest.json <tool>-<channel> → tag map (regenerated by build.sh; consumed by the level-2 stub at runtime)
bootstrap/
level1/<tool>.html same-origin level-1 stubs (4 tools, no landing)
track-stable/<tool>.html level-2 stubs that track the current-stable channel
track-alpha/<tool>.html level-2 stubs that track the alpha channel
track-beta/<tool>.html level-2 stubs that track the beta channel
bootstrap/
level1.html.tmpl per-project bootstrap template (relative ../<tool>.html)
level2.html.tmpl level-2 channel-tracking bootstrap template
README.md install / channel / pin docs
Critical: dist/ files are gitignored. They're the canonical built artifact for testing and the source for --release uploads to Codeberg, but they aren't checked in. Never edit them directly.
The per-version <tool>_v<X.Y.Z>.html artifacts and zddc-server binaries also aren't checked in — they live on Codeberg as release assets attached to git tags. website/releases/ only contains index.html (versions index) and manifest.json (channel → tag map), both regenerated by build.sh from a Codeberg API call.
Shared CSS (shared/base.css)
Included as the first positional arg to every tool's concat_files CSS call. Provides:
:rootCSS custom properties —--primary,--bg,--text,--border,--font, etc.- Brand color:
--primary: #2a5a8a(matches zddc.varasys.io) - Button primitive:
.btn,.btn-primary,.btn-secondary,.btn-sm,.btn-lg,.btn-link .app-header+.app-header__titlechrome rules.build-timestamp,.hidden,.truncate, webkit scrollbars
Do not define these in any tool's own CSS — they come from shared.
Toast CSS lives in classifier/css/base.css only (classifier is the only tool that uses toasts).
Transmittal CSS quirks
transmittal/css/base.cssoverrideshtml { font-size: 16px }inside@media screen— this must stay.shared/base.csssets14px; transmittal's floating labels are rem-based and were designed for 16px.- The floating label position is defined in
transmittal/css/forms.css, not Tailwind classes. If adding new Tailwind classes totemplate.html, add them totransmittal/css/utilities.csstoo — there is no Tailwind build step.
Build system rules
- Every
build.shsourcesshared/build-lib.shfirst (providesensure_exists,concat_files,build_timestamp). Setroot_dirbefore sourcing. - Build scripts use POSIX sh (
#!/bin/shwithset -eu), not bash. concat_filesaccepts positional args only (not array names).awkprocessestemplate.html, replacing{{PLACEHOLDER}}markers and stripping CDN<script>/<link>tags (pattern:https?://){{BUILD_LABEL}}is substituted in all five tools viagsubin awk (usegsub, notprint— the placeholder is inline in an HTML line). Value isBuilt: <timestamp> BETAfor dev builds,v<version>for stable releases, and<channel> · <date> · <sha>for alpha/beta channel builds; computed before the awk step. The sharedis_redflag controls whether the label is wrapped in a red+bold<span>(true for dev/alpha/beta, false for stable).- Cleans up temp files via
trap cleanup EXIT
</ escaping is mandatory. Any JS containing </tag> inside string or template literals will break inline <script> embedding. Run:
sed 's#</#<\\/#g' "$input_js" > "$safe_js"
Required for any new tool with vendor JS or JS containing HTML template literals.
JS module pattern
All JS is vanilla, no bundlers. Files are IIFEs, registered on window.app.modules. Load order = declaration order in build.sh. window.app is the only global.
(function() {
window.app.modules.mymodule = { ... };
})();
Exception: archive uses plain globals (APP_STATE, top-level functions) — not the IIFE/modules pattern.
ZDDC filename parsers
All parsing/formatting goes through shared/zddc.js, exposed as window.zddc. Tools call it directly — no per-tool wrappers.
window.zddc exports:
parseFilename(name)→{ trackingNumber, revision, status, title, extension, valid } | null(extension WITHOUT leading dot)parseFolder(name)→{ date, trackingNumber, status, title, valid } | nullparseRevision(rev)→{ base, modifier, modifierType, modifierNumber, isDraft, modifierIsDraft, full }compareRevisions(a, b)→ number (canonical sort order)formatFilename(parts)/formatFolder(parts)— round-trips parsed outputisValidStatus(code)— accepts known status codes plus---
All file objects across tools use file.trackingNumber (string) and file.extension (string, no leading dot, e.g. 'pdf' not '.pdf'). When concatenating into a filename, write name + '.' + ext.
Coverage lives in tests/zddc.spec.js (47 cases). Add new edge cases there, not in tool tests.
Testing quirks
- Playwright + Chromium only (File System Access API requirement)
- Tests open
dist/tool.htmlviafile://protocol — always build before testing - File System Access API is mocked via
page.addInitScript()usingtests/fixtures/mock-fs-api.js - Use
waitUntil: 'load'or'domcontentloaded'not'networkidle'— bundled scripts keep the network "active" - Archive's
#noDirectoryMessageempty-state overlay isposition: absolute; top: 50px— it must clear the header or it will block button clicks in tests
ZDDC filename convention
Format: trackingNumber_revision (status) - title.extension
trackingNumber: no spaces or underscores (e.g.123456-EL-SPC-2623)revision:A,B,0; draft prefix~; modifiers+C1,+B1,+N1,+Q1status:IFA IFB IFC IFD IFI IFP IFR IFU REC RSA RSB RSC RSD RSIor---- Folder names prefix with date:
2025-10-31_trackingNumber (status) - title
Git workflow
- Feature-branch workflow; squash-merge feature branches to
main - Conventional commits:
feat(archive): ...,fix(transmittal): ... - Release tags:
archive-v1.0.0(per-tool semver) - Commit dist files:
git add -f tool/dist/tool.html
Releasing — channels and layout
Three channels. Versioning is pre-release semver: stable owns clean vX.Y.Z; alpha and beta carry vX.Y.Z-alpha.N / vX.Y.Z-beta.N. The next-stable target X.Y.Z is patch-bumped from the latest clean <tool>-vX.Y.Z tag.
Storage model. Built artifacts live on Codeberg as release assets attached to git tags — not committed to this repo. The website at zddc.varasys.io serves them by reverse-proxying /releases/<tag>/<asset> to the corresponding Codeberg URL, so consumers (operators' bootstrap stubs, zddc-use) only ever talk to zddc.varasys.io. Channel resolution is via website/releases/manifest.json — a small file build.sh regenerates from the Codeberg API and commits.
- Stable:
sh tool/build.sh --release [version](or just--releaseto auto-bump patch from the latest stable tag). Tags<tool>-v<version>, uploads<tool>_v<version>.htmlas a release asset on Codeberg. Label:vX.Y.Z(black). Skips silently if source has not changed since the latest stable tag (HEAD-vs-tag diff). - Beta:
sh tool/build.sh --release beta. Tags<tool>-v<next-patch>-beta.N, uploads<tool>_v<next-patch>-beta.N.html. Label:vX.Y.Z-beta · <date> · <sha>(red). - Alpha:
sh tool/build.sh --release alpha. Tags<tool>-v<next-patch>-alpha.N, uploads. Label:vX.Y.Z-alpha · <date> · <sha>(red). - Plain dev builds (no
--release): producetool/dist/<tool>.htmlonly. No website/releases side-effect, no Codeberg upload. To publish, re-run with--release alpha.
After any release run, sh build.sh queries the Codeberg API once and rewrites website/releases/index.html and manifest.json. Commit those alongside the release.
After cutting a release, run git push --tags to publish the tag.
$CODEBERG_TOKEN must be exported before any --release invocation. The promote_release helper calls publish_codeberg_release which uses the token to create the release and upload the asset.
website/index.html (the root URL of zddc.varasys.io) is hand-edited static content, not built by landing/build.sh. The landing tool ships only as a Codeberg release asset; the self-contained install snippet curls landing_v<ver>.html to <deployment-root>/index.html at customer-deployment time.
Channel discipline (MUST rules)
The build system does not enforce these. Treating channels carelessly defeats the point of having three. Be disciplined.
- Stable doesn't regress. No known-broken features that worked in the previous stable. If you ship
v0.0.5with a bug, the path forward isv0.0.6with a fix — never editv0.0.5in place. Stable files are immutable. - No backports. Don't try to patch an older stable version. Always cut a new stable at a higher version. Users pinned to the old version stay pinned by their own choice; they can move forward when they want.
- Alpha and beta are mutable. Document this anywhere you invite users to test them. Pinning
?v=alpha(or_alpha.html) in a production deployment is a mistake; it gets rebuilt without notice. - Stale-channel rule. Users tracking alpha (or beta) MUST never see a build older than current stable. After every stable release, run
./freshen-channel <tool> alphaand./freshen-channel <tool> betaso each channel is at-least-current. This is not optional. - Hotfix path. For critical bugs: fix on
main, cut a new stable (no beta soak required), then freshen alpha + beta. Tag the commit messagefix:or include "hotfix" so the intent is visible ingit log. - Beta soak before promoting (recommended). Give a beta a few days of exposure before cutting the same code as stable. Not enforced; use judgment for trivial changes.
Freshen helper
./freshen-channel <tool> <channel> rebuilds the alpha or beta channel of a tool from its current stable tag, cutting a new pre-release tag (e.g., <tool>-v<next-patch>-alpha.N) and uploading the asset to Codeberg. Use it after every stable release (rule 4 above) and any other time alpha/beta has fallen behind stable.
./freshen-channel archive alpha
./freshen-channel transmittal beta
What it does:
- Finds the latest
<tool>-v*clean stable tag. - Creates a temporary git worktree at that tag — does not touch the main worktree's HEAD or working tree.
- Runs
<tool>/build.sh --release <channel>inside the worktree, which tags<tool>-v<next-patch>-<channel>.Nand uploads to Codeberg. - Removes the worktree.
The on-page label of the freshened build is v<next-stable>-<channel> · <today> · <stable-tag-sha> — the SHA pins which stable was used as the source, recoverable via git checkout.
Note: the build pipeline used is the one at the tag, not on main. That is intentional (pure reproducibility). If you have made build-system improvements since stable was cut and want the freshen to use them, cut a new stable first.
Bootstrap stubs
build.sh regenerates website/bootstrap/ on every invocation:
bootstrap/level1/<tool>.html— 4 same-origin level-1 stubs (archive, transmittal, classifier, mdedit; landing has no level-1 stub since it only lives at deployment root).bootstrap/track-{alpha,beta,stable}/<tool>.html— 5 level-2 stubs per channel that resolve the channel viazddc.varasys.io/releases/manifest.jsonand fetch the asset viazddc.varasys.io/releases/<tag>/<tool>_v<version>.html(Caddy proxies to Codeberg).
End users install via copy-paste shell snippets on the home page's "Install on your server" section — each snippet curls the relevant stubs (or a one-shot version-pinned HTML, for the self-contained option) into the operator's deployment directory.
See bootstrap/README.md for the install / pin / audit story.
Worktrees
Use git worktree to run multiple agents on separate branches simultaneously without filesystem collisions.
- Worktrees live at
~/src/zddc-<branch-name>(sibling of the main clone) - Before starting work on a feature branch, check
git worktree list; if no worktree exists, create one:git worktree add ~/src/zddc-<branch-name> -b <branch-name> - All edits, builds (
sh build.sh), and tests (npm test) run from within the worktree directory — build scripts use relative paths so this works correctly - The
dist/force-commit rule (git add -f) applies per-worktree - After the branch is merged, clean up:
git worktree remove ~/src/zddc-<branch-name>then delete the branch - Never run
git checkoutorgit switchinside a worktree that another agent may be using
Transmittal-specific
- Two-phase hydration:
populateStatic()before publish,hydrate()on load of published file - Reactive state via Proxy —
app.state.mode = 'view'auto-notifies subscribers - Runtime CDN loads (jszip, docx-preview, xlsx) are allowed only for the optional DOCX/XLSX preview; core features work offline
- Published payload stored in
<script id="transmittal-data" type="application/json">
mdedit-specific
css/tailwind-utils.cssis a pre-generated static subset (~80 classes). Add new Tailwind classes here; do not re-run Tailwind.- Toast UI Editor v3.2.2 is bundled in
vendor/;template.htmlloads it from CDN for dev convenience </escaping is essential:sed 's#</#<\\/#g'runs on both app JS and vendor JS at build time
zddc-server
Go HTTP server sub-project living at zddc/. Replaces caddy file-server --browse for ZDDC archives.
Build
zddc-server ships as a cross-compiled binary, not a container image. There's no Containerfile or compose file in this repo (the chart Dockerfiles in tnd-zddc-chart compile from source at deploy time, fetching the right tag from Codeberg).
# Compile a local binary for the host platform (requires Go 1.24+)
(cd zddc && go build -o zddc-server ./cmd/zddc-server)
# Or run directly without producing a binary
(cd zddc && go run ./cmd/zddc-server)
The repo's top-level sh build.sh cross-compiles the four release binaries (linux/amd64, darwin/amd64, darwin/arm64, windows/amd64) into zddc/dist/ when Go is on PATH. It's silently skipped otherwise — the HTML tools build regardless.
Run (development)
ZDDC_ROOT=/path/to/your/archive ZDDC_TLS_CERT=none ZDDC_ADDR=:8080 \
go run ./cmd/zddc-server
For a release binary (downloaded from Codeberg or built via sh build.sh):
ZDDC_ROOT=/path/to/your/archive ZDDC_TLS_CERT=none ZDDC_ADDR=:8080 \
./zddc/dist/zddc-server-linux-amd64
Key environment variables
| Variable | Default | Purpose |
|---|---|---|
ZDDC_ROOT |
(required) | Path to served file tree |
ZDDC_ADDR |
:8443 |
Bind address |
ZDDC_EMAIL_HEADER |
X-Auth-Request-Email |
Header set by upstream proxy with user email (oauth2-proxy / nginx auth-request convention) |
ZDDC_INDEX_PATH |
.archive |
Virtual archive index URL segment |
ZDDC_LOG_LEVEL |
info |
Logging verbosity |
ZDDC_CORS_ORIGIN |
https://zddc.varasys.io |
Comma-separated CORS allowlist; empty value disables CORS. Default lets tools served from zddc.varasys.io call back into a customer-deployed server. |
Release tagging
zddc/release.sh is the canonical path. It tags the commit, compiles
the binaries (native Go), and uploads them as Codeberg release assets.
There's no container image build / push anymore — the chart's
Dockerfile.prod and Dockerfile (dev) compile zddc-server from
source at build time, fetching the right tag from Codeberg directly.
The upstream codeberg.org/varasys/zddc-server registry is frozen
(historical tags only).
sh zddc/release.sh # alpha cut, version auto-derived
sh zddc/release.sh alpha # same
sh zddc/release.sh beta # beta cut
sh zddc/release.sh stable # stable cut, patch++ from latest stable
sh zddc/release.sh stable 0.1.0 # stable cut, explicit version
Default channel is alpha so a stable-equivalent tag never
appears by accident during active development. Pass beta to soak;
pass stable only when deliberately promoting. The script tags the
commit but does NOT push — finish with git push origin <branch> and
git push origin <tag>.
Versioning — pre-release semver. Stable cuts get clean vX.Y.Z
tags. Alpha and beta cuts get vX.Y.Z-alpha.N / vX.Y.Z-beta.N
where X.Y.Z is the next patch of the latest clean stable and N
is a per-channel counter that resets when stable advances. Example
sequence (current stable v0.0.7):
alpha → v0.0.8-alpha.1
alpha → v0.0.8-alpha.2
beta → v0.0.8-beta.1
alpha → v0.0.8-alpha.3 (alpha and beta count separately)
stable → v0.0.8 (counter resets at next-patch advance)
alpha → v0.0.9-alpha.1
Pre-release semver ordering (0.0.8-alpha.1 < 0.0.8-alpha.2 < 0.0.8-beta.1 < 0.0.8) is honored by all standard tooling — Codeberg
release sorting, git tag --sort=-v:refname, sort -V, npm, cargo —
so consumers can pin or compare versions without surprises.
Binary publishing — release.sh uploads the four cross-compiled
binaries (zddc-server-{linux,darwin,windows}-{amd64,arm64}) as
release assets attached to the new git tag on Codeberg. The website
at zddc.varasys.io reverse-proxies /releases/<tag>/<asset> URLs to
the corresponding Codeberg release-asset URL, so consumers
(zddc-use, the level-2 bootstrap stubs, the dynamic chart
Dockerfiles) only ever talk to zddc.varasys.io.
After publishing: run sh build.sh to refresh
website/releases/index.html and manifest.json against the new
release list, and commit those.
Prerequisites:
- Go 1.24+ on PATH (or run from a Go container).
$CODEBERG_TOKENexported, scoped to write the VARASYS/ZDDC repo.
There is no CI for this — solo workflow benefits from one canonical local path that fails loudly and visibly on the developer's terminal.
Notes
- No external test framework yet — Go unit tests run with
go test ./...insidezddc/(requires Go 1.24+) - Portfolio files (
*.portfolio) in the served tree appear as virtual group directories - Every folder exposes a
.archivevirtual directory backed by the same global index — the depth in the URL only matters so HTML produced for offline use can reach.archive/via../.archive/relative links and have the browser resolve them before the request hits the server. The flat listing emits two redirect entries per tracking number:<tracking>.html(highest base rev) and<tracking>_<rev>.html(each specific base rev). Both redirect to the first chronologically received copy of the named revision. Modifier files (<tracking>_<rev>+C1.htmletc.) remain reachable via the resolver but are not surfaced in the listing — they're return traffic, not primary documents. ACL is the only filter: the listing endpoint is gated by the contextPath's.zddcchain, and each entry is then filtered against the ACL of its resolved file's directory; per-target denials return 404 rather than 403 to avoid leaking that the tracking number exists in another subtree - ACL is enforced via cascading
.zddcYAML files; authentication is delegated to the upstream proxy via theX-Auth-Request-Emailheader (configurable withZDDC_EMAIL_HEADER) .zddcschema also supports a top-leveladmins:glob list, peer toacl.allow/acl.deny. Honored only at the root.zddc(subdiradminsentries are ignored to prevent privilege escalation via subtree write access). Drives the built-in debug dashboard at/.admin/(sub-routes:/whoami,/config,/logs); non-admin requests get 404 so the page is invisible. Seezddc/README.md§ "Admin Debug Page".- Reserved entry prefixes under
ZDDC_ROOT:.-prefixed entries are excluded from listings AND 404 on direct fetch (only.archiveand.adminare exempt) — for invisible side-state like dev-shell home dirs._-prefixed entries are excluded from listings only — for operator scaffolding like the_template/directory created by the self-contained install snippet, still reachable by direct URL. Drop side-state under_if it should be linkable; under.if it should be unreachable.