Retire the bespoke, chrome-less /.tokens page. It now renders through the
shared tables engine — getting the standard header (logo, theme, profile
menu) + declarative columns/filters for free — from a server-injected,
pre-assembled #table-context built from the user's tokens (Store.List).
New, reusable "tables over an API collection" primitive (tables/js/
api-actions.js): when the injected context carries an `apiActions` block,
it drives create (a modal form → POST, surfacing the one-time secret) and
per-row delete (→ DELETE) against a REST endpoint, and hides the file-model
toolbar affordances (+ Add row / Save). It deliberately does NOT touch the
file-save/row-ops machinery (ETag/conflict/row-file writes), so the secrets
surface stays on the existing tested /.api/tokens endpoints.
Server: handler.injectTableContextObj injects an arbitrary pre-assembled
context; EmbeddedTablesHTML() exposes the renderer to sibling handlers;
ServeTokensPage builds the token context (+ apiActions for /.api/tokens)
and serves the tables HTML, falling back to the legacy skeleton only when
the store or the tables renderer is unavailable.
This is the first dynamic/virtual-record collection rendered by the same
declarative engine + chrome as on-disk tables — no bespoke page. Validated
end-to-end in a containerized browser (list + create→secret + revoke);
tests/tokens.spec.js updated to the new UI; full Go suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>