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 — bail; whatever's left is suspect.
- return b.String()
- }
- body = body[i+j+len(""):]
- }
-}
-
// 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 still ships inertly so any caller could
- // hydrate it after a successful /access fetch — but a non-admin's
- // /access response carries empty AdminSubtrees and the JS skips
- // instantiation. The active-markup check below proves the live DOM is
- // admin-clean regardless.
+ // The page now renders through the shared tables engine with a server-
+ // injected #table-context (no bespoke scaffolds). The per-role contract:
+ // the context must never NAME a capability the caller lacks — super-admin
+ // diagnostics (config/logs/whoami) appear only for a super-admin, so a
+ // non-admin's bytes can't even reference them.
+ diag := ProfilePathPrefix + "/config"
+
+ // Anonymous: "Not signed in" identity, no diagnostics.
anon := render("")
if !strings.Contains(anon, "Not signed in") {
t.Errorf("anonymous body missing 'Not signed in'")
}
- anonActive := stripTemplates(anon)
- for _, marker := range []string{
- `