Commit graph

7 commits

Author SHA1 Message Date
ef849ab3fa feat(shared): replace floating elevation toggle with a header profile menu
Drop the bottom-right floating "Admin mode" switch in favour of a proper
account menu in the header's upper-right (every tool's .header-right).

New shared/profile-menu.{js,css}: a circular avatar button (email initial)
opening a dropdown with the signed-in email, an "Admin mode" item (only for
can_elevate principals — drives elevation.setOn/setOff, drops on leave),
Profile (/.profile), and Access tokens (/.tokens). The panel is portaled to
<body> + position:fixed so it overlays content reliably regardless of the
app's stacking contexts; the button shows a red ring while elevated.

No logout: authentication is the upstream proxy's concern (oauth2-proxy /
Authelia) — ZDDC owns no session, so the menu doesn't render sign-out.

elevation.js keeps the state machine (cookie, armed banner/frame, ephemeral
pagehide-clear, zddc:elevationchange, ?admin= URL) but no longer renders any
control — the profile menu is the UI. elevation.css drops the floating-
toggle styles (keeps banner + frame). All 7 templates drop the dead
elevation-toggle placeholder; all 7 build.sh bundle profile-menu.{js,css}.

Validated in a containerized browser: menu items, links, elevation arming +
armed ring, dropdown overlays content, no floating toggle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 19:43:43 -05:00
1721b4b1db feat(tables): explicit Save button + clearer dirty-row marker
Three triggers for flushing pending edits:
  - Save button in the toolbar — shown only when ≥1 row is dirty,
    label reads "Save (N unsaved)". Disappears after a clean settle.
  - Ctrl+S (Cmd+S) anywhere on the page, capturing-phase so it beats
    the browser's "Save Page As" default.
  - focusout of #table-root with a relatedTarget outside the grid —
    catches "edit cell, click a header link, expect it to save".

The row-blur trigger stays — moving between rows still flushes. The
new triggers fill the gap when the user edits one row and then leaves
the grid entirely without first navigating to another row.

Dirty marker gets a 4px (was 3px) left swatch AND a faint blue
background tint on the row, so "unsaved" reads as a row state rather
than a small marker on the edge.

editor.setDraft / clearDraftField notify save.onDraftsChanged,
which refreshes the Save button + reapplies the dirty class.
saveRow on 200/201/202 also refreshes the button so it disappears
the moment its row settles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:38:35 -05:00
847e082e6e feat(tables): Export CSV button in the table toolbar
Client-side download of the current view — filter + sort + column
order match what's on screen, values pass through util.formatCell so
dates / numbers / booleans render the same way they do in cells. RFC
4180 quoting; UTF-8 BOM so Excel detects encoding without an import
wizard. Sits next to "+ Add row" and shows for every table that
loaded with columns (no HTTP gate — the data is already in the
client), so MDL, RSK, SSR, and both project-level rollups all get
the affordance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 07:00:23 -05:00
050902fa9e chore: elevation slot in every tool + docs + helper file splits + smell cleanup
Polish pass after the big refactor in 2d114fc.

== Header elevation slot propagated ==

shared/elevation.{js,css} surface a header checkbox for admins.
30-minute sudo-style cookie window (Max-Age=1800, SameSite=Lax).
Only renders when /.profile/access reports can_elevate=true; quiet
for non-admins. Slot added to all 7 tool templates and concat'd
into all 7 build.sh files; admin in any tool now sees the toggle.

Three text-rename ride-alongs in archive/classifier/transmittal
templates: "Add Local Directory" → "Use Local Directory" (the same
rename that landed in browse earlier in this branch).

== Docs ==

- CLAUDE.md gets an "Admin elevation is sudo-style" paragraph in
  the "Things that bite if you forget" section.
- AGENTS.md gets a dedicated "Admin elevation (sudo-style)" section
  alongside "Bearer tokens" — same depth as the existing auth docs.

== Helper file splits ==

The retired form editor's shared helpers got bundled into a single
zddc_admin.go in the cleanup; that name is now misleading. Split by
concern:

- admin_helpers.go: hasAnyAdminScope (the only admin-specific helper)
- paths.go: resolvePath, urlPathOf, chainDirs (URL ↔ filesystem path
  math — used by several profile / zddc-file handlers)
- profile_assets.go (renamed from zddc_admin_assets.go): custom CSS
  pipeline. URL renamed from /.profile/zddc/assets/ → /.profile/assets/
  since /.profile/zddc/ no longer hosts an editor.
- treeEntry moves to profilehandler.go (alongside AccessView, its
  only consumer).
- writeError moves to profileprojects.go (its only consumer).

== Smell cleanup ==

- zddc.HasAnyAdminGrant(fsRoot, email) — new elevation-independent
  primitive that walks the cascade and reports whether email is named
  in any admin: list anywhere. Replaces the synthetic-elevated probe
  hack in enumerateAccess (`Principal{Email, Elevated: true}` was
  "lying" to the elevation gate to ask what it would say). The handler's
  hasAnyAdminScope collapses to a 4-line wrapper that gates on
  p.Elevated and delegates.
- Access-log middleware records `elevated` per request, so forensics
  can distinguish "admin acting as user" from "admin exercising power."
- browse/js/app.js's ?file= deep link walks multi-segment paths. Each
  intermediate segment is matched + expanded; the leaf gets
  selected/previewed. Auto-shows hidden when any segment starts with
  . or _. Silently no-ops on unresolved segments.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 12:15:41 -05:00
3a4a1c7f39 feat(mdl): default columns mirror tracking-number components + customizable
Per the reference doc at zddc.varasys.io/reference.html#tracking-numbers,
a tracking number is composed of: originator, [phase], project,
[area], discipline, type, sequence, [suffix]. The default Master
Deliverables List now surfaces every component as its own column,
plus the standard MDL metadata (title, plannedRevision,
plannedDate, status, owner). Columns appear in the canonical
filename order so the table reads left-to-right like the tracking
number itself.

Optional components ([phase], [area], [suffix]) render in the
table even when blank — keeps the layout consistent across rows.
Projects on a schema that doesn't use them hide the columns by
overriding (see customization).

Form schema (default-mdl.form.yaml):

- One JSON Schema property per tracking-number component, plus
  the deliverable metadata. originator / project / discipline /
  type / sequence are required; phase / area / suffix are
  optional. The schema is intentionally permissive — free-text
  strings on every component, no enums or regex constraints.
  Projects pick their own conventions for originator codes,
  discipline vocabularies, etc.; a default that imposed a
  fixed set would just get in the way.

- Phase 2's editable-cell widget factory derives the right
  per-cell editor from this schema: text inputs for the
  components, the existing select for `status` (which keeps
  its enum), date input for `plannedDate`, textarea for
  `notes`.

Customization (the "way for end users to customize"):

- Drop your own table.yaml and / or form.yaml into the rows
  directory (archive/<party>/mdl/, or any directory hosting a
  table). Operator-supplied files override the embedded defaults
  ATOMICALLY — there's no field-level merge, the operator file
  wins entirely. This matches every other "spec on disk wins"
  convention in zddc-server.

- Hide a column: omit it from the columns: list.
- Rename a column header: change `title:`.
- Add a column: append a {field, title} entry AND add a
  matching property in form.yaml's schema.properties.
- Tighten constraints: use `enum:`, `pattern:`, `minLength:`
  etc. on form.yaml properties.
- Pre-filter rows on load: defaults.filter[<field>].

The whole rows-directory is self-contained — copying mdl/ to a
new project takes the spec, the form, and every row YAML
together.

Documentation:

- AGENTS.md "Tables system" gains a paragraph on the default-MDL
  column set + the customization mechanism + a pointer to the
  embedded source files.

- tables/template.html help panel rewrites the body to cover:
    * What the directory IS (spec + form + row YAMLs together).
    * Editable-cell keyboard shortcuts (the Phase 1-5 sequence
      we just shipped — arrows, Tab, Enter, F2, Delete, Ctrl+D /
      R / C / V / Z, Shift+arrow / Shift+click for ranges).
    * The auto-save model + per-row state swatch colors.
    * The customization model with a worked file-tree example.
  Replaces the obsolete pre-Phase-1 wording that referenced
  `*.table.yaml` parent files and click-to-navigate-row UX.

Tests: no schema test changes — the default YAMLs are loaded
through the same RecognizeTableRequest / RecognizeFormRequest
paths that already cover the fallback. Full Playwright + Go
suites green (44 + 13).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:09:31 -05:00
e6d9966593 refactor(tables): in-dir convention + unified table+form HTML bundle
Two intertwined refactors that share too many files to split cleanly.
Both are described separately below.

PART 1 — in-dir convention for table+form spec files

Old layout had the spec at the parent and rows in a child:

    archive/<party>/
      mdl.table.yaml         spec
      mdl.form.yaml          row-edit form
      mdl/                   rows-dir
        row-001.yaml ...

URLs were /<dir>/mdl.table.html and /<dir>/mdl.form.html. Copying
mdl/ elsewhere lost the spec and form because they lived next door.

New layout collapses everything into the rows-dir:

    archive/<party>/mdl/      self-contained
      table.yaml              spec
      form.yaml               row-edit form
      row-001.yaml ...        rows

URLs become /<dir>/mdl/table.html and /<dir>/mdl/form.html. The
"copying-the-folder-takes-everything" property the user asked for
falls out by construction; the row-edit URL /<dir>/<id>.yaml.html
keeps the same shape (spec is now in the same dir, not the
grandparent).

Server changes:

- internal/handler/tablehandler.go RecognizeTableRequest fires on
  /<dir>/table.html when <dir>/table.yaml exists. The .zddc.tables
  alias map is gone — pure presence-based discovery now matches
  the form system's existing convention. Default-MDL fallback at
  archive/<party>/mdl/ stays for the virgin-archive case (the
  rows-dir need not exist on disk; the URL renders fully virtually).

- internal/handler/formhandler.go RecognizeFormRequest fires on
  /<dir>/form.html and /<dir>/<id>.yaml.html with spec at
  <dir>/form.yaml. specEligible accepts on-disk files OR the
  default-MDL virtual path so an empty mdl/ dir still surfaces the
  add-row form.

- internal/handler/tablehandler.go IsDefaultMdlSpec moves to
  serving archive/<party>/mdl/{table,form}.yaml (5 segments after
  ZDDC_ROOT). New isAtArchivePartyMdlLevel predicate; new
  isAtArchivePartyMdlDir for directory-based recognition. New
  IsDefaultMdlSpecAbs accessor for callers that hold an abs path
  rather than a URL (formhandler).

- internal/handler/formhandler.go loadFormSpec(fsRoot, path) falls
  back to embedded default-MDL bytes when os.ReadFile returns
  NotExist AND the path matches the archive-party-mdl shape. Three
  call sites updated to pass cfg.Root.

- internal/handler/formhandler.go serveFormCreate writes
  submissions to filepath.Dir(req.SpecPath) — the spec, the form,
  and rows all live in one directory. The submissionsDir creation
  is idempotent (MkdirAll); cascade falls back one level for ACL
  evaluation when the dir hasn't been materialized yet.

- internal/handler/tablehandler.go tableRowsRedirect now points at
  /<dir>/table.html (was /<dir>.table.html) when the directory
  request maps to a recognized table.

- cmd/zddc-server/main.go dispatch synth flips from
  urlPath + ".table.html" to urlPath + "/table.html" for the
  no-trailing-slash → tables-app routing.

- internal/apps/availability.go DefaultAppAt comment clarified
  that the dir at archive/<party>/mdl/ IS the table (not a child).

Client changes:

- tables/js/context.js walkServer fetches <currentdir>/table.yaml
  directly — no .zddc walk for table declarations. Rows are every
  *.yaml in current dir EXCLUDING table.yaml and form.yaml. The
  .zddc fetch-for-aliases is gated on file:// (online mode 404s
  on .zddc reads via the dispatcher's reserve guard, so skipping
  the request avoids browser console noise).

- tables/js/main.js add-row button links to relative form.html
  (same dir).

- tables/js/render.js + filters.js: every column's autofilter is
  uniformly a text-contains input, even enum columns — keeps the
  filter row visually consistent and doesn't constrain users to
  the enum vocabulary.

PART 2 — unified table+form HTML bundle

The form-render and table-render code paths share field schemas,
the cell editor for excel-mode IS a form widget, and the form
system's POST-back / validation already exists. Combining the two
HTMLs eliminates duplicating jsyaml/jsonschema/theme/source-
detection/.zddc-parsing across two single-file tools.

- tables/template.html grows two top-level mode containers:
  #table-mode (toolbar + sortable table) and #form-mode (form +
  submit button). Both hidden at parse time; the dispatcher
  unhides one. The shared #form-context placeholder was added
  here so the server's existing injectFormContext target
  resolves.

- tables/js/mode.js (new) sets window.zddcMode synchronously
  based on URL pattern: /form.html or /<id>.yaml.html → form,
  /table.html → table, else inline-context fallback for
  file:// (whichever context blob is non-empty wins). Unhides
  the matching container at DOMContentLoaded.

- tables/js/main.js init() and form/js/main.js boot() each guard
  early when mode isn't theirs. Both apps live on different
  globals (window.tablesApp vs window.formApp) so module
  registration doesn't collide.

- form/js/main.js title write falls back from #form-title to
  #table-title (the unified bundle's shared header element)
  when the dedicated id isn't present.

- tables/build.sh concatenates form modules (widgets, render,
  object, array, errors, post, serialize, util) and form CSS.
  No new external deps. Bundle grows from ~95KB to ~120KB.

- internal/handler/formhandler.go drops the //go:embed form.html
  directive; serveFormRender now writes embeddedTablesHTML via
  a small formRenderHTML() accessor (var declared in
  tablehandler.go, same package). The embedded form.html file
  is removed.

- build script: cp form/dist/form.html → internal/handler/form.html
  step is gone (file no longer exists in the source tree). cp
  tables/dist/tables.html → internal/handler/tables.html now
  runs unconditionally rather than only on beta/stable cuts —
  the renderer is a fixed binary component and dev iteration
  needs the embedded copy refreshed every build. Channel-cascaded
  apps (internal/apps/embedded/) stay channel-gated as before.

- form/dist/form.html still builds for standalone offline-only
  use (downloadable from /releases/), but no longer goes into
  the binary.

Tests:

- internal/handler/tablehandler_test.go and formhandler_test.go
  rewritten for the in-dir layout. New test
  TestRecognizeFormRequest_DefaultMdlAtArchiveParty covers
  empty-form, create POST, re-edit row, and the negative cases
  (Working/, non-mdl name) where the fallback must NOT fire.

- internal/handler/directory_test.go updated for the new
  /<dir>/table.html redirect target.

- cmd/zddc-server/main_test.go TestDispatchSlashRouting Location
  expectation updated.

- tests/form-safety.spec.js loads tables/dist/tables.html
  (named form.html in the temp dir to trigger form-mode in the
  dispatcher) so it tests the same bytes the server returns.
  Title-element selector switches to #table-title.

- tests/tables.spec.js updates the status-filter test for the
  uniform text-input filter.

Docs:

- AGENTS.md form-data system rewrites the URL conventions and
  storage layout for in-dir; gains a Tables system section
  parallel to forms describing the self-contained-directory
  property; subfolder rules ("one table per folder by
  construction; subfolders allowed and silently ignored as rows
  — legitimate uses: nested sub-tables, per-row attachments,
  drafts, future history sidecars") so we don't re-derive this.

Not included (deferred):

- ACL gating on cell-level writes — not relevant until Phase 3.
- Editable cells UI — separate commit (Phase 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:15:26 -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