Commit graph

229 commits

Author SHA1 Message Date
a7faeed8fb refactor(ci): extract chart-bump logic to .forgejo/scripts/
Both notify-chart-dev.yml and the notify-chart-prod job in
deploy-release.yml were carrying ~80 lines of inline shell each,
duplicating the clone-bump-push flow. Extract to a single script:

  .forgejo/scripts/notify-chart-bump.sh <beta|stable> [VERSION]

Three benefits:

1. **Locally testable**. The script is invocable directly:
     CHART_FORGEJO_TOKEN=$FORGEJO_TOKEN \
       .forgejo/scripts/notify-chart-bump.sh beta
   No more "push to main and watch what the runner does" debug loop.

2. **Manual escape hatch**. When CI is broken, the same script is
   how we recover. The 0.0.16-beta-1ddd331 chart bump preceding
   this commit was performed via this very script.

3. **Runner-quirk-immune**. The previous three commits chased a
   Forgejo runner v12.9.0 phantom-SIGPIPE bug that would only
   surface under the runner's `bash -e -o pipefail {0}` wrapper.
   A real script with its own `#!/usr/bin/env bash` and explicit
   error handling sidesteps the wrapper entirely.

The workflow YAMLs shrink to checkout + run-script. No GITHUB_OUTPUT
plumbing, no inline if/else gates, no shell flag overrides. The
behavior is identical to the prior inline versions.
2026-05-04 07:49:16 -05:00
b14d8a3e38 chore(embedded): cut v0.0.16-beta
Some checks failed
Notify chart dev on beta cut / notify-chart-dev (push) Failing after 4s
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>.
2026-05-03 21:39:28 -05:00
582db6d86d feat(browse): vendored JSZip, SVG home icon, auto-filter rows
- Vendor JSZip locally (shared/vendor/jszip.min.js) and bundle into
  the browse build instead of CDN-loading. Eliminates the failure
  mode where ZIP rows can't expand because the CDN script doesn't
  load (CSP, network, etc.). Tool now works fully offline.
- Replace the toolbar filter input + ext multi-select with two
  spreadsheet-style auto-filter rows in <thead>:
    - 📄 row: file-name filter + extension filter
    - 📁 row: folder-name filter
  Each input uses shared/zddc-filter syntax (substring/!negate/
  ^startsWith/$endsWith/regex/| or/space and).
- New visibility model with ancestor-of-match awareness:
    - file matches keep their ancestor folders visible (path-to-hit)
    - folder match keeps its descendants visible
    - filters compose (file ∧ folder ∧ ext) so combinations narrow
  Computed model-side; render walks only visible nodes.
- Replace 🏠 emoji breadcrumb-root with an inline outline-stroke SVG
  that tints with currentColor.
2026-05-03 21:35:15 -05:00
6e80e2bf12 release: v0.0.15 lockstep
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 4s
Build + deploy releases / build-and-deploy (push) Successful in 8s
Build + deploy releases / notify-chart-prod (push) Successful in 1s
2026-05-03 20:43:41 -05:00
a6c3c9df5e chore(headers): standardize titles + refresh icon across tools
Cross-tool header inconsistencies cleaned up after the audit
prompted by the browse Phase 2 work:

  - landing/template.html: title was 'ZDDC Archive' (a holdover from
    when landing WAS the archive). The page is now the project
    picker — title shortened to plain 'ZDDC'. Browser tab title
    follows: 'ZDDC Archive — Projects' → 'ZDDC — Projects'. Title +
    build label wrapped in title-group div for layout consistency
    with archive/classifier/mdedit/browse.
  - form/template.html: title was bare; same title-group wrapping.
    The id='form-title' stays — its content is overwritten at
    runtime by form.js based on the form schema's name.
  - classifier/template.html: refresh button text 'Refresh' →
    '⟳' icon to match archive + browse. Same title attribute, just
    smaller visual weight.

Untouched (intentionally):
  - archive's button stays 'Add Local Directory' + addDirectoryBtn
    id — semantically different from the others (archive
    accumulates multiple directories; everyone else operates on
    one). The naming reflects that.
  - transmittal — different layout entirely (page-header with
    sender/receiver logo cells); not a candidate for app-header
    standardization.
2026-05-03 20:42:49 -05:00
d874643af5 release: v0.0.14 lockstep
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 3s
Build + deploy releases / build-and-deploy (push) Successful in 8s
Build + deploy releases / notify-chart-prod (push) Successful in 1s
2026-05-03 20:40:02 -05:00
424bf8e769 feat(browse): Phase 2 — preview popup, ZIP expansion, ext filter, breadcrumbs
Bundles Phase 2 polish + the user-requested header/breadcrumb work:

- Breadcrumbs replacing the plain currentPath span. Server mode
  renders linkified ancestor segments (each <a> navigates to that
  directory; the browser fetches browse.html, the new instance
  auto-loads the listing). FS-API mode renders the rootHandle name
  as a non-link (no ancestor handles to navigate). Both prefix the
  path with a 🏠 root icon. Trailing slash + bold-current segment
  match common file-explorer conventions.

- Subdued 'Select Directory' button in server mode. Once browse is
  serving a real directory listing, the local-folder switcher is
  available but visually quiet (btn--subtle: transparent, muted
  color). FS-API mode keeps the primary styling (it's how the user
  got there). New btn--subtle CSS class added to browse's tree.css.
  A refresh button (⟳) appears next to it in both modes; clicking
  it re-fetches the current root listing.

- Header consistency: browse now matches archive's header layout
  (refresh + help buttons in addition to theme on the right). Help
  is a placeholder for future help dialog wiring.

- File preview popup. Click a file row → opens a popup window with
  the file rendered. Plain types (PDF, HTML, image) load in
  iframes; TIFF + ZIP listings via shared/preview-lib.js's
  renderTiff / renderZipListing helpers; text via <pre>; unknown
  types → 'click Download' placeholder. Modifier-click (ctrl/cmd/
  shift) and middle-click still open the file in a new tab via the
  underlying <a target=_blank>. Single popup window is reused
  across multiple file clicks (matches archive's UX).

- ZIP inline expansion. .zip files have a chevron and act like
  folders in the tree. First expand fetches the zip bytes
  (server URL or FS handle or parent-zip read), parses with JSZip
  (auto-loaded from CDN), and synthesizes the entry tree. Nested
  directories within the zip lazy-expand on demand by re-walking
  the cached entry list at the right path prefix. Click on a
  zip-entry file opens the preview popup with bytes read from
  JSZip. Recursive expand-all skips zip archives by design — they
  can be very large, and explicit click-to-expand is safer.

- Extension multi-select filter. Toolbar now has a <select
  multiple> populated with extensions present in the current
  view. Filter is OR-of-selected; combined with the name filter
  it's AND-of-both. Folders pass through (so expanding a folder
  whose name doesn't match the ext filter still shows its file
  children that do match).
