hub: merge the customer edit page into the Edit tab (v0.48.0 edit-a, part 2)

- Settings tab renamed Edit; embeds config_form_body (.ConfigForm via the
  builder) + Controller Update + Geo + a new Danger zone card holding the
  relocated Block/Unblock/Delete forms (endpoints + confirm() unchanged).
  All cards are SIBLINGS after </form> — never nested in the config form.
- Customer Info header loses the Edit link and Block/Delete forms; only the
  config-less Create Config action stays.
- GET /configs/{id}/edit is a 302 to /customers/{id}#tab=edit; tabs JS gains
  the settings→edit legacy-hash alias.
- Post-action redirects land back on their tab: update/block/unblock/
  offsite-reissue/offsite-freeze/pbsdr-reissue → #tab=edit, regen-password
  → #tab=setup; delete unchanged (/configs).
- handleConfigUpdate gains the server-side twin of the form's required
  fields; the error path re-renders the STANDALONE page with the SUBMITTED
  overrides (B3 red-proof: nil overrides → typed values reset → test FAILS;
  header red-proof: restored header buttons → count=2 → test FAILS; both run).
- Tests: Group A (panel surface, sibling forms, header cleaned by COUNT),
  Group B (B1 302, B2 create unchanged, B3 typed-values, B4/B5 anchor table).
  Amended pins: customer_tabs_test settings→edit; pbsdr_test postUpdate now
  supplies the required fields + FormRendersState asserts the embedded render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TZc5w5jDhFLv6qDC32KN5v
This commit is contained in:
2026-07-12 17:33:07 +02:00
parent e74014730a
commit 2e03de1e0c
6 changed files with 384 additions and 40 deletions
@@ -89,7 +89,7 @@
<a href="#tab=overview" data-tab="overview" class="active">Overview</a>
<a href="#tab=applications" data-tab="applications">Applications</a>
<a href="#tab=setup" data-tab="setup">Setup</a>
<a href="#tab=settings" data-tab="settings">Settings</a>
<a href="#tab=edit" data-tab="edit">Edit</a>
<a href="#tab=backup" data-tab="backup">Backup &amp; DR</a>
<a href="#tab=events" data-tab="events">Events{{with mapGet .EventCounts "error"}}<span class="tab-badge">{{.}}</span>{{end}}</a>
<a href="#tab=notifications" data-tab="notifications">Notifications</a>
@@ -104,26 +104,9 @@
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<h2>Customer Info</h2>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
{{if .HasConfig}}
<a href="/configs/{{.CustomerID}}/edit" class="btn btn-outline btn-sm">Edit</a>
{{if .IsBlocked}}
<form method="POST" action="/customers/{{.CustomerID}}/unblock" style="display:inline">
{{.CSRFField}}
<button type="submit" class="btn btn-sm">Unblock</button>
</form>
{{else}}
<form method="POST" action="/customers/{{.CustomerID}}/block" style="display:inline"
onsubmit="return confirm('Block this customer? They will be hidden from the Dashboard.')">
{{.CSRFField}}
<button type="submit" class="btn btn-outline btn-sm">Block</button>
</form>
{{end}}
<form method="POST" action="/configs/{{.CustomerID}}/delete" style="display:inline"
onsubmit="return confirm('Delete configuration for {{.CustomerID}}? This cannot be undone.')">
{{.CSRFField}}
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
{{else}}
{{/* v0.48.0 edit-a: Edit/Block/Delete moved to the Edit tab (form + danger
zone); only the config-less bootstrap action stays in the header. */}}
{{if not .HasConfig}}
<form method="POST" action="/customers/{{.CustomerID}}/create-config" style="display:inline">
{{.CSRFField}}
<button type="submit" class="btn btn-sm">Create Config</button>
@@ -539,8 +522,17 @@
</div>
<!-- ═══ Settings ═══ -->
<div class="tab-panel" data-tab="settings">
<!-- ═══ Edit ═══ (v0.48.0 edit-a: the standalone config form embedded via the shared
config_form_body sub-template, plus the relocated Block/Delete danger zone. The
Controller Update / Geo / Danger zone cards are SIBLINGS after </form> — nesting
them inside the config <form> is invalid HTML and breaks the formaction sub-buttons. -->
<div class="tab-panel" data-tab="edit">
{{if .HasConfig}}
{{template "config_form_body" .ConfigForm}}
{{else}}
<section class="card"><p class="text-muted">No managed config yet — create one with the Create Config button above.</p></section>
{{end}}
{{if .HasReports}}
<!-- Controller Update -->
@@ -596,9 +588,8 @@
</form>
<p class="text-muted" style="margin-top: 0.75em; font-size: 0.8em;">
Controller updates are agent-driven (the version floor above) and config is delivered by the
box pulling it on a config change — the hub never connects into the box. Edit the config via
the <strong>Edit</strong> button (Overview tab); the controller re-pulls and restarts on its
next report.
box pulling it on a config change — the hub never connects into the box. Edit the config in
the form above; the controller re-pulls and restarts on its next report.
</p>
</section>
@@ -673,6 +664,34 @@
<section class="card"><p class="text-muted">Controller update and geo-restriction settings appear once the first report arrives.</p></section>
{{end}}
{{if .HasConfig}}
<!-- Danger zone (v0.48.0 edit-a): the Block/Delete forms relocated verbatim from the
Customer Info header — endpoints and confirm() handlers unchanged. -->
<section class="card">
<h2>Danger zone</h2>
<p class="text-muted">Blocking hides the customer from the Dashboard (reports are still accepted); deleting removes the managed configuration permanently.</p>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.5rem;">
{{if .IsBlocked}}
<form method="POST" action="/customers/{{.CustomerID}}/unblock" style="display:inline">
{{.CSRFField}}
<button type="submit" class="btn btn-sm">Unblock</button>
</form>
{{else}}
<form method="POST" action="/customers/{{.CustomerID}}/block" style="display:inline"
onsubmit="return confirm('Block this customer? They will be hidden from the Dashboard.')">
{{.CSRFField}}
<button type="submit" class="btn btn-outline btn-sm">Block</button>
</form>
{{end}}
<form method="POST" action="/configs/{{.CustomerID}}/delete" style="display:inline"
onsubmit="return confirm('Delete configuration for {{.CustomerID}}? This cannot be undone.')">
{{.CSRFField}}
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
</form>
</div>
</section>
{{end}}
</div>
<!-- ═══ Backup & DR ═══ -->
@@ -1049,7 +1068,9 @@
panels.forEach(function(p) { known[p.getAttribute('data-tab')] = true; });
function currentTab() {
var m = (location.hash || '').match(/^#tab=([a-z-]+)$/);
return (m && known[m[1]]) ? m[1] : 'overview';
var t = m ? m[1] : '';
if (t === 'settings') t = 'edit'; // legacy alias: the Settings tab became Edit (v0.48.0)
return known[t] ? t : 'overview';
}
function activate() {
var tab = currentTab();