fix: apply-template auto-solves + frontend reloads via activateProject

Two changes to close the UX hole m hit on slice 6 — Apply Template
appeared to do nothing because (a) the canvas wasn't refreshed cleanly
and (b) the cables hadn't been computed yet.

Backend (internal/server/solver.go applyTemplate handler):
- After ApplyTemplate succeeds, run Solve(false) inside the same
  request. Combined response shape:
    { template_apply: <ApplyTemplateResult>, solve: <SolveResult> }
- Opt out with ?solve=0 for power-users who want to inspect the
  seeded devices/requirements before the solver runs. Response in that
  case is { template_apply: ... } only.
- If Solve fails after a successful apply, return
  { template_apply, solve_error: "..." } so the frontend can recover
  (devices are still there; m can hit Solve manually).

Frontend (web/static/main.js apply-template modal submit):
- Replaced the bare re-snapshot with a call to activateProject(pid).
  That's the canonical project-load path — it re-hydrates ALL
  collections (frames, devices, ports, io_markers, cables, bundles,
  requirements, cable_types, device_types), clears state.selection
  so a stale pre-apply selection can't linger, and routes through the
  same render() the URL-state hydration uses on initial page load.
- The slice-6 inlined re-snapshot missed the device_types refresh +
  selection reset, which I suspect was what made the canvas look
  stuck — render()ing with state.selection.kind="cable_type" or
  "requirement" pointing at a not-yet-loaded row.

Hand-test (local): Living Room + auto-solve produces 4 devices + 3
requirements + 3 cables; ?solve=0 leaves cables empty. Snapshot
includes the cables on auto-solve path.
This commit is contained in:
mAi
2026-05-16 01:23:37 +02:00
parent 0c7d165ed6
commit 2cd981d3ae
2 changed files with 35 additions and 11 deletions

View File

@@ -108,7 +108,27 @@ func (h *handlers) applyTemplate(w http.ResponseWriter, r *http.Request) {
writeError(w, err, nil)
return
}
writeJSON(w, http.StatusOK, res)
// Auto-solve by default. ?solve=0 opts out for power users who want
// to inspect the seeded devices/requirements before the solver runs.
// This is THE fix for the v6 UX hole: m hit Apply, saw an empty
// canvas because nothing reloaded *and* nothing solved. With the
// frontend re-snapshotting after the POST returns and the response
// already carrying solver output, m sees the wired diagram in one click.
skipSolve := r.URL.Query().Get("solve") == "0"
combined := map[string]any{"template_apply": res}
if !skipSolve {
solveRes, err := h.store.Solve(pid, false)
if err != nil {
// Apply succeeded but Solve failed — don't 500 the whole
// call. Return template_apply with the solve error inline so
// the UI can recover (devices are there; m can re-solve).
combined["solve_error"] = err.Error()
} else {
combined["solve"] = solveRes
}
}
writeJSON(w, http.StatusOK, combined)
}
// fmtSscan parses a base-10 int from a string, returning (n, nil) on success.

View File

@@ -2055,21 +2055,25 @@ async function openApplyTemplateModal() {
if (did) skip.push(did);
});
try {
await applyTemplate(state.active.id, {
// The server auto-solves by default since v0c7d165 — the response
// is {template_apply, solve} (or {template_apply, solve_error}).
// We don't need to read the body here; activateProject() below
// pulls a fresh snapshot that includes both the seeded devices
// and any cables the solver placed.
const projID = state.active.id;
await applyTemplate(projID, {
template_id: tid,
name_overrides: overrides,
skip_devices: skip,
});
const snap = await getSnapshot(state.active.id);
state.frames = snap.frames || [];
state.devices = snap.devices || [];
state.ports = snap.ports || [];
state.ioMarkers = snap.io_markers || [];
state.requirements = snap.connection_requirements || [];
state.cables = snap.cables || [];
state.bundles = snap.bundles || [];
dlg.close();
render();
// Route through the canonical project-load path. That re-hydrates
// ALL collections (frames, devices, ports, io_markers, cables,
// bundles, requirements, cable_types, device_types) AND clears
// the selection — important because m may have had a stale
// selection from before the apply. Slice 6's bare re-snapshot
// missed the device_types refresh + selection reset.
await activateProject(projID);
} catch (ex) {
showError(err, ex.message || "Apply failed");
}