2026-05-03 20:39:49 -05:00
127163dfa2 release: v0.0.13 lockstep
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 3s
Build + deploy releases / build-and-deploy (push) Successful in 8s
Build + deploy releases / notify-chart-prod (push) Successful in 1s
2026-05-03 20:21:06 -05:00
7caf3ecf3f fix(browse): listing fetch + row height + recursive expand/collapse
Three issues from initial v0.0.12 dev/prod testing:

  1. Online listings empty.
     directory.go was missing Vary: Accept on its responses, so
     browser/CDN cached the HTML response (the embedded browse.html)
     and served it again when browse's JS later fetched the same URL
     with Accept: application/json. JSON parse failed, autoDetect
     returned null, empty state showed. Adds Vary: Accept on both
     branches and changes browse.html cache-control to no-cache so
     deployed updates land immediately.

  2. Top-level folder rows tall, shrink as subtree expands.
     The .browse-table had flex:1 in a flex column. <table> in flex
     doesn't reliably distribute height across rows — with few rows,
     each row stretched. Wrap the table in a div with overflow:auto
     and drop flex:1 from the table itself.

  3. Recursive expand/collapse.
     Shift-click (or alt-click) on a folder now expand-all or
     collapse-all its subtree. Plain click still toggles just that
     folder. Implementation: tree.expandSubtree() walks BFS, loading
     each level's children in parallel, re-rendering between levels
     so the user sees progress. tree.collapseSubtree() recursively
     marks the subtree collapsed (children stay loaded for instant
     re-expand).
2026-05-03 20:20:54 -05:00
d6448159fa release: v0.0.12 lockstep
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 3s
Build + deploy releases / build-and-deploy (push) Successful in 8s
Build + deploy releases / notify-chart-prod (push) Successful in 1s
2026-05-03 19:59:38 -05:00
fb13ff4fd8 feat(browse): generic directory listing tool — default at folder URLs
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 5s
A new HTML tool — browse — that lists the contents of any directory.
Designed for ZDDC archives but no ZDDC-specific filtering; just a
straight folder browser with expand/collapse, sort, and name filter.

Modes (auto-detected at page load):
  - Online: when served by zddc-server at a folder URL, queries
    the same URL with Accept: application/json to load the listing
    and renders it. Auto-served as the default at any directory
    under ZDDC_ROOT without an index.html (replacing the previous
    minimal-HTML stub from directory.go).
  - Local: 'Select Directory' button uses FileSystemAccessAPI to
    pick any folder on disk; works in Chromium-based browsers.

Features (Phase 1 — what's in this commit):
  - Tree view with lazy-loaded folders (children fetched on first
    expand).
  - Sort by name / size / extension / date (column header click).
  - Filter by name substring (toolbar input).
  - File click opens in a new tab — for server-backed pages,
    routes through zddc-server's normal handler so .archive
    redirects + apps cascade overrides + ACL all apply.

Phase 2 deferred:
  - ZIP files inline expansion (treat archive entries as virtual
    children).
  - File preview popup (reuse shared/preview-lib.js).
  - Extension multi-select filter.

Wiring:
  - browse/ added to top-level ./build's per-tool list, embed
    block, versions.txt, and the lockstep release commit + tag set.
    All seven tools (archive, transmittal, classifier, mdedit,
    landing, form, browse) advance together on stable cuts.
  - shared/build-lib.sh: browse added to ZDDC_RELEASE_TOOLS and
    verify_channel_links's per-tool loop.
  - zddc/internal/apps/embed.go: //go:embed browse.html +
    EmbeddedBytes("browse") case.
  - zddc/internal/apps/availability.go: browse available at every
    directory (same as archive).
  - zddc/internal/apps/handler.go: MatchAppHTML routes
    /<dir>/browse.html → 'browse'.
  - zddc/internal/handler/directory.go: when a directory request
    arrives with Accept: text/html and no index.html exists,
    serve the embedded browse.html bytes (with a JSON-fallback
    if the embedded slot is empty during bootstrap).
2026-05-03 19:56:51 -05:00
1033d30ad9 fix(ci): notify-chart workflows push to Forgejo, not GitHub
The chart repo (BMCD/tnd-zddc-chart) is mirrored Forgejo→GitHub
one-way (we set this up so the chart matches the same canonical-
on-Forgejo pattern as the public repos). When notify-chart-prod
and notify-chart-dev pushed directly to GitHub, the bump landed
on GitHub but Forgejo never got it — and the next time Forgejo's
push-mirror ran, it force-overwrote GitHub's bump with Forgejo's
older state. Symptom: prod stuck at v0.0.9 even after auto-bump
appeared to succeed; manual investigation showed Chart.yaml
appVersion was actually still 0.0.10 (the previous manual bump
that DID land on Forgejo).

Fix: clone+push to Forgejo (git.varasys.io/BMCD/tnd-zddc-chart)
instead of GitHub. Forgejo's mirror replicates to GitHub on the
next sync — going through the canonical-Forgejo path keeps both
sides in sync. Uses a new CHART_FORGEJO_TOKEN secret (separate
from CHART_GITHUB_TOKEN, which is no longer needed for these
workflows but kept for any future direct-GitHub use case).
2026-05-03 19:39:48 -05:00
bf54651fb0 release: v0.0.11 lockstep
All checks were successful
Notify chart dev on beta cut / notify-chart-dev (push) Successful in 3s
Build + deploy releases / build-and-deploy (push) Successful in 8s
Build + deploy releases / notify-chart-prod (push) Successful in 3s
2026-05-03 19:03:05 -05:00
042884ac5d ci: notify-chart-prod also bumps chart develop on stable cut
Per the bake-in invariant: when no active beta exists, dev tracks
stable. Previously notify-chart-prod only bumped chart's main, so
a stable cut updated BMCD prod but left dev one cut behind until
the next beta or manual dispatch.

Now the job loops through {main, develop} and pushes the same
appVersion bump to both. Each branch push triggers its own BMCD
pipeline (pipeline-prod for main, pipeline-dev for develop), so
prod + dev both rebuild against the new ZDDC stable in parallel.
notify-chart-dev.yml continues to handle the beta-cut path
(advances develop ahead of main between stable cuts).
2026-05-03 19:02:53 -05:00
915ab8a87a fix(preview): make HTML iframe links navigate (zddc-server-backed archive)
User report: opening an .html file with a '../.archive/' hyperlink in
a new tab works (zddc-server intercepts and serves the right file),
but clicking the same link inside the file previewer does nothing.

Two combined causes:

  1. The previewer's iframe was loaded from a blob: URL (built from
     the file's bytes). Relative URLs in the iframe resolve relative
     to the blob URL — '../.archive/X.html' becomes 'blob:.../.archive/
     X.html', which is gibberish. The browser never sends a request to
     the server, so the .archive interception never fires.

  2. sandbox="" disables every iframe capability including popups,
     so even <a target=_blank> is silently swallowed.

Fix per tool:

  - archive (table.js): for HTML preview, use file.url (the real
    server URL) directly when available; fall back to blob only for
    File-System-Access-API mode where there's no server to intercept
    anyway. Now relative links in archived HTMLs resolve against the
    actual server origin and the .archive interception fires as
    designed. Sandbox loosens to allow-same-origin + allow-popups +
    allow-popups-to-escape-sandbox so resources within the iframe
    load and link clicks (default target / target=_blank / middle-
    click) work normally. allow-scripts is intentionally NOT set —
    archived HTML still cannot run JS in the popup's origin.

  - transmittal (files-preview.js) + classifier (preview.js): same
    sandbox loosening for consistency. These tools' files are
    typically local (FileSystemAccessAPI), so the file.url branch
    doesn't apply — relative URLs that depend on a server still
    won't resolve in local mode (intrinsic limitation, no server).

Tested behavior preserved:
  - PDFs: unchanged (no sandbox, browser's PDF viewer handles).
  - Images / docx / xlsx / tiff / zip / text: unchanged.
  - HTML in zddc-server-backed archive: relative '../.archive/' links
    now navigate the iframe to the correct target file.
2026-05-03 18:54:55 -05:00
2820dffeaa fix(ci): single-line commit messages in notify workflows (YAML pipe block)
Multi-line git commit message bodies broke YAML parsing — pipe blocks
end on unindented lines, so the body lines starting at column 0 were
being interpreted by Forgejo's YAML parser as keys, yielding:

  yaml: line 158: could not find expected ':'

Switch to repeated `-m` flags (one per paragraph). Same end result
in git log; valid YAML.
2026-05-03 18:38:25 -05:00
b47b5222af fix(build): remove skip-if-unchanged in lockstep stable cut
The skip-if-no-source-changes-since-latest-tag check in
promote_release was a relic of per-tool independent versioning.
In the lockstep era it actively breaks CI re-cuts at a tag commit:

  - HEAD is at the v0.0.10 release commit
  - latest archive-v* tag is archive-v0.0.10 (== HEAD)
  - git diff archive-v0.0.10 HEAD = empty
  - SKIP archive promote → no archive_v0.0.10.html written
  - dist/release-output/ stays at whatever was seeded from
    /srv/zddc/releases/ (i.e. v0.0.9 from the previous deploy)
  - ./deploy --releases rsyncs that → live site STAYS at v0.0.9

Symptom: tag-triggered Forgejo deploy-release.yml workflow runs
(run 16) reports success but /srv/zddc/releases/archive_stable.html
still points at archive_v0.0.9.html.

Fix: always run _promote_stable for every tool on a stable cut.
The bytes written are deterministic at the same source, so
overwriting an existing per-version file is a no-op on disk —
the actual work the cut performs is advancing the symlink chain
(_v<X.Y>, _v<X>, _stable, _beta, _alpha) to the new version.
2026-05-03 18:37:03 -05:00
2f9f26a544 ci: auto-bump tnd-zddc-chart appVersion on ZDDC cut
Closes the loop on the user-described workflow:

  1. Iterate on tools / cut alpha → no chart involvement.
  2. `./build beta` → embedded/ commits to ZDDC main →
     notify-chart-dev.yml pushes a chart appVersion bump to
     burnsmcd/tnd-zddc-chart's develop branch → BMCD pipeline-dev
     fires automatically → dev image rebuilt with new beta bytes
     baked in.
  3. `./build release` → tag pushed → existing deploy-release.yml's
     new notify-chart-prod job pushes a chart appVersion bump to
     burnsmcd/tnd-zddc-chart's main branch → BMCD pipeline-prod
     fires automatically → prod image rebuilt with new stable bytes.

The chart repo IS still committed to (one Chart.yaml line, auto-
generated by either workflow), but no human ever touches it for
routine ZDDC releases. The chart commits are idempotent (skip if
appVersion already at target) and clearly marked as bot-generated.

The truly chart-commit-free version would require either (a)
BMCD's private helm-deploy-latest reusable to accept --set overrides
we'd compute, or (b) bypassing it entirely with our own helm step.
Both are deeper changes than this PR; this is the simplest reliable
solution within the existing reusable.

Auth: a new repo-scoped Forgejo Actions secret CHART_GITHUB_TOKEN
holds the classic GitHub PAT (already provisioned for the
Forgejo→GitHub mirror; same token, repo+workflow scopes,
SAML-SSO authorized for burnsmcd). The bot identity is
'ZDDC Release Bot <noreply@zddc.varasys.io>'.

Tested behavior:
- Workflow files are added by THIS commit. Pushing this commit
  does not fire either workflow (notify-chart-prod requires a
  tag; notify-chart-dev requires changes under
  zddc/internal/apps/embedded/). Safe to land before testing.
- First real test fires on the next ZDDC stable cut or beta cut.
2026-05-03 18:16:50 -05:00
f5ffd408f2 release: v0.0.10 lockstep
All checks were successful
Build + deploy releases / build-and-deploy (push) Successful in 8s
2026-05-03 17:11:46 -05:00
3494053421 fix(preview): render HTML files instead of showing literal source
HTML files in the file previewer (archive, transmittal, classifier
popups) were dispatched to the text renderer because 'html'/'htm'
are in shared/preview-lib.js's TEXT_EXTENSIONS (which is shared with
the syntax-highlighting code path). Result: opening an .html file in
preview showed its source as a <pre> block, not the rendered page.

Fix in each tool's popup builder + dispatcher:

  - Add 'html' / 'htm' to the iframe branch (alongside pdf), so the
    popup ships an <iframe src=blob:...> instead of an empty
    #previewContent div. The blob's MIME type from getMimeType()
    is already 'text/html', so the browser renders natively.
  - Skip the text-render dispatch for html/htm (the iframe is enough).
  - Add  to the HTML iframe so an arbitrary archived
    HTML file cannot run scripts, navigate top, submit forms, or
    open popups in the popup-window's origin. PDFs don't need this
    since the browser's PDF viewer is sandboxed natively.

classifier/js/preview.js uses a getPreviewType() switch instead of
chained ifs; adds 'html' as its own preview type (checked BEFORE
'text' since html is in TEXT_EXTENSIONS).

mdedit already handled HTML specially (file-tree.js has an isHtml
check); no change there.

TIFF was already rendered via the shared zddc.preview.renderTiff
canvas viewer in all four tools — no change needed for that path.
If TIFF preview appears broken on the live prod server, that's the
v0.0.9-alpha-baked-in image; the fresh stable redeploy fixes it.
2026-05-03 16:48:19 -05:00
8dbd002727 fix(build): commit embedded artifacts before tagging; alpha never bakes in
Two related fixes to the lockstep release flow + the project invariant
that prod must always run stable bytes (and dev only ever beta-or-stable).

1) tag-after-commit ordering. `./build release X.Y.Z` previously
   regenerated zddc/internal/apps/embedded/* with stable labels but
   tagged BEFORE folding those changes in. The tag landed on the
   source-side commit (alpha-dirty embedded), and the operator was
   expected to commit the embedded changes as a follow-up — which got
   dropped in practice, leaving prod binaries with alpha-dirty bytes
   baked in. (See the v0.0.9 re-anchor in the immediately preceding
   commit for the manifestation.)

   Refactor:
   - _promote_stable / promote_zddc_server in shared/build-lib.sh
     no longer call `git tag`. They keep their pre-flight check
     (now: tag must be in HEAD's history rather than == HEAD, since
     HEAD will advance after the release commit).
   - Top-level ./build adds a new "Release commit + tag" block at
     the end of stable cuts: stages the regenerated embedded files,
     makes a `release: vX.Y.Z lockstep` commit, and tags all seven
     artifacts at the new commit. Idempotent — no commit if there
     are no changes.

2) bake-in invariant. Plain `./build` and `./build alpha` now
   leave zddc/internal/apps/embedded/ untouched — the binary keeps
   shipping whatever the last beta or stable cut wrote. `./build
   beta` and `./build release` are the only paths that update
   embedded bytes. Active dev iteration uses tool/dist/<tool>.html
   directly; the binary's embedded copy is the default fallback,
   not a workbench.

Verification on this commit:
  ./build       → embedded mtime unchanged, no "M" lines for embedded/
  ./build alpha → embedded mtime unchanged, no "M" lines for embedded/

Docs updated to match in CLAUDE.md "Things that bite" + AGENTS.md
"Releasing — lockstep" + the leading help text in ./build itself.
2026-05-03 16:44:39 -05:00
b15382ba9d release: v0.0.9 lockstep — re-anchor to clean embedded artifacts
All checks were successful
Build + deploy releases / build-and-deploy (push) Successful in 7s
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.
2026-05-03 16:18:05 -05:00
a02a26d3c2 feat: form-data system v0 (sixth tool + zddc-server endpoints)
All checks were successful
Build + deploy releases / build-and-deploy (push) Successful in 8s
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>
2026-05-02 20:12:16 -05:00
c099676024 ci: connect verify step to caddy via container name + tag trigger
Runner now runs in a quadlet container on caddy-net, so 127.0.0.1
is the runner's own loopback. Reach the Caddy container by name
('caddy') with --connect-to keeping SNI/Host as the public hostname
so the right vhost matches.

Also adds the tag trigger: push of zddc-server-v[0-9]+.[0-9]+.[0-9]+
auto-cuts a stable release. The lockstep set pushes six tags at once;
filtering on zddc-server-v* gives exactly one workflow run per cut.
Re-cutting at the tagged commit is safe — _promote_stable in
shared/build-lib.sh is idempotent re: tag creation.
2026-05-02 11:35:20 -05:00
49fab7b5ba ci: workflow_dispatch for build + deploy releases
Forgejo Actions workflow that runs ./build alpha|beta|release [version]
followed by ./deploy --releases. Uses the host-mode runner so the
behavior is identical to manual cuts. Tag-trigger added later once
the dispatch path is exercised.
2026-05-02 10:44:53 -05:00
7570fb7494 refactor: separate website repo + deploy-host model
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>
2026-05-02 09:14:40 -05:00
76e1e78c55 chore: ./build is dev-only; ./build alpha is the explicit deploy
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>
2026-05-02 08:29:58 -05:00
6167e99f3a chore: simplify CLI to ./build / ./build beta / ./build release
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>
2026-05-02 08:11:10 -05:00
76820fa8dd chore: split website out into orphan branch + worktree
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>
2026-05-02 07:52:20 -05:00
d688e20dad feat(releases): channel options in version dropdown, drop chip pills
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>
2026-05-01 21:19:59 -05:00
8c2e65e4a2 fix(mdedit): two-line ZDDC tree display + dark-mode editor contrast
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>
2026-05-01 21:09:46 -05:00
6cc0d2ae27 feat(zddc-server): /.auth/admin forward_auth endpoint
A machine-only HTTP endpoint that returns 200 if the request's
X-Auth-Request-Email is in the root .zddc admins: list, 403 otherwise.
No body, no redirect — pure authorization decision intended to be
polled by an upstream proxy's forward_auth directive.

The motivating use case is gating /devshell/* (code-server) in the
dev-shell pod on root-admin status before the request ever reaches
code-server, which has no built-in ACL of its own. zddc-server's
own routes keep the existing .zddc cascade ACL and don't go through
this endpoint.

Reuses zddc.IsAdmin (one cached map lookup) so the check is cheap
enough to call on every request. Edits to /srv/.zddc propagate via
the existing fsnotify watcher's policy-cache invalidation.

Tests cover empty email, non-admin, admin, and the bootstrap state
where no root .zddc exists (deny everyone — the safe default).

Docs: zddc/README.md "Forward-auth target for upstream proxies"
section + AGENTS.md notes bullet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:08:39 -05:00
17b0a4dff9 release: v0.0.8 lockstep
First lockstep cut. All six tools (5 HTML + zddc-server) at the same
version. zddc-server jumped from 0.0.7 → 0.0.8; HTML tools jumped from
0.0.2 → 0.0.8 (the coordinated next-stable rule picks max-tag + 1, so
HTML tools skip ahead in lockstep with the binary).

Tags created:
  archive-v0.0.8
  transmittal-v0.0.8
  classifier-v0.0.8
  mdedit-v0.0.8
  landing-v0.0.8
  zddc-server-v0.0.8

All tags point at 9fce18c (the source-state commit).

Artifacts under website/releases/:
  - 5 per-version HTML tool files (immutable real bytes)
  - 4 zddc-server binaries × 5 cascade levels (per-version + 4 symlink
    levels) per platform = 20 binary entries per platform × 4 platforms
  - 6 channel/per-version stub HTML pages for zddc-server (matrix-cell
    download fan-out)
  - All HTML tool channel mirrors updated to track v0.0.8
  - Matrix index regenerated with v0.0.8 as current stable

channel-link verification: 30 link(s) ok
First time the verifier runs in non-bootstrap mode (zddc-server stable
chain anchored).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:12:48 -05:00
9fce18cd45 feat: lockstep release infra + cascade/.archive fixes + profile perf + page redesign
Four entangled change-sets from one session, committed together because
their file-level overlap (build.sh, docs, embedded/, watcher.go, …) makes
post-hoc separation noisy:

* fix(archive): nested-party + folder-type cascade
  transmittalIsUnderVisibleParty short-circuited on the first matched
  party segment, only checking the immediately-next segment for a
  folder-type marker. Paths like BM/sub/Issued/<txn> bypassed the Issued
  toggle entirely. Replaced with isUnderHiddenFolderType (full-path) +
  any-segment party match. Eight new Playwright cases pin the contract
  in tests/archive-cascade.spec.js.

* refactor(zddc-server): scope .archive index by project
  archive.Index now buckets by top-level segment
  (.ByProject[<project>].ByTracking[<tracking>]). Resolve and AllEntries
  take a project parameter; handler extracts it from contextPath's first
  segment. /.archive/ at root returns 404 — stable refs must be
  project-rooted. Within-project (tracking, rev) collisions emit a WARN
  with both paths. Cross-project tracking-number duplicates no longer
  collide.

* perf(zddc-server): lazy-load expensive bits of the profile page
  serveProfilePage now ships a minimal shell: Email, EmailHeader,
  IsSuperAdmin (root .zddc only). Visible projects + admin subtrees +
  editable scaffolds populate client-side via /.profile/access. Subtree-
  admin scaffolds live in <template id="tmpl-subtree-admin">; pure
  non-admins receive no live admin form. ScanZddcFiles now memoized,
  invalidated on .zddc events by the watcher and writer helpers.

* feat: lockstep release + redesigned releases page
  sh build.sh --release [version|alpha|beta] is the canonical lockstep
  cut: every tool (5 HTML + zddc-server) bumps to the same coordinated
  version. zddc-server binaries now committed under website/releases/
  with the same cascade chain as HTML tools (no more Codeberg release-
  asset publication). zddc/release.sh deprecated (kept as a guard);
  shared/publish-codeberg-release.sh removed.

  Releases page redesigned as an action-first install guide: hero +
  version dropdown that rewires every download link, channel chips for
  always-visible alpha/beta access (state-aware labels: "tracks stable"
  vs "active dev"), Path A (zddc-server with platform auto-detect from
  UA), Path B (5 standalone tool HTMLs), version-pinning empowerment
  narrative (drop-a-copy vs .zddc apps: cascade), channels explainer.

  Channel-link verifier asserts every <tool>_{stable,beta,alpha}.html
  resolves at the end of every build. Bootstrap-friendly: zddc-server
  artifact checks skip until the first lockstep cut anchors the chain.

Tests: 167 Playwright + all Go packages green.
Docs: CLAUDE.md, AGENTS.md, ARCHITECTURE.md, zddc/README.md updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 20:11:38 -05:00
4ede42010a feat(zddc-server): CLI flags, --version, CWD-default ZDDC_ROOT
Adds command-line flags to zddc-server alongside the existing env vars.
Each setting can be set via --<flag-name> or ZDDC_<NAME>; the flag wins
on conflict, the env var wins over the hard-coded default.

  --root          / ZDDC_ROOT          (now defaults to CWD if both unset)
  --addr          / ZDDC_ADDR          (:8443)
  --tls-cert      / ZDDC_TLS_CERT      ("none" / empty / path)
  --tls-key       / ZDDC_TLS_KEY
  --log-level     / ZDDC_LOG_LEVEL     (info)
  --index-path    / ZDDC_INDEX_PATH    (.archive)
  --email-header  / ZDDC_EMAIL_HEADER  (X-Auth-Request-Email)
  --cors-origin   / ZDDC_CORS_ORIGIN   (https://zddc.varasys.io; "" disables)
  --insecure-direct / ZDDC_INSECURE_DIRECT (false)
  --help          (prints flag list to stderr, exits 0)
  --version       (prints binary + embedded tool versions, exits 0)

So an operator can `cd /srv/zddc && zddc-server` with zero config — the
served root defaults to the current directory, and TLS defaults to a
self-signed cert. config.Load now takes []string (test-friendly: nil
skips flag parsing entirely; tests pass an empty slice for env-only
loads).

Adds a `version` package-level var in main.go injected at link time via
`-ldflags="-X main.version=..."`. The build.sh runs git describe against
zddc-server-v* tags; for in-flight commits between releases it produces
e.g. zddc-server-v0.0.7-19-gadb6904-dirty.

Adds an embedded versions manifest:
  - Each tool's compute_build_label (in shared/build-lib.sh) writes a
    sidecar <tool>.label to $BUILD_LABELS_DIR if that env var is set.
  - Top-level build.sh sets BUILD_LABELS_DIR before running each tool's
    build, then assembles zddc/internal/apps/embedded/versions.txt as
    one `<app>=<build label>` line per app.
  - apps.EmbeddedVersions() loads the manifest at runtime.
  - main.go logs a compact summary on every startup; --version dumps
    the full per-app label.

Removes the old cfg.BuildVersion field — the X-ZDDC-Source: embedded
header now uses the package-level main.version directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:43:31 -05:00
adb6904397 docs: rewrite for embedded + cascade install model
Updates every repo doc to reflect the simplified install model:

  - Local install is just a download from /releases/.
  - Server install is just running zddc-server (current-stable HTMLs
    embedded at compile time).
  - Customize via .zddc apps: cascade entries (channel/version/URL/path,
    with default + per-app composition); editor at /.profile/zddc/.

Removes references to the old install scripts, level-1/level-2 stubs,
admin UI at /.profile/apps, SHA-256 verification, TOFU writes, refresh
worker, and ZDDC_APPS_* env vars.

zddc/README.md: replaces "Landing Page and Tool Install" section with
"Apps: virtual tool HTMLs" — covers the folder-name availability rules,
the resolution chain (real-file override / cascade / embedded), spec
syntax cheat sheet, cache layout under <ZDDC_ROOT>/_app/, the ?v=
cache-only override, and the X-ZDDC-Source response header.

ARCHITECTURE.md: install-distribution-model section rewritten to
describe the embed-first / cascade-override model with one canonical
example.

AGENTS.md, CLAUDE.md: short-form summaries pointing at the same model.

README.md: install bullet rewritten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:25:57 -05:00
7b764956bd feat(zddc-server): apps section in .zddc editor
Extends the form-based .zddc editor at /.profile/zddc/edit?path=<dir>
with an Apps section between Admins and the Effective chain.

The section is a six-row table — default plus the five canonical apps —
with one text input per row. Each row's right column shows a server-
rendered "Resolves to" preview computed by walking the cascade through
this directory and applying default + per-app composition. The preview
displays the final URL, "embedded (build-time default)", or "local file:
<path>" so operators see exactly what will be served.

Help text covers the full spec syntax (channel/version/URL/path forms,
:channel shorthand, default key) plus the ?v= per-request override and
its cache-only security constraint.

Permission gating is unchanged: existing CanEditZddc() strict-ancestor
rule applies — subtree admins cannot edit the file that grants their
own authority. Field-level errors land inline next to the input, just
like the existing ACL/admins fields.

POST handler (internal/handler/zddchandler.go) accepts a new Apps map
in the JSON write request, validates via the existing zddc.ValidateFile
flow (which now enforces apps.<name> spec syntax), and writes
atomically through the unchanged zddc.WriteFile path.

Three new tests: round-trip apps including the default key, per-field
validation error returns, and editor renders the apps section with
existing .zddc values pre-filled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:25:42 -05:00
8b6a2dc3e3 feat(zddc-server): apps fetch+cache subsystem with cascade overrides
Adds internal/apps/ package serving the five tool HTMLs at virtual paths
based on the surrounding folder name convention:

  archive      every directory (multi-project, project, archive, vendor)
  classifier   any Incoming/Working/Staging directory and subtree
  mdedit       any Working directory and subtree
  transmittal  any Staging directory and subtree
  landing      only at deployment root

The current-stable build of every tool is //go:embed'd into the binary
at compile time — that's the default with zero config. Operators
override per-directory via .zddc apps: entries; closer-to-leaf wins.

Spec syntax (in any apps: value):

  stable / beta / alpha / :stable          channel
  v0.0.4 / v0.0 / v0 / :v0.0.4              version
  https://my-mirror/releases                URL prefix only
  https://my-mirror/releases:beta           URL prefix + channel
  https://my-fork/archive.html              terminal full URL
  ./local.html / /abs/path.html             terminal local path

The special apps.default key provides a baseline URL prefix and channel
inherited by any app not overridden per-name. Per-axis cascade: a deeper
.zddc can override the URL, the channel, or both.

Cascade walks root→leaf; default applies first at each level, then the
per-app entry. Terminal sources (paths and full .html URLs) short-circuit
composition; deeper non-terminal entries override parent terminals.

URL sources fetch once on first request and cache forever in
<ZDDC_ROOT>/_app/<host>/<path> — different upstreams with the same
filename stay distinct. No background refresh, no SHA-256 verification:
operators delete the cache file to force a refetch. Concurrent misses
for the same source dedupe via a 30-line hand-rolled singleflight.

Per-request override: any user can append ?v=<spec> to a tool URL
(e.g. ?v=beta, ?v=v0.0.4, ?v=:alpha, ?v=https://mirror/releases:beta)
to ask for a different build for one request. Security: ?v= serves
ONLY versions already in the cache (cache miss returns 404; path
sources are rejected outright with 400). Users cannot trigger
arbitrary upstream fetches via crafted URLs.

Failed URL fetches (network down, 5xx) fall back to embedded with a
one-time WARN log. The X-ZDDC-Source response header reports what
served: fetch:URL / cache:URL / path:/abs / embedded:<app>@<build>.

Wire-in (cmd/zddc-server/main.go): dispatch routes <dir>/<app>.html
through apps.MatchAppHTML + AppAvailableAt + apps.Server.Serve when
no real file exists. Direct URL access to /_app/... is blocked at
the dispatch layer — cached files must go through the apps resolver
so they get correct Content-Type and ACL gating.

Schema (internal/zddc/file.go): ZddcFile gains Apps map[string]string
for cascade overrides. Validator (internal/zddc/validate.go) accepts
the special "default" key alongside the five canonical app names and
all spec forms.

Removes ZDDC_APPS_* env vars (no admin UI, no refresh interval, no
upstream allow-list — the simpler model has fewer knobs).

40+ unit tests across the new package: parser shapes, cascade
resolution with default+per-app interactions, terminal short-circuit
semantics, ?v= cache-only enforcement, embedded fallback, atomic
cache writes, singleflight dedup. Plus end-to-end dispatch tests in
cmd/zddc-server/main_test.go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:25:25 -05:00
9b3b11fc20 refactor: drop install scripts; containerize zddc-server build
Drops the install/distribution machinery in favor of two simpler paths:
  - Local: download a tool .html from website/releases/ and open it.
  - Server: run zddc-server, which embeds current-stable HTMLs at compile
    time via //go:embed (delivered in the next commit).

Removed:
  - bootstrap/install.sh, bootstrap/README.md, bootstrap/level{1,2}.html.tmpl
  - website/bootstrap/{level1,track-stable,track-beta,track-alpha}/
  - website/install.sh symlink
  - The bootstrap-stub generation in top-level build.sh

Rewritten:
  - website/index.html "Install on your server" section: two cards
    (server / local) replacing the old four script-based snippets.
  - website/reference.html, website/css/style.css: updated to match.
  - website/releases/index.html: regenerated by build.sh from filesystem.

Top-level build.sh:
  - All five tool HTMLs now copied into zddc/dist/web/ and into the
    new zddc/internal/apps/embedded/ for //go:embed.
  - Cross-compile is always containerized via podman or docker against
    docker.io/golang:1.24-alpine (the same image the helm prod chart
    uses), with named volumes for module + build cache. ZDDC_GO_BUILD_IMAGE
    overrides for air-gapped or pinned-mirror deployments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:24:52 -05:00
d0929a2aa9 feat(landing): groups + click-to-open redesign
Replaces the caret-dropdown preset menu with two stacked cards:

  Groups (top)    — saved bundles of projects; click to open the archive
                    with that group's project set; per-row edit/delete
                    buttons.
  Projects        — filterable table; in default mode no checkboxes,
                    click any row to open just that project.

"+ New group" or a row's edit button enters select-mode: checkboxes
appear on each project row, an action bar shows above the projects card
with Save group / Open visible-checked / Cancel.

"Open visible-checked" intentionally excludes filter-hidden checked
projects so users can scope to a subset they're currently looking at.

Storage migrates from old zddc_landing_presets to zddc_landing_groups
(simpler shape: {name, projects: [...]}). One-shot migration runs on
first load.

Adds the new favicon SVG to the landing header alongside the title.
Drops the ?projects= URL state since selection is no longer the page's
primary state in click-to-open mode.

Updates Playwright suite: 9 new test cases covering click-to-open, group
crud, edit pre-population, "open selected visible" scoping, and legacy
preset migration. Adds a LandingApp._setNavigate test hook since
window.location.href cannot be reliably patched in modern engines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:23:42 -05:00
f01a177b73 feat(html): TIFF and ZIP listing previews + favicon in app headers
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>
2026-05-01 15:23:26 -05:00
fedc3650b5 fix(zddc-server): access log was always email=anonymous (middleware ordering)
The middleware chain in main.go was:

  AccessLogMiddleware ( CORSMiddleware ( ACLMiddleware ( dispatch ) ) )

ACLMiddleware extracts the user email from the configured header and
stores it in the request context via r.WithContext. But Go's context
propagates DOWN the chain (to handlers further in) — not back UP. The
new context-bearing request only exists inside the call to
next.ServeHTTP; once that returns, the outer middleware still has the
ORIGINAL request without the email. So AccessLogMiddleware's
EmailFromContext(r) call (which runs after next.ServeHTTP returns to
log the request) read from the original context and got an empty
string, falling through to "anonymous".

The /.profile/ page worked correctly because it reads the email
directly inside the handler — at that depth the context-bearing
request is the one in scope.

Fix: invert the chain so ACL is OUTERMOST.

  ACLMiddleware ( AccessLogMiddleware ( CORSMiddleware ( dispatch ) ) )

Now ACL extracts the email and the new request flows down through
AccessLog (which sees the email-bearing context), CORS, and dispatch.

Add three regression tests in middleware_test.go:

  TestAccessLogReadsEmailFromACLContext
    The fix: with ACL outer, AccessLog logs email=alice@example.com
    when X-Auth-Request-Email is set.

  TestAccessLogAnonymousWhenNoEmail
    The unchanged path: no header → email=anonymous (correct fallback).

  TestAccessLogOuterDoesNotSeeInnerContext
    Locks down Go's actual context-propagation behavior. Builds the
    INVERTED (buggy) chain and asserts that AccessLog (outer) does NOT
    see the email ACL (inner) set. If this ever fails, Go's context
    propagation has changed in a way that lets inner-set context flow
    upward — which would mean the reordering fix could be reverted.

All zddc-server tests pass via `go test ./...` (run in podman against
golang:1.24-alpine since this dev host doesn't have Go installed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:46:49 -05:00
7365e94cac docs: align with simplified release model
Updates to all six top-level docs to describe the new flow:

- Storage: HTML tools live in website/releases/ as committed static
  files. Per-version files are real bytes; partial-version pins and
  channel mirrors are checked-in symlinks. No manifest.json, no Codeberg
  indirection, no Caddy regex-rewrite.
- URL scheme: <tool>_v<X.Y.Z>.html (exact), <tool>_v<X.Y>.html (latest
  patch), <tool>_v<X>.html (latest minor), <tool>_<channel>.html
  (channel mirror). All resolve via the symlink chain.
- Cascade rule: stable cut → beta + alpha symlinks reset to stable;
  beta cut → alpha resets to beta. Channels are never stale.
- No -alpha.N / -beta.N counter tags. Channel URLs are stable URLs by
  design; counters defeat that. The on-page <date> · <sha> label is
  enough for traceability.
- bootstrap/install.sh is the canonical install path. The four hand-
  rolled snippets are gone; one script handles all three deployment
  patterns + both target shapes.
- Helm charts under helm/ (zddc-server-{prod,dev}/) build from source
  via init container; documented as the recommended k8s deployment
  path.
- zddc-server now publishes binaries on stable cuts only — no alpha/
  beta channel for binaries. Active dev runs through the dev helm chart
  which builds from source on each rollout.

Files updated:

- CLAUDE.md — Repo shape, Most-used commands, Things that bite if you
  forget. Drops mentions of manifest.json, the Codeberg-as-canonical
  model, and -alpha.N/-beta.N tags.
- AGENTS.md — website/ tree, Releasing — channels and layout, Channel
  discipline rules (renumbered to add coordinated minor/major bump
  rule), Freshen helper, Bootstrap stubs, zddc-server Release tagging.
- ARCHITECTURE.md — website/ tree, build.sh step 5, Channels section,
  level-2 bootstrap description.
- README.md — tool publishing description, link to helm/.
- bootstrap/README.md — install path is install.sh now; pin URL table
  uses static symlinks; CORS check uses release-asset URLs (not
  manifest.json).
- zddc/README.md — Quick Start uses Codeberg URLs directly (no proxy);
  Release tagging is stable-only; Distribution / Versioning sections
  rewritten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:56:34 -05:00
607121a9ea feat: example helm charts for zddc-server (production + dev)
Two charts under helm/, both compile zddc-server from source via an
init container — no container image registry, no pre-built binary.
The init container clones the repo at a configured git ref, runs
`go build`, and writes the binary into a shared emptyDir; the main
container is alpine + the freshly built static binary.

helm/zddc-server-prod/  Production-shaped:
                        - gitRef pinned to a stable tag in
                          values.yaml.example (zddc-server-v0.0.7).
                        - imagePullPolicy IfNotPresent.
                        - Slower probe cadence (30s liveness, 10s
                          readiness).
                        - ZDDC_LOG_LEVEL=info.
                        - replicaCount: 1 (operators raise as needed
                          when backed by a shared filesystem).

helm/zddc-server-dev/   Dev/soak-shaped:
                        - gitRef defaults to "main" (rebuilt every pod
                          restart). build-time annotation forces
                          recreate on every helm upgrade.
                        - imagePullPolicy Always on the build image
                          so the latest golang:1.24-alpine is pulled.
                        - Faster probe cadence (10s liveness, 5s
                          readiness) — fail-fast in dev.
                        - ZDDC_LOG_LEVEL=debug. NOTE: debug logs every
                          request's full header map (includes auth
                          tokens / cookies) — this chart is for
                          private dev namespaces only.
                        - Strategy: Recreate (single replica racing
                          on different SHAs would be a mess).

Both charts:

- Wire the ZDDC_* env-var contract (ZDDC_ROOT, ZDDC_ADDR,
  ZDDC_TLS_CERT=none, ZDDC_INSECURE_DIRECT=1, ZDDC_EMAIL_HEADER,
  ZDDC_CORS_ORIGIN, ZDDC_LOG_LEVEL, ZDDC_INDEX_PATH).
- Mount a caller-supplied PVC at ZDDC_ROOT (chart does not create the
  PVC; operators provision storage themselves).
- Optional Ingress (ingress.enabled: true). TLS is expected to be
  terminated upstream of the pod; the pod listens on plain HTTP.
- No secrets in values.yaml.example. ACL email lists go in .zddc files
  inside the data volume; image-pull and TLS secrets are referenced by
  name only.

helm/README.md documents the design rationale (why build from source
instead of using a registry image), a quick-start example, and the
explicit list of what the charts do and don't do.

Note: `helm lint` cannot be run in this dev environment (helm isn't
installed). YAML syntax of Chart.yaml and values.yaml.example
verified via `python3 -c "yaml.safe_load(...)"`. Operators should
run `helm lint` and `helm template` before installing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:48:02 -05:00
d37ff58e11 feat: unified bootstrap/install.sh replaces 4 hand-rolled install snippets
bootstrap/install.sh is a POSIX-sh script that handles all three ZDDC
deployment patterns plus both target shapes in a single command. The
homepage's "Install on your server" section now prints `sh -c
"$(curl -fsSL https://zddc.varasys.io/install.sh)"` invocations instead
of inlining four separate cURL loops.

Modes / channels / target:

  --mode copy   (default)  fetch actual HTML files locally; site is
                           self-contained after install.
  --mode track             install level-2 stubs that fetch the channel
                           from upstream on every page load. Only valid
                           with channel names (stable/beta/alpha); for
                           pinned versions, use --mode copy.

  --channel stable         (default)
  --channel beta | alpha   channel mirrors (auto-update upstream)
  --channel 0.0.2          exact version
  --channel 0.0            latest patch within 0.0.x (symlink-resolved)
  --channel 0              latest within 0.x (symlink-resolved)
  Optional leading 'v' is accepted.

  --target root            5 tool HTMLs (incl. landing as index.html)
                           plus _template/ directory of level-1 stubs
  --target project         4 level-1 stubs that fetch ../<tool>.html
  --target auto (default)  detect from CWD: 'project' if parent has a
                           ZDDC-looking index.html, 'root' otherwise

  --source URL             Override the upstream base URL (default:
                           https://zddc.varasys.io). Use this to point
                           at a private dev server during alpha work;
                           install.sh sed-rewrites the embedded source
                           inside fetched track-channel stubs so they
                           use your URL.

Also:

- website/index.html — replaces the four inlined install-card snippets
  with four use-case-oriented cards that all invoke the same script
  (self-contained / track-channel / pin-to-version / project-subdir).
- website/index.html — fixes the stale "pre-built image at codeberg.org/
  varasys/zddc-server (channel-tagged :stable, :beta, :alpha)" reference;
  zddc-server is now distributed as Codeberg release-asset binaries with
  helm chart examples in the repo.
- website/install.sh — symlink to ../bootstrap/install.sh so the upstream
  serves the script at https://zddc.varasys.io/install.sh.

Verified end-to-end against a file:// source: copy:root, copy:project,
track:root with custom --source (rewrite of base URL inside fetched
stub), and auto-detection of project vs root from CWD's parent
index.html all behave correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:44:13 -05:00
408a1a0571 refactor: HTML tools live in website/releases/ as static files + symlink hierarchy
Rolls back the HTML-tool side of the Codeberg-as-canonical refactor
(commits 2dc9ad2, b28c4ae, bdac8dc) in favor of a simpler model:
per-version HTML files committed under website/releases/ as immutable
real files; partial-version pins (<tool>_v<X.Y>.html, <tool>_v<X>.html)
and channel mirrors (<tool>_<channel>.html) are checked-in symlinks.
Docker-tag pattern: :1.2.3 is pinned, :1.2 floats, :1 floats further,
:stable floats furthest.

URL scheme — every URL resolves to actual HTML via the symlink chain;
no JS indirection, no manifest.json, no Caddy regex-rewrite:

  /releases/<tool>_v<X.Y.Z>.html    exact version (real file)
  /releases/<tool>_v<X.Y>.html      latest patch within X.Y.* (symlink)
  /releases/<tool>_v<X>.html        latest within X.*.* (symlink)
  /releases/<tool>_stable.html      current stable (symlink)
  /releases/<tool>_beta.html        current beta (symlink to stable when no
                                    active beta; real file when beta is in flight)
  /releases/<tool>_alpha.html       current alpha (similar — symlink to beta
                                    or stable when no active alpha)

Cascade rule (in shared/build-lib.sh promote_release):
  --release [version] (stable cut) → write per-version file; refresh 5
                                     symlinks (_v<X.Y>, _v<X>, _stable,
                                     _beta, _alpha) → new versioned file;
                                     tag <tool>-v<X.Y.Z>.
  --release beta                   → overwrite <tool>_beta.html with real
                                     bytes; cascade _alpha.html → _beta.html
                                     (symlink). No tag — channel URLs are
                                     stable URLs by design; counters defeat
                                     that.
  --release alpha                  → overwrite <tool>_alpha.html with real
                                     bytes. No tag, no other side-effects.
  Plain `sh tool/build.sh`         → dist/ only. No website/releases/
                                     side-effect, no commit.

Code changes:

- .gitignore — drop website/releases/*.html and website/releases/zddc-server-*
  exclusions; HTML tool files are tracked again. Replace the comment with
  the new model description.
- shared/build-lib.sh — drop next_prerelease (no -alpha.N / -beta.N counter
  tags). Drop the Codeberg-upload path for HTML tools (no longer sourcing
  publish-codeberg-release.sh from build-lib). promote_release rewritten
  with two helpers: _promote_stable (per-version file + 5 symlinks + tag)
  and _promote_channel (overwrite mirror + cascade alpha→beta on beta cut).
- zddc/release.sh — drop alpha/beta channel path entirely; binaries publish
  only on stable cuts. zddc-server's beta/alpha builds-from-source via the
  helm charts (next phase) — no binary distribution needed for those channels.
- bootstrap/level2.html.tmpl — drop manifest.json fetch; resolve ?v= to a
  static URL via the symlink chain. New suffixFor() handles channel names,
  exact versions, and partial-version pins (?v=0.0, ?v=0). Same logic in
  level1.html.tmpl already works because the local-staging files (e.g.
  ../<tool>_v0.0.html) exist via the same symlink scheme.
- build.sh build_releases_index — revert to filesystem scan of
  website/releases/ instead of Codeberg API call. Drop manifest.json
  generation. Per-tool sections list channel chips + per-version pin links;
  zddc-server section links to Codeberg release pages directly.
- tests/build-label.spec.js — fix the channel-label regex to match the
  pre-release-semver format introduced in commit 9459139 ("v0.0.3-alpha · ...").
  Pre-existing test failure that wasn't caught at the time.

Storage:

- 30 new committed files under website/releases/ — 10 real (per-version) +
  20 symlinks (5 tools × 4 partial/channel variants, plus alpha as a real
  file by default).
- Initial state: stable v0.0.2 across all 5 tools; alpha/beta/v0.0/v0
  symlinks all point at <tool>_v0.0.2.html.
- manifest.json deleted (no longer needed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 09:40:16 -05:00
bdac8dc4fb docs: clean up drift left over from the Codeberg release-assets refactor
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>
2026-04-30 08:01:20 -05:00
b28c4aef81 release: backfill HTML-tool stable releases on Codeberg + drop local mirrors
Completes the deferred cleanup from 2dc9ad2 ("refactor: distribute via
Codeberg release assets, drop the upstream image"), now that
$CODEBERG_TOKEN is provisioned.

Backfilled to Codeberg release assets:
- archive, transmittal, classifier, mdedit, landing — v0.0.1 and v0.0.2
  stable releases (10 releases, one HTML asset each)
- zddc-server v0.0.8-alpha.2 — four cross-compiled binaries (the same
  artifacts that previously lived in website/releases/, renamed to the
  canonical zddc-server-<os>-<arch>[.exe] without the -alpha suffix)

Older zddc-server stable tags (v0.0.1 through v0.0.7) are not
backfilled — they were image-only releases under the old flow and have
no binary artifacts to upload. Operators wanting those builds can
checkout the tag and run `sh zddc/release.sh stable <ver>` to publish
binaries; nothing in the new flow depends on them existing on Codeberg.

Removes from the working tree (now redundant — Codeberg is canonical):
- website/releases/<tool>_v0.0.{1,2}.html  (10 files)
- website/releases/<tool>_{alpha,beta}.html  (10 files; mutable channel
  mirrors written by the old build flow)
- website/releases/<tool>_stable.html  (5 symlinks)
- website/releases/zddc-server-*-alpha[.exe]  (4 binaries)

Also: regenerates website/releases/{index.html,manifest.json} from the
post-backfill Codeberg release list. The manifest currently has stable
entries for the five HTML tools and an alpha entry for zddc-server;
HTML alpha/beta channels and zddc-server stable will populate as
operators cut those releases through the new flow.

One repo-config side effect: enabled the "Releases" repo unit on
codeberg.org/VARASYS/ZDDC (was off; the API returned 404 on every
release endpoint until enabled).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 07:41:33 -05:00
2dc9ad240c refactor: distribute via Codeberg release assets, drop the upstream image
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>
2026-04-29 21:18:26 -05:00
6e1777e9c2 feat(web): list zddc-server binaries + image tags on releases page
The releases index already enumerated HTML tools by channel and
version. Add a parallel section for zddc-server with two artifact
families:

- Standalone binaries: a channel × platform table linking to
  website/releases/zddc-server-{linux,darwin,windows}-{amd64,arm64}-<channel>
  (the files release-image.sh now mirrors there). Empty cells render
  as em-dashes; the table only renders when at least one binary
  exists locally so freshly-cloned repos don't show a ghost section.
- Container images: pull-snippet, channel chips linking to the
  codeberg package page, and a "Pin to version" row of historical
  tags (clean stables first, recent pre-releases listed separately
  with a "recent pre-releases:" label so the pre-release noise
  doesn't drown out the stable list).

Recent pre-releases capped at 10 entries to keep the list scannable
as alpha cuts proliferate over time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 20:04:28 -05:00