Commit graph

10 commits

Author SHA1 Message Date
66232598db test: server-backed Playwright harness + /.tokens spec
Adds the first Playwright spec that drives a real running zddc-server
in Chromium. Future UI debugging (the conflict-UI in phase 5, browser-
side iteration on the master's HTML pages, etc.) reuses the same
harness — beforeAll spins up a master on a random port, the spec
talks to it, afterAll tears it down.

Files:

- tests/lib/server.js: CommonJS module exporting startMaster(opts).
  Builds the binary on first run via the canonical podman/zddc-go:1.24
  invocation from AGENTS.md, caching at zddc/dist/zddc-server-test
  with a sibling .hash file (SHA256 of cmd/+internal/+go.{mod,sum})
  that invalidates on source change. Subsequent runs skip the build.
  Set ZDDC_TEST_BIN=<path> to use a pre-built binary (CI / debugging).

  Seeds a minimal master root in os.tmpdir() with a permissive .zddc
  granting the test user (default alice@example.com) full access plus
  read for *@example.com. Picks a free port via net.listen(:0), spawns
  the binary on 127.0.0.1:<port>, polls until listening (max 10s).
  Returns { baseURL, root, proc, logs(), stop() }.

  CommonJS (require/module.exports) rather than ESM because Playwright's
  loader transforms top-level `import` in *.spec.js files but not in
  the .js helpers we ship alongside; mixing produces "exports is not
  defined in ES module scope" at the helper's first line. Spec files
  use `import { ... } from './lib/server.js'` and the import resolves
  through the CJS interop layer cleanly.

- tests/tokens.spec.js: 8 server-backed scenarios covering the entire
  /.tokens contract:

  1. Anonymous → 401 on /.tokens (X-Auth-Request-Email empty).
  2. Authenticated GET /.tokens renders the page with the user's email
     visible in the .who line and the create form + tokens table both
     present and populated.
  3. GET /.api/tokens returns an empty list initially.
  4. Create-via-page round-trip: fill the form, click submit, plaintext
     appears once in #created .token-secret (hidden from later reads),
     row appears in the table, API list confirms the description, the
     row's Revoke button removes it from both the table and the API.
  5. Plaintext token authenticates a subsequent Bearer request even
     when X-Auth-Request-Email is empty — confirms the middleware
     bridge from Bearer to ACL email.
  6. Invalid Bearer → 401 (no silent fallback to anonymous).
  7. Cross-user revoke returns 404 (not 403) — the ownership-non-leak
     guarantee.
  8. XSS guard: description with <img src=x onerror="window.__xss=1">
     should render as text (assert window.__xss !== 1) — the inline
     JS's escapeHTML is the only thing standing between an attacker
     who could create tokens and stored XSS on the management page.

  test.use({ extraHTTPHeaders }) injects X-Auth-Request-Email on every
  request from the Playwright browser context, mimicking what an
  upstream auth proxy adds in production. Per-test overrides clear it
  to test anonymous paths.

- playwright.config.js: adds the `tokens` project. Bumps the global
  timeout from 30s → 60s so the first run's binary-build (~30s on a
  cold gocache) doesn't time out the suite. The tokens project
  testMatches only tokens.spec.js, so other projects (the file://-
  driven tool tests) are unaffected.

Verified: all 8 tests pass (12.5s warm; ~45s cold including the build).
The harness is ready to graft additional server-backed specs onto —
phase 5's conflict-UI in particular will follow the same pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:09:54 -05:00
e19667b5a2 test: add tests/data/test-archive.sh — synthetic ZDDC fixture builder
Generates a realistic ZDDC archive layout for end-to-end testing of
master + cache + mirror, with zero identifying data and a script
that creates and clears on demand.

Output: ~/zddc-test-data (default; override via TEST_ARCHIVE_DIR),
intentionally OUTSIDE the repo. Defensive .gitignore entries cover
in-repo redirects and the source-reference CSV (~/archive-export*.csv,
which the script never reads at runtime — distributions are baked in
here as constants extracted from a one-time inspection).

Layout mirrors a real archive's shape (project → Archive → party →
Received|Issued → dated transmittal folder → tracking-numbered file)
with synthetic codes throughout — Project-1/2/3, PartyA/B/C, FAC1-4,
lorem-ipsum titles, example.com emails. Disciplines, doc-type codes,
status codes (IFR/IFI/IFA/IFU/RSB), revision letters (A/B/0/0A/0B/C/D),
and tracking-number format are kept as-is — they're public ZDDC
convention vocabularies, not identifying data.

Each file's content is the metadata block:
  Tracking Number: <synthetic>
  Revision: <letter>
  Status: <code>
  Title: <lorem-ipsum>
rendered into the appropriate format per extension. Open any file and
verify it's the right one — md as a table, yaml as keys, html as a
styled table, .zddc as YAML, .zip with three views (md+yaml+html), pdf
rendered via docker.io/pandoc/latex (already-existing 563MB image)
through podman with --userns=keep-id so output is host-user-owned.
Falls back to a hand-rolled minimal valid PDF (Python stdlib only)
when podman or the pandoc image is unavailable.

Subcommands:
  build [--small]   Generate the fixture. --small produces ~12 files,
                    full produces ~550 with every one of the six
                    extensions (md/yaml/pdf/html/zddc/zip) guaranteed
                    in every transmittal.
  clear             rm -rf the fixture. Refuses unless target contains
                    a .zddc — defense against an accidental misconfigured
                    TEST_ARCHIVE_DIR pointing at something important.
  info              File count, total size, by-extension breakdown,
                    top-level layout. No content snippets.

