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>
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>
Three tools (archive, browse, classifier) independently implemented
an empty-state pattern with three different CSS class naming
conventions and slightly different rules:
archive: .empty-state + .empty-state-content (BEM-less)
browse: .empty-state + .empty-state__inner (BEM)
classifier: .empty-state + .empty-state-content (BEM-less)
Same visual intent ("nothing's loaded yet — here's a welcome card
with instructions"), implemented three times with subtly different
spacing, no shared body styling for h2/p/ul/li, and incompatible
class names that prevented a future tool from copy-pasting the
pattern.
Promote a single consolidated rule set to shared/base.css using
BEM naming throughout:
.empty-state — base (flex centered, padding)
.empty-state--overlay — modifier: position absolute,
top 50px to clear app-header,
z-index 10. Used by archive
and classifier (their empty
states sit OVER the main
layout).
.empty-state__inner — content card (left-aligned,
text-muted, max-width 640)
.empty-state__inner--centered — modifier: tighter max-width
500, centered text, 2rem
padding. Used by tools whose
welcome screen reads as a
centered card.
.empty-state__inner h2/p/ul/ol/li — typography defaults
.empty-state__inner .note — italic small-print
.welcome-list — bullet list with left-aligned
text + auto margins; safe to
nest inside a centered card.
Per-tool changes:
- archive/template.html, archive/js/app.js: rename
.empty-state-content → .empty-state__inner empty-state__inner--centered;
add .empty-state--overlay to the outer .empty-state container.
Also the runtime-injected unsupported-browser markup in
showUnsupportedBrowserMessage() and the showHttpErrorState
selector.
- classifier/template.html: same renames.
- archive/css/layout.css + components.css: delete .empty-state*
and .welcome-list rules (now in shared).
- classifier/css/layout.css: same. Keep .empty-state.drag-over
locally — classifier is the only tool whose empty state acts
as a drop target.
- browse/css/base.css: delete .empty-state* (shared covers it).
browse's template was already using .empty-state__inner so no
template change needed.
LOC: shared/base.css gains ~70 lines; per-tool overrides lose ~85
combined. Net -15. More importantly, future tools can reuse the
pattern by adding two divs and (optionally) the --centered or
--overlay modifiers; no copy-paste required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six inline style="" attributes in archive, browse, mdedit templates
moved into stylesheets:
- The font-size:1.1rem override on #refreshHeaderBtn (three tools) is
now a single rule in shared/base.css — the refresh ⟳ glyph genuinely
reads smaller than ◐ / ?, so the rule lives next to the existing
shared icon-button block.
- The flex-start justify-content + select-all margin on archive's
Revisions column header become a .th-content--start variant +
.select-all-checkbox class in archive/css/table.css.
- The 450px initial width + 200px min-width on mdedit's #file-nav move
into mdedit/css/base.css; the runtime resizer continues to override
via inline style.width when the user drags.
No visual change — the output is exactly equivalent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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.