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:
@@ -333,6 +333,11 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
// (1 today, N for a later HA cluster). Each entry is the hostDetailData view-model map
|
||||
// the shared host_detail_body sub-template renders.
|
||||
Hosts []map[string]interface{}
|
||||
|
||||
// ConfigForm (v0.48.0 edit-a): the embedded config form's view model for the Edit tab —
|
||||
// the same configFormData the standalone chrome renders. Zero-valued (and never rendered)
|
||||
// when the customer has no config.
|
||||
ConfigForm configFormView
|
||||
}
|
||||
|
||||
pendingSet := make(map[string]bool, len(pendingTails))
|
||||
@@ -415,6 +420,13 @@ func (s *Server) handleCustomerUnified(w http.ResponseWriter, r *http.Request, c
|
||||
Hosts: hostViews,
|
||||
}
|
||||
|
||||
// Edit tab (v0.48.0 edit-a): embed the config form. nil overrides → the builder parses the
|
||||
// STORED ConfigJSON (the read path; submitted-value preservation is the standalone error
|
||||
// re-render's job).
|
||||
if cfg != nil {
|
||||
data.ConfigForm = s.configFormData(r, false, cfg, nil, "")
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := s.templates.ExecuteTemplate(w, "customer_unified.html", data); err != nil {
|
||||
s.logger.Printf("[ERROR] Template render: %v", err)
|
||||
@@ -538,15 +550,16 @@ func (s *Server) handleConfigCreate(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=created", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleConfigEditForm shows the edit form for a customer config.
|
||||
// handleConfigEditForm — the standalone edit page merged into the customer page's Edit tab
|
||||
// (v0.48.0 edit-a); old links and bookmarks land on the tab. POST /configs/{id}/edit stays the
|
||||
// real mutation endpoint (the embedded form posts to it).
|
||||
func (s *Server) handleConfigEditForm(w http.ResponseWriter, r *http.Request, customerID string) {
|
||||
cfg, err := s.store.GetCustomerConfig(customerID)
|
||||
if err != nil || cfg == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
s.renderConfigForm(w, r, false, cfg, nil, "")
|
||||
http.Redirect(w, r, "/customers/"+customerID+"#tab=edit", http.StatusFound)
|
||||
}
|
||||
|
||||
// handleConfigUpdate processes the edit form submission.
|
||||
@@ -565,6 +578,17 @@ func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, cust
|
||||
cfg.CustomerName = strings.TrimSpace(r.FormValue("customer_name"))
|
||||
cfg.Domain = strings.TrimSpace(r.FormValue("domain"))
|
||||
cfg.Email = strings.TrimSpace(r.FormValue("email"))
|
||||
|
||||
// Server-side twin of the form's required attributes (v0.48.0 — B3). The error re-render is
|
||||
// the STANDALONE page and carries the SUBMITTED overrides, so nothing the operator typed is
|
||||
// lost; runs BEFORE provisioning so an invalid submit never touches Hetzner/ep0.
|
||||
if cfg.CustomerName == "" || cfg.Domain == "" {
|
||||
var submitted map[string]interface{}
|
||||
_ = json.Unmarshal([]byte(buildConfigJSON(r)), &submitted)
|
||||
s.renderConfigForm(w, r, false, cfg, submitted, "Display Name and Domain are required.")
|
||||
return
|
||||
}
|
||||
|
||||
cfg.ConfigJSON = buildConfigJSON(r)
|
||||
|
||||
if err := s.applyOffsite(r.Context(), r, cfg); err != nil {
|
||||
@@ -588,7 +612,7 @@ func (s *Server) handleConfigUpdate(w http.ResponseWriter, r *http.Request, cust
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] Customer config updated: %s", customerID)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=updated", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=updated#tab=edit", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleOffsiteReissue (F4) resets the customer's offsite credential and stores a fresh one-time password —
|
||||
@@ -632,7 +656,7 @@ func (s *Server) handleOffsiteReissue(w http.ResponseWriter, r *http.Request, cu
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] offsite credentials re-issued for %s (fresh one-time password stored; ConfigVersion bumped)", customerID)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=offsite_reissued", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=offsite_reissued#tab=edit", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleOffsiteFreeze (SLICE 4) freezes/unfreezes the customer's shared sub-account (readonly) — an
|
||||
@@ -672,7 +696,7 @@ func (s *Server) handleOffsiteFreeze(w http.ResponseWriter, r *http.Request, cus
|
||||
if !frozen {
|
||||
flash = "offsite_unfrozen"
|
||||
}
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash="+flash, http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash="+flash+"#tab=edit", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleConfigDelete deletes a customer config.
|
||||
@@ -726,7 +750,7 @@ func (s *Server) handleConfigRegenPassword(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] Retrieval password regenerated for %s", customerID)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=password_regenerated", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=password_regenerated#tab=setup", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleBlockCustomer sets a customer's status to "blocked".
|
||||
@@ -742,7 +766,7 @@ func (s *Server) handleBlockCustomer(w http.ResponseWriter, r *http.Request, cus
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] Customer blocked: %s", customerID)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=blocked", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=blocked#tab=edit", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleUnblockCustomer sets a customer's status back to "active".
|
||||
@@ -758,7 +782,7 @@ func (s *Server) handleUnblockCustomer(w http.ResponseWriter, r *http.Request, c
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] Customer unblocked: %s", customerID)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=unblocked", http.StatusSeeOther)
|
||||
http.Redirect(w, r, "/customers/"+customerID+"?flash=unblocked#tab=edit", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// countBoxesBelowFloor counts reporting boxes whose EFFECTIVE floor (per-customer override else the
|
||||
|
||||
Reference in New Issue
Block a user