POSIX sh (dash-compatible). Randomness via /dev/urandom (no $RANDOM;
dash doesn't expose it). Per-directory .zddc ACL configs use synthetic
emails from RFC-2606 example.com.

Verified: full fixture builds in ~3min (PDF generation dominates),
contains 144 PDFs all valid 1-page, no real-archive tokens leak
(grep -i for known sentinels from the source CSV returns zero hits).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:03:38 -05:00
9ca36f25d8 feat(tables): new sortable/filterable grid tool for directories of YAML files
Tables is the eighth HTML tool: a read-only tabular view over a
directory of YAML files declared via `tables:` in `.zddc`. Anchor use
case is the Master Deliverables List, where each row is one
`<tracking>.yaml` under `Archive/<Party>/MDL/`. Rows click through to
the existing form renderer for editing.

Schema (zddc/internal/zddc/file.go)
  - New `Tables map[string]string` on ZddcFile. Map key becomes the URL
    stem (`tables[MDL]` → `<dir>/MDL.table.html`); the value is a path
    relative to the .zddc pointing at a `*.table.yaml` spec describing
    columns + the rows directory. No upward cascade in v1 — each
    directory hosting a table declares it directly.

Server handler (zddc/internal/handler/tablehandler.go)
  - `RecognizeTableRequest` matches GET `/<dir>/<name>.table.html`
    against the cascade's `tables:` declarations. Dispatch routes
    table requests before the form-system intercept.
  - `ServeTable` ACL-gates with `policy.ActionRead` and serves the
    embedded `tables.html` template; client walks the directory itself
    via the listing JSON or FS Access API.
  - tables.html embedded via //go:embed — same pattern as form.html.

Frontend (tables/)
  - Vanilla JS: app/context/util/filters/sort/render/main modules.
  - Reads spec + row YAML files via window.zddc.source (HTTP polyfill
    or local FS handle); js-yaml 4.1.0 vendored in shared/vendor for
    client-side parsing.
  - Sample fixtures under tables/sample/ for local testing.

Build + CI
  - Lockstep build registers tables alongside the other 7 tools (HTML
    output, embed mirror, versions.txt, release-output, tags).
  - Playwright project added; `npx playwright test --project=tables`
    is part of `npm test`.

Drive-by: rename mdedit Playwright selectors `#select-directory` →
`#addDirectoryBtn` to fix three pre-existing failing tests.

Drive-by: ignore locally-built `zddc/zddc-server` binary so it doesn't
get accidentally staged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:32:01 -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
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
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
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
c95f07966d feat(tools,build): in-flight HTML-tool reworks and build-infra updates
Bundles a stretch of in-progress work across the SPA tools so the
tree returns to a coherent shippable state ahead of cutting a new
zddc-server stable image:

- landing: substantial rework of the project picker (sortable/filterable
  table, presets refactor, ?projects= filter, ?v= channel propagation,
  loading/error states)
- archive: presets cleanup, source.js refactor, filtering/url-state
  alignment with the landing page
- mdedit: file-system module split, resizer, file-tree improvements,
  base/toc styling tweaks
- transmittal/classifier: small template touch-ups for shared chrome
- shared: build-lib.sh helpers, new favicon.svg
- bootstrap, build.sh: pick up the channel-aware install/track zip
  generation
- tests: new landing.spec.js, expanded archive/mdedit/build-label specs
- docs: CLAUDE.md picks up the zddc-server section and freshens the
  alpha-build exception note
- regenerated artifacts: install.zip, track-{alpha,beta,stable}.zip,
  *_alpha.html — these are produced by `sh build.sh` and per project
  convention are committed alongside the source changes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:52:27 -05:00
f8a3da2ea1 feat(archive,landing): local-mode ?projects= filter + ?v= propagation
Two small additions to the project-filter / channel-selector flow that
already worked end-to-end for HTTP-mode but were missing in the local
File-System-Access path and across landing→archive navigation:

* archive: scanLocalRecursive now applies window.app.projectFilter at
  depth 0, mirroring the HTTP source's existing filter at source.js:316.
  Loading archive.html?projects=A,B in local mode (file://) now virtually
  merges A and B into one combined view, same as HTTP mode does today.

* landing: openArchive() reads ?v= from its own URL and passes it through
  to the archive.html link it generates. This keeps the user on the same
  channel (alpha/beta/stable/<version>) when they cross from the project
  picker to the archive — without it, alpha-channel users would silently
  drop back to whatever the deployment-default channel is at the
  archive.html boundary.

Test exercises the local-mode filter via the existing mock-fs-api
fixture: three top-level projects, projectFilter set to {A, B}, scan
produces only A's and B's files. (The url-state.restore() URL parsing
path is well-trodden in the HTTP case — the test sets projectFilter
directly to isolate the new source.js change from a pre-existing init()
fragility in the mock environment.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 17:24:07 -05:00
ea385b5366 Initial commit
ZDDC — Zero Day Document Control. A file-naming convention plus five
single-file HTML tools (archive, transmittal, classifier, mdedit,
landing) and an optional Go HTTP server (zddc-server) with ACL and a
virtual archive index. Self-contained, offline-capable, dependency-free.

See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the
build/release/architecture detail, bootstrap/README.md for the
two-level deployment install pattern, and zddc/README.md for the
HTTP server.
2026-04-27 11:05:47 -05:00