diff --git a/shared/elevation.js b/shared/elevation.js
index f53c1b9..326c3d1 100644
--- a/shared/elevation.js
+++ b/shared/elevation.js
@@ -1,20 +1,23 @@
-// shared/elevation.js — admin elevation toggle.
+// shared/elevation.js — admin elevation via URL toggle.
//
-// Sudo-style model: admins behave as normal users by default; clicking
-// the header toggle elevates the session so admin escape hatches (WORM
-// bypass, .zddc edit authority, profile admin scaffolds) start firing.
-// State is carried in a `zddc-elevate=1` cookie that the server reads
-// via handler.ACLMiddleware → zddc.Principal{Elevated}.
+// Sudo-style model: admins behave as normal users by default; elevating
+// the session turns on admin escape hatches (WORM bypass, .zddc edit
+// authority, profile admin scaffolds). State is carried in a
+// `zddc-elevate=1` cookie that the server reads via handler.ACLMiddleware
+// → zddc.Principal{Elevated}.
//
-// Only renders the toggle when /.profile/access reports the caller has
-// some admin scope — a non-admin sees nothing, which keeps the chrome
-// quiet for the common case. The toggle fades in once access loads so
-// non-admins never even see the affordance flash.
+// Toggle is by URL query param — `?admin=true` to arm, `?admin=false`
+// (or the red banner's "Drop admin" button) to drop — so it's reachable
+// from ANY zddc-server page, not just ones that render a header control.
+// The cookie is the sticky state: it persists across navigation for its
+// Max-Age window, so the param need not stay in the URL (we strip it).
+// Arming is gated on /.profile/access `can_elevate`, so only real admins
+// can set it; a non-admin's ?admin=true is a silent no-op.
//
-// Click flow: set/clear the cookie, then reload the page so the server
-// sees the new state on the next render. The reload is intentional —
-// admin scaffolds in tool HTML are server-rendered for some tools, so
-// a soft state flip on the client alone wouldn't reach those.
+// Applying the cookie reloads to the cleaned URL so the server re-renders
+// under the new state (admin scaffolds in some tool HTML are server-
+// rendered, so a client-only flip wouldn't reach them). The red viewport
+// border + banner (applyArmedChrome) reflect the cookie on every load.
(function () {
'use strict';
@@ -59,22 +62,65 @@
}
}
- function render(host, elevated) {
- host.classList.remove('hidden');
- host.innerHTML =
- ''
- + '';
- var cb = host.querySelector('#elevation-checkbox');
- cb.addEventListener('change', function () {
- setElevated(cb.checked);
- // Hard reload so server-rendered admin surfaces (profile
- // page scaffolds, hidden-entry listings) catch up. URL
- // and scroll state are preserved by the browser's normal
- // back-forward cache rules.
- window.location.reload();
- });
+ // ── URL toggle: ?admin=true | ?admin=false (typeable anywhere) ──────
+ //
+ // Admin mode is toggled via a URL query param rather than an on-screen
+ // checkbox, so it's reachable from any zddc-server page. The param only
+ // SETS the cookie; the cookie is the sticky state (it persists across
+ // navigation for its Max-Age window and is what the server reads), so
+ // there's no need to keep ?admin= in the URL once applied.
+
+ // adminParam returns true/false for a recognised ?admin= value, or null
+ // when absent / unrecognised (ignored).
+ function adminParam() {
+ try {
+ var v = new URLSearchParams(window.location.search).get('admin');
+ if (v === null) return null;
+ v = v.toLowerCase();
+ if (v === 'true' || v === '1' || v === 'on' || v === 'yes') return true;
+ if (v === 'false' || v === '0' || v === 'off' || v === 'no') return false;
+ return null;
+ } catch (_e) { return null; }
+ }
+
+ // urlWithoutAdmin returns the current URL with the admin param stripped
+ // (other params + hash preserved) — what we navigate/replace to so the
+ // dirty param isn't bookmarked and Back doesn't re-trigger it.
+ function urlWithoutAdmin() {
+ var u = new URL(window.location.href);
+ u.searchParams.delete('admin');
+ var qs = u.searchParams.toString();
+ return u.pathname + (qs ? '?' + qs : '') + u.hash;
+ }
+
+ // handleAdminParam applies a ?admin= request. Returns true when a
+ // navigation (reload) is underway so the caller can stop. Enabling is
+ // gated on can_elevate — a non-admin who types ?admin=true just gets
+ // the param stripped, never a misleading red border. Disabling is open
+ // (anyone may drop a cookie they somehow hold).
+ async function handleAdminParam() {
+ var want = adminParam();
+ if (want === null) return false;
+ var clean = urlWithoutAdmin();
+ if (want === isElevated()) {
+ // Already in the requested state — just clean the URL, no reload.
+ try { history.replaceState(history.state, '', clean); } catch (_e) {}
+ return false;
+ }
+ if (want === true) {
+ var access = await fetchAccess();
+ if (!access || !access.can_elevate) {
+ try { history.replaceState(history.state, '', clean); } catch (_e) {}
+ return false;
+ }
+ setElevated(true);
+ } else {
+ setElevated(false);
+ }
+ // Navigate to the clean URL (a real load, so the server re-renders
+ // under the new cookie) and replace history so Back is safe.
+ window.location.replace(clean);
+ return true;
}
// Page-wide affordances when elevation is active. The toggle alone
@@ -116,26 +162,16 @@
}
async function init() {
- // Body chrome applies on every page load whether or not the
- // header has a toggle slot — the banner needs to surface in
- // tools / pages that don't host the toggle (e.g. iframed
- // classifier inside browse's grid mode), so the user can't
- // accidentally write through an elevated context elsewhere.
+ // Apply (or tear down) the red border + banner from the cookie on
+ // every page load — admin mode is toggled by URL, but the armed
+ // chrome must surface everywhere so the user can't accidentally
+ // write through an elevated context on a page they didn't toggle.
applyArmedChrome(isElevated());
- var host = document.getElementById('elevation-toggle');
- if (!host) return; // tool doesn't include the slot yet — no-op
- var access = await fetchAccess();
- if (!access) return; // anonymous / endpoint missing — no-op
- // Surface ONLY for users who have admin authority somewhere.
- // /.profile/access ships `can_elevate` as an elevation-
- // INDEPENDENT signal — true for any user named in any admin
- // list, regardless of current cookie state. The other flags
- // (is_super_admin, has_any_admin_scope) reflect EFFECTIVE
- // authority and would be false for an un-elevated admin
- // who hasn't toggled yet — so we can't gate on those.
- if (!access.can_elevate) return;
- render(host, isElevated());
+ // Honour ?admin=true|false typed into any zddc-server URL. There's
+ // no on-screen toggle anymore — the URL is the enable path and the
+ // red banner's "Drop admin" button is the one-click disable.
+ await handleAdminParam();
}
if (document.readyState === 'loading') {