From d9256050d21c301cabed1e606658021ef81732f4 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Sat, 6 Jun 2026 16:07:56 -0500 Subject: [PATCH] feat(profile): render /.profile via the tables engine (access + create + diagnostics) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retire the bespoke profile page. /.profile now renders through the shared tables engine (header chrome incl. the profile menu) from a server-injected context: the caller's "Effective access" — projects + admin subtrees — as clickable rows (rowNav opens each), identity in the description, and an apiActions "+ New project" (name → POST /.profile/projects, gated on can_create_project; roles are added afterward by editing the project's .zddc, which is now standing-editable). Super-admin diagnostics (config/logs/whoami/effective-policy) stay discoverable as rows linking to their unchanged endpoints — gated on IsSuperAdmin so a non-admin's context never even names them. Dropped as redundant/niche: the in-page theme picker (the header has the theme button), the localStorage inspector, and the "editable .zddc" links (those files are now standing-editable in browse). Extends the generic apiActions layer (tables/js/api-actions.js) with `fixed` constant fields (e.g. parent="/"), `required` field validation, and `rowNav` clickable rows (capture-phase, so it beats the editor's per-cell handlers). Rewrote TestServeProfileHTMLLayered to the new model (per-role context correctness: no admin leak; super-admin diagnostics present) and dropped the now-dead stripTemplates helper. Validated in a containerized browser; full Go suite green. Co-Authored-By: Claude Opus 4.8 (1M context) --- tables/js/api-actions.js | 40 ++++++- zddc/internal/handler/profilehandler_test.go | 112 +++++-------------- zddc/internal/handler/profilepage.go | 97 +++++++++++++++- zddc/internal/handler/tables.html | 42 ++++++- 4 files changed, 201 insertions(+), 90 deletions(-) diff --git a/tables/js/api-actions.js b/tables/js/api-actions.js index a002983..56c624d 100644 --- a/tables/js/api-actions.js +++ b/tables/js/api-actions.js @@ -49,9 +49,10 @@ var inputs = {}; (c.fields || []).forEach(function (f) { var lab = el('label', { class: 'api-modal__field' }); - lab.appendChild(el('span', null, f.label || f.name)); + lab.appendChild(el('span', null, (f.label || f.name) + (f.required ? ' *' : ''))); var inp = el('input', { type: f.type || 'text' }); if (f.placeholder) inp.setAttribute('placeholder', f.placeholder); + if (f.required) inp.required = true; inputs[f.name] = inp; lab.appendChild(inp); form.appendChild(lab); @@ -76,6 +77,12 @@ form.addEventListener('submit', function (e) { e.preventDefault(); err.hidden = true; + var missing = (c.fields || []).filter(function (f) { return f.required && !inputs[f.name].value.trim(); }); + if (missing.length) { + err.textContent = 'Required: ' + missing.map(function (f) { return f.label || f.name; }).join(', '); + err.hidden = false; + return; + } var body = {}; (c.fields || []).forEach(function (f) { var v = inputs[f.name].value.trim(); @@ -83,6 +90,9 @@ // Date fields → RFC3339 so the Go time.Time decoder accepts them. body[f.name] = (f.type === 'date') ? new Date(v + 'T00:00:00').toISOString() : v; }); + // Constant fields the server requires but the user doesn't set + // (e.g. project create's parent="/"). + if (c.fixed) Object.keys(c.fixed).forEach(function (k) { body[k] = c.fixed[k]; }); submit.disabled = true; fetch(c.url, { method: 'POST', @@ -175,12 +185,40 @@ }); } + // Per-row navigation: clicking a row opens its data-url (the project / + // subtree it represents) — used by the profile "Effective access" table. + // Clicks on inner controls (buttons/links/inputs) are left alone. + function ensureRowNav() { + var tbody = document.querySelector('#table-root tbody'); + if (!tbody) return; + var trs = tbody.querySelectorAll('tr'); + for (var i = 0; i < trs.length; i++) { + var tr = trs[i]; + if (tr.getAttribute('data-nav') === '1') continue; + var url = tr.getAttribute('data-url'); + if (!url) continue; + tr.setAttribute('data-nav', '1'); + tr.style.cursor = 'pointer'; + (function (target) { + // Capture phase: fire before the tables editor's per-cell + // click handlers (which would otherwise swallow the click on + // read-only rows). Inner controls (buttons/links/inputs) still + // opt out. + tr.addEventListener('click', function (e) { + if (e.target.closest('button, a, input')) return; + window.location.href = target; + }, true); + })(url); + } + } + function tick() { var c = cfg(); if (!c) return; hideNative(); if (c.create) mountCreate(c.create); if (c.deleteRow) ensureRowDelete(c.deleteRow); + if (c.rowNav) ensureRowNav(); } function start() { diff --git a/zddc/internal/handler/profilehandler_test.go b/zddc/internal/handler/profilehandler_test.go index 8e89d58..249dcca 100644 --- a/zddc/internal/handler/profilehandler_test.go +++ b/zddc/internal/handler/profilehandler_test.go @@ -235,32 +235,6 @@ func TestServeProfileLogsLevelFilter(t *testing.T) { } } -// stripTemplates removes every block from the -// HTML body so substring assertions check only ACTIVE markup — i.e. live -// DOM content the user (and their browser) actually sees, as opposed to -// inert content that JS may clone in based on a later access fetch. -// -// Naive but sufficient for the controlled output of profileTemplate (the -// template tags are unnested and well-formed). If the page ever grows -// nested templates, swap this for an html.Tokenizer-based pass. -func stripTemplates(body string) string { - var b strings.Builder - for { - i := strings.Index(body, "") - if j < 0 { - // Unterminated "):] - } -} - // TestServeProfileHTMLLayered pins the page-render contract after the // lazy-load refactor: // @@ -306,85 +280,53 @@ func TestServeProfileHTMLLayered(t *testing.T) { return rec.Body.String() } - // Anonymous: identity says "Not signed in", no live admin markup, no - // diagnostics. The