The engine migration replaced the old mm/* modal-stack wizards (which slid forward/back between steps) with wizard2, but never carried the slide over — step transitions went flat. Restore the original mechanism in the shared engine so all wizards (new-invoice, vendor, client, pay, transaction-rule) get it: - wizard2/step-slide-classes: the group-[.forward]/transition:htmx-* and group-[.backward]/* slide variants, applied to the swapped <form>. - wizard2/transitioner: the #transitioner wrapper whose @htmx:after-request hook reads the x-transition-type response header and toggles group/transition + forward|backward on itself. All 5 configs' :open-response now use it. - wizard2/handle-step-submit sets x-transition-type (forward on advance, backward on Back, none on a same-step validation re-render) + HX-reswap "outerHTML swap:0.16s" so the slide-out plays before the swap. Direction computed from step order (transition-type). - Removed the interim per-card fade-in in favor of this. - Rebuilt output.css so the 16 fwd + 16 back slide variants are compiled. REPL-verified: open-wizard emits the transitioner, the form carries the slide classes, and submit responses carry the transition headers. Live verification needs a server refresh (the dev server froze its route table at startup). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
15 KiB
SSR Modal Regression Fixes — QA Findings & Resumable Task List
Status: ALL identified items FIXED + verified (2026-06-27) — BUG A, BUG C, BUG D, BUG B, BUG E/F, the CSS size root cause, and a subtle wizard fade-in (§3). The only thing intentionally left is the optional richer forward/back slide transition, which needs design sign-off (§3). Owner: Bryce Branch:
integreat-execute-refactorDate: 2026-06-27 Context: Follow-up to the SSR rendering-modernization migration (2026-06-02-001-refactor-ssr-rendering-modernization-plan.md, Phases 2–11 complete). The migration to simplified route rendering + whole-form/wizard swaps introduced modal size, animation, and a set of HTTP 500 regressions. This doc records a full browser QA pass (agent-browser, 9 migrated modals) with root causes and fixes.
0. How to resume / reproduce
- App is running on the port in
.http-port(was42987); nREPL innrepl-port(was35689). - Browser session:
agent-browser --session integreat .... Log in viahttp://localhost:<port>/dev-login→ "Continue to dashboard". - Test at a realistic viewport:
agent-browser --session integreat set viewport 1440 900(a short viewport exaggerates modal overflow and gives false positives). - Screenshots from this pass are in
./tmp/(gitignored):02..19-*.png. Raw running notes:./tmp/findings.md. - After rebuilding CSS, the browser caches the old
output.css; bust it by swapping the<link>href (?v=<n>) or hard-reloading, or you'll keep seeing the old sizes.
1. Test matrix (9 migrated modals)
| Modal | File | Type | Result |
|---|---|---|---|
| Transaction Edit | transaction/edit.clj |
form | Opens OK; BUG C — whole-form swap 500 |
| Transaction Bulk Code | transaction/bulk_code.clj |
form | ✅ Works |
| Sales Summary Edit | pos/sales_summaries.clj |
form | ✅ Works |
| Invoice Bulk Edit | invoices.clj |
form | ✅ Works (empty-selection 500 = BUG D) |
| Transaction Rule | admin/transaction_rules.clj |
wizard | ✅ Works |
| Invoice Pay | invoices.clj |
wizard | ✅ Works (narrow until CSS rebuild — see §2) |
| New Invoice | invoice/new_invoice_wizard.clj |
wizard | BUG A — choose-client 500 |
| Vendor | admin/vendors.clj |
wizard | Size broken (§2) + BUG E inner overflow |
| Client | admin/clients.clj |
wizard | Size broken (§2) + BUG F inner overflow |
Cross-cutting: §2 (stale CSS → sizes), BUG B (footer EDN leak), animations (§3).
2. ★ ROOT CAUSE of broken modal SIZES — stale compiled Tailwind CSS
resources/public/output.css (committed, last built Jun 24) is missing the migration's
newer arbitrary size classes. Tailwind only compiles classes present in source at build time,
so any size value added/changed after Jun 24 isn't in the CSS and the element falls back to its
w-full h-full sibling class → the modal balloons to full width.
- Present (these modals size correctly today):
md:w-[750px](New Invoice),md:w-[950px](Transaction Edit / Bulk Code / Sales Summary),md:h-[600px],md:h-[650px],w-[850px](Transaction Rule). - Missing (these balloon / mis-size):
md:w-[760px](Vendor),md:w-[820px](Client),md:h-[520px],md:h-[560px],w-[50em](Invoice Pay).
Verified: rebuilding to a temp file generates all missing classes; after rebuild + cache-bust, the Vendor card went 1257px → 760×520 and Client → 820×560 (exactly as declared).
DEEPER ROOT CAUSE found while fixing (2026-06-27): tailwind.config.js content only scanned
./src/**/*.{cljs,clj,cljc} — it never scanned resources/templates/**/*.html (the 46 Selmer
templates the migration introduced). So a naive rebuild drops every template-only class
(e.g. md:w-[950px] / md:h-[650px], used only in templates/transaction-edit/edit-modal.html,
which would have re-broken Transaction Edit / Bulk Code / Sales Summary). The durable fix is to add
the templates glob to the config, then rebuild.
FIX — DONE & verified:
- Added
"./resources/templates/**/*.html"totailwind.config.jscontent. npx tailwindcss -i resources/input.css -o resources/public/output.css(kept unminified to match the committed file; add--minifyonly if the prod pipeline minifies).
- Verified: all modal size classes now present (
md:w-[760px],md:w-[820px],md:h-[520px],md:h-[560px],w-[50em]and the template-sourcedmd:w-[950px]/md:h-[650px]). Class-diff vs the old CSS shows the only removed classes are orphaned (the deletedmm/*modal-stackforward/backward/group/transition/htmx-*:translate-x-2/3animation set + the unusedlg:w-[900px]sales size) — none still referenced in src/ or templates/. Vendor modal confirmed live at 760×520 (was 1257px). - Still TODO: confirm the prod/CI build runs this tailwind step (so output.css can't drift
again) — ideally wire it into
lein build/ buildspec.
Note: during QA I rebuilt
output.cssto verify, then reverted it so the working tree is clean for review. The verified rebuild is preserved attmp/output-rebuilt.css; backup attmp/output.css.bak.
3. Modal ANIMATIONS
The new wizard step cards swap via hx-target "this" hx-swap "outerHTML" but their
modal-card-advanced carries no transition classes, so step→step and modal enter/leave do not
animate:
- New Invoice steps (
render-basic-details,render-accounts): card class is onlymd:w-[750px] md:h-[600px] w-full h-full— nohtmx-swapping:/htmx-added:variants. next-steps-modal(new_invoice_wizard.clj ~678): only staticscale-100 translate-x-0 opacity-100— missing thehtmx-swapping:/htmx-added:swap variants.- By contrast,
dialog/success-modal-and the Invoice Pay card (last-modal-step transition duration-150) keep transitions, so the intended pattern still exists to copy from.
FIX — DONE (forward/back slide restored) in the shared wizard2 engine. The user confirmed the
old wizard had a directional slide forward/back between steps; the engine migration dropped it.
Restored the original mechanism (read out of the deleted mm/multi_modal.clj) in one shared place:
wizard2/step-slide-classes— thegroup-[.forward]/transition:htmx-*+group-[.backward]/…variants, now applied to the swapped wizard<form>.wizard2/transitioner— the#transitionerwrapper with the@htmx:after-requesthook that reads thex-transition-typeresponse header and togglesgroup/transition+forward|backwardon itself (so the variants fire on the next swap). All 5 wizard configs':open-responsenow wrap the form inwizard2/transitionerinstead of a plain#transitionerdiv.wizard2/handle-step-submitnow setsx-transition-type(forward on advance, backward on Back,noneon a same-step validation re-render) +HX-reswap: outerHTML swap:0.16s(the swap delay that lets the slide-out play). Direction is computed from the step order (transition-type).- The earlier per-card
fade-in(interim) was removed in favor of this. - CSS rebuilt so the
group-[.forward]/transition:htmx-*variants (16 fwd + 16 back) are compiled. - Applies to ALL wizards (new-invoice, vendor, client, pay, transaction-rule) since it lives in the
engine. REPL-verified:
open-wizardemits the transitioner, the form carries the slide classes, and submit responses carryx-transition-type+ theHX-reswapswap delay. - Live-verify caveat: the long-lived dev server froze its route table at startup
(
auto-ap.handler/match->handler-lookupis adefthat merged the per-nskey->handlermaps), so an nREPL reload of leaf namespaces does NOT reach the running router — a server refresh (stop jetty → tools.namespace refresh → user/start-http) or a freshlein runis needed to see it live. (Documented hazard; see the QA memory.)
4. Bug list + fixes (prioritized task list)
P0 — functional 500s
BUG A — New Invoice: choosing a client → 500 from /invoice/new/due-date.
- File:
src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj, fndue-date(and same latent bug inscheduled-payment-date). - Exception:
ClassCastException: org.joda.time.DateTime cannot be cast to java.util.Date. - Cause:
form-vendor-clientdecodes:invoice/date/:invoice/dueto clj-timeDateTime(viaclj-date-schema), butdue-datethen calls(some-> date coerce/from-date)—coerce/from-dateexpects ajava.util.Date.scheduled-payment-datehas the same(some-> due coerce/from-date)but is masked becausedueis nil-guarded by awhen. - Repro: open New Invoice, pick any client (vendor still empty) → red "unexpected error" banner;
network shows PUT
/invoice/new/due-date= 500. REPL-confirmed the decoded date is already aDateTimeandtime/plusworks on it directly. - FIX — DONE & verified live: dropped the
coerce/from-datecalls —due-datenow uses the decodeddate/dueDateTimes directly (removed thedate (some-> date coerce/from-date)rebind and theduecoerce); same forscheduled-payment-date. Live: selecting a client now fires PUT/invoice/new/due-date+/scheduled-payment-date→ 200 (was 500).
BUG C — Transaction Edit: any whole-form swap (mode toggle, vendor change, add/remove row, $/%
toggle) → 500 from /transaction2/edit-form-changed, whenever the txn has ≥1 autopay-invoice
match. (HIGH severity — breaks the flagship modal's core swap doctrine.)
- File:
src/clj/auto_ap/ssr/transaction/edit.clj. - Exception:
ClassCastException: PersistentVector cannot be cast to Namedat edit.clj:849 (links-body*does(name (:action step-params))). - Cause: the link-panels each render a hidden
<input name="action">. The unpaid panel (line 690) and rule panel (line 730) add:form ""to exclude that hidden from form serialization. The autopay panel (line 661) is missing:form "", so itsaction="link-autopay-invoices"hidden is serialized alongside the mainactionhidden (line 862, value "manual"). Ring collapses the duplicateactionparams into a vector →(name vector)throws. - Repro: edit a transaction whose "Link to autopay invoices" tab shows a badge count ≥1, click
"Switch to advanced mode". REPL-confirmed: single
action→ 200;actionas a vector → the exact 500. (Txns with 0 autopay matches renderpanel-empty*with no action-hidden and swap fine — hence intermittent.) - FIX — DONE & verified live: added
:form ""to the autopay panel's action-hidden (edit.clj:661) to match the unpaid/rule panels. Live: on a txn with an autopay match, "Switch to advanced mode" now POSTsedit-form-changed→ 200 with a singleaction=manualparam (the duplicateaction=link-autopay-invoicesis gone) and the mode toggles correctly.- (Optional hardening, not applied: make
links-body*coerce a vector action via(some-> (:action step-params) (as-> a (if (coll? a) (last a) a)) name).)
- (Optional hardening, not applied: make
P1 — sizing / layout (after §2 CSS rebuild)
§2 CSS rebuild — do first; fixes Vendor/Client/Invoice-Pay widths.
BUG E/F — Vendor + Client wizard step layout overflows the card — DONE & verified live.
- Actual root cause (found while fixing):
step-cardplaces the step timeline in a vertical left sidebar (grow-0 ... self-stretch hidden md:block), butvendor-timeline/client-timelinerendered the horizontaltimeline/timelinecomponent. A horizontal<ol>in a shrink-to-fit sidebar forced the sidebar to ~full card width, pushing the body off-screen → horizontal scroll, clipped fields, the "huge grey left region". (Vendor additionally pinned its bodies to a fixedw-[600px], compounding it.) - FIX applied:
vendors.cljvendor-timeline+clients.cljclient-timeline: usetimeline/vertical-timeline+timeline/vertical-timeline-step(the components already existed) instead of the horizontal pair.vendors.clj: the 5 step bodies'w-[600px] h-[350px]→w-full h-[350px]so the body fills the remaining width next to the now-narrow vertical timeline.
- Verified live: Vendor 760×520 and Client 820×560 with a proper vertical timeline, no horizontal overflow, fields fill the body; forward nav (Info→Terms) works, the vendor-name chip renders, and the validation-error re-render also lays out correctly.
- Note: edits made with the plain
Edittool (exact-string) — the clojure-mcp editors reformat the whole file against a stricter config than the project'slein cljfmt, producing large spurious diffs; both files passlein cljfmt check.
P2 — robustness / cosmetic
BUG D — Invoice Bulk Edit with no selection → 500 — DONE & verified live. With no
selection, selected->ids returns nil, which all-ids-not-locked fed straight into a Datomic
:in $ [?i ...] query → "Unable to find data source" exception → global "Oh, drat!" toast.
- File:
src/clj/auto_ap/ssr/invoices.clj— wrappedall-ids-not-locked's body in(when (seq all-ids) ...), so an empty/nil selection yields[]and the modal opens cleanly (also protects every other caller). Live: Bulk Edit with nothing selected now opens the modal, no toast, no 500.
BUG B — Footer leaks a raw Hiccup attr-map as text in the red "unexpected error" banner
({:x-show "unexpectedError", ...}). Pre-existing in master (NOT a migration regression), but
visible whenever unexpectedError flips true (e.g. it showed during BUG A). Cause: modal-footer-
calls (hx/alpine-appear {...}) twice — the 2nd return value lands in child position and
renders as literal EDN.
- File:
src/clj/auto_ap/ssr/components/dialog.clj~line 59. - FIX — DONE & verified: deleted the duplicate
(hx/alpine-appear ...)line inmodal-footer-. Rendering the footer to HTML now shows a singlealpine-appear(the legit attrs) and no literal{:x-show ...}text child.
Cosmetic — over-tall empty modals. Invoice/Transaction Bulk Edit and similar use fixed
md:h-[650px]; with little content they show a large empty lower region. Consider letting height
hug content (cap with max-h) rather than a fixed height. Low priority.
5. Suggested order of work
- §2 CSS rebuild (unblocks all width checks; commit
output.css; verify prod build does this). - BUG A + BUG C (the two functional 500s; one-liners each, REPL-verified).
- BUG E / BUG F (wizard step-body layout rework; needs visual iteration per step).
- §3 animations (add swap-transition classes to wizard step cards).
- BUG D, BUG B (robustness / cosmetic).
- Re-run the full agent-browser pass (all 9 modals) + the Playwright e2e suite as the parity gate.
6. What was verified vs. inferred
- Verified in browser + REPL: BUG A (500 + exact exception + fix direction), BUG C (500 + exact exception + vector repro + line 661 cause), §2 CSS root cause (missing classes + rebuild fixes Vendor 1257→760 and Client→820), BUG D (500 on empty selection), Vendor/Client inner overflow (screenshots 16/17), and that Bulk Code / Sales Summary / Transaction Rule / Invoice Pay / Transaction Edit-open all render without errors.
- Inferred (static analysis, not yet visually A/B'd against master): §3 animation regression
(step cards lack
htmx-swapping:/htmx-added:classes) and BUG B being pre-existing.