- playwright.config.ts: honor BASE_URL env (and skip the auto-started webServer when
set) so a server booted from a specific worktree on a non-default port can be tested
without fighting over :3333.
- skill test-recipes.md: record the recipe for running e2e from a non-default worktree
(in-process test server + reseed helper) and the measured baseline on the merged
hx-select reference: swap-doctrine 6/6 green; transaction-edit.spec.ts has a
pre-existing Shared-Location save failure that masks 7 via serial mode; full suite
30 pass / 2 fail / 7 skip. Gate for the refactor = swap spec + REPL pure-fn checks.
The strangler foundation for migrating interactive SSR components from Hiccup to
Selmer (plain-HTML Alpine/HTMX attributes instead of mixed keyword/string encodings).
- project.clj: add [selmer "1.12.61"].
- auto-ap.ssr.selmer: render / render-str (selmer/render-file + string), hiccup->html
(Hiccup -> string for {{ frag|safe }}), raw (wrap a rendered fragment for embedding
in a Hiccup tree without double-escaping), render->hiccup.
- resources/templates/interop-smoke.html: proves render-file from the classpath and
that plain-HTML alpine attrs (x-model, @keydown, tippy?.show()) pass through verbatim.
- selmer_test: 4 tests / 8 assertions covering both interop directions; all green.
Proven via REPL + tests: a Hiccup component renders inside a Selmer template, and a
Selmer fragment renders inside a Hiccup tree. Both valid during the transition.
Note in 3.1 that targeted hx-select/hx-target swaps in repeated/nested
structures may want a consistent scheme -- semantic markup + data-attributes,
or a form-path->selector helper (mirroring cursors) -- instead of hand-minting
a unique id per element. Framed as a consideration for advanced cases, with a
Phase 5 task to settle the convention into the skill cookbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Most grid pages auto-submitted their date-range filter on every change
event, which fired mid-typing and re-rendered the date inputs, breaking
manual date entry. Invoices and ledgers already gated date submission
behind an explicit Apply button; this brings the other ten pages in line.
- date-range component: stop `change` from the date inputs bubbling to
the form (@change.stop) and always render the Apply button, so typed or
picked dates submit only via the Apply button's `datesApplied` event.
The All/Week/Month/Year presets and all other filters are unaffected.
- payments, invoice import, transactions, import batches, sales
summaries, expected deposits, cash drawer shifts, refunds, tenders,
sales orders: add `datesApplied` to the form hx-trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the EDN snapshot + piecewise merge for multi-step wizards with per-step
form state stored in the session, combined only at the end -- the Django
formtools WizardView / SessionStorage model. Cite the inspiration and refs.
Adds rationale 2.4, reworks the engine snippet in 3.3 to thread session state
keyed by wizard-id (no snapshot, no merge), and updates goal 3, the Phase 6
engine tasks, the risk row, and Open decision 1 accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reframe goal 2, the rationale (2.2), the render-function pattern (3.2), and
scorecard heuristic 1 so the target is top-rooted cursors. Cursors stay; what
we remove is faking a cursor to start deeper in the tree and the duplicate
*-no-cursor* variants that fakery forces.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rewrite the plan to stand on its own: state the goals and target patterns
directly (illustrated with code snippets) instead of reconciling experimental
workstreams. Spell out every migration as concrete, checkboxed tasks an agent
can execute, with per-modal rationale and specifics.
Reorder so the first step distils the proven transaction-edit migration into a
ssr-form-migration skill (Phase 1), then trials that skill on the same modal as
its first test subject (Phase 2), then rolls out simplest-first with every
phase feeding the skill. Adds an explicit migration inventory, per-migration
playbook, quality scorecard, and test-first strategy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Synthesize three SSR refactor exercises into one low-risk, compounding
rollout plan: the render-whole-form HTMX swap doctrine, the critique-wizard
architecture simplification, and a Hiccup -> Selmer templating migration.
Includes a code-quality ratchet (per-migration scorecard), an explicit
test-first strategy with an e2e regression gate, simplest-first phasing, and
a self-reinforcing ssr-form-migration skill so each migration makes the next
cheaper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
content-card used `overflow-scroll`, which renders scrollbar tracks even
when the content fits — visible as superfluous bars around the admin chart
cards. Switch to `overflow-auto` so scrollbars only appear when content
genuinely overflows (e.g. wide data tables still scroll).
Also add a gitignored ./tmp/ scratch directory (tracked via .gitkeep) and
document in AGENTS.md that temp files belong there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin page was the only consumer of Chartist while the dashboard and
expense report already use Chart.js. Convert the admin "Growth in clients"
(bar) and "Changes by hour" (line) charts to Chart.js using the same
Alpine x-data/x-init canvas pattern as the dashboard, and drop the global
Chartist CSS/JS includes from the base page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refine per-trigger granularity now that the swap target is explicit:
- Memo issues no request at all -- it affects nothing else, so its value just
rides along in the form and is merged into the snapshot on save. (Changing the
Location *value* likewise issues no request -- it never did; that cell's request
is the account->location dependency.)
- Account select swaps only that row's Location cell (#account-location-<index> /
#simple-account-location) instead of the whole form. Selecting an account only
affects the valid Location options (computed from the posted account-id), so a
precise cell swap is safe -- no snapshot dependency.
Account-structural changes (vendor, add/remove row, mode toggle, $/% radio) keep
swapping the whole form: their accounts+amount-mode state is interdependent and
round-trips through the single form-level snapshot hidden field, so a whole-form
swap is what keeps it consistent with zero OOB.
Update the memo test to assert it fires no request and keeps its value/caret.
Full e2e suite: 27 passed / 2 failed (same pre-existing, unrelated failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Alphabetize the import.clj :require block (AGENTS.md Import Formatting).
- Remove unused imports (digest, strip) flagged by clj-kondo.
- Make the client-not-found classify-table test independent: it previously
reused the bank-account-not-found input and added zero marginal coverage;
now seeds an orphan bank account so only the client error fires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the SSR/alpine/htmx manual transaction import, wiring the
already-declared but unhandled ::external-import-page/parse/import routes.
Mirrors the SSR ledger import: paste the exact master-branch Yodlee
positional-column TSV, review parsed rows in an editable grid with per-row
error/warning badges, and import. Every master validation is preserved and
the existing import.transactions engine is reused unchanged
(via import.manual/import-batch), so core components are untouched.
- New ns auto-ap.ssr.transaction.import (page, paste/parse, editable grid,
two-tier validation, import handler) + admin-only transactions Import nav.
- Two-tier validation: fixable problems (bad date/amount, unknown client or
bank-account code, missing fields) are hard errors that block the whole
batch; inherent skip-conditions (non-POSTED, before start-date/locked,
already-imported) are warnings computed from the engine's own
categorize-transaction so the grid preview matches the import result.
- Tests: failing-first Playwright e2e (e2e/transaction-import.spec.ts) plus
unit/integration coverage (ssr/transaction/import_test.clj, 10 tests).
- Deterministic bank-account code in the e2e seed.
Plan: docs/plans/2026-06-01-001-feat-manual-transaction-import-ssr-plan.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the section-swap + OOB approach with uniform whole-form swaps,
eliminating both out-of-band swaps:
- Discrete edits (vendor, account, location, mode, add/remove row) now swap all
of #wizard-form via hx-select. The active action/tab already round-trips
(:action is in edit-form-schema and the tab x-data inits from it), so a
whole-form swap re-creates the tab state from the server value and the active
tab is preserved -- no #wizard-snapshot OOB needed, since the snapshot hidden
field rides along inside the form.
- Move the totals into their own <tbody id="account-totals"> (new optional
:footer-tbody param on data-grid-) so the amount field updates them with a
plain targeted swap instead of an OOB swap of #total,#balance. The totals tbody
is a sibling of the input rows, so the amount input is never replaced.
- Memo unchanged (hx-swap=none).
Net: 0 hx-select-oob, 0 morph. The focus invariant is unchanged -- the typed
field is never inside a region it swaps. Tab clicks stay Alpine (instant); only
the action value round-trips. Revert the now-unneeded #wizard-snapshot id.
Full e2e suite: 27 passed / 2 failed (same pre-existing, unrelated failures).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the whole-form alpine-morph swap in favour of posting the whole form
but swapping back only what changed, never the input the user is editing --
so focus and caret survive a plain swap with no morph extension.
- Discrete changes (vendor, account, location, mode, add/remove row) swap the
#manual-coding-section fragment via hx-select, plus an OOB refresh of the
#wizard-snapshot hidden field so the round-tripped wizard state stays in sync
(the snapshot lives at #wizard-form level, outside the swapped fragment, and
the new/remove-account handlers read it).
- The amount field OOB-swaps only #total/#balance (hx-swap=none); memo posts
with hx-swap=none. Neither input is ever replaced.
- Give the BALANCE cell a unique id (#balance) so the OOB selector is unambiguous.
- Remove the alpine-morph ext + @alpinejs/morph plugin and all the key/x-data
re-init tricks they required. Rebuilding the fragment fresh makes vendor->account
population and repeat vendor changes work without any keying.
- Rename e2e/transaction-edit-morph.spec.ts -> -swap.spec.ts; assertions unchanged
(focus/caret preservation, vendor->account, repeat vendor changes all hold).
Full e2e suite: 27 passed / 2 failed (both pre-existing and unrelated -- the
legacy save-flow test and the date-range filter test).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Re-render the entire #wizard-form on each field edit and swap with
hx-swap="morph" so the focused input keeps focus/caret/value while typing.
- Field-level routes return the full form and target #wizard-form
- Key state-owning wrappers (account rows, simple-mode wrapper, vendor
typeahead) so server-driven value changes re-init across the morph
- Guard tippy/$refs access in typeahead against stale post-morph state
- Round-trip simple/advanced mode via step-params[mode]
- Add e2e/transaction-edit-morph.spec.ts covering focus/caret preservation,
vendor->account population, and repeated vendor changes
- Seed a second vendor/account for test isolation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a transaction is pre-coded, the snapshot stores :transaction-account/account
as a Datomic ref map {:db/id N} rather than a bare integer. simple-mode-fields*
and the simpleAccountId Alpine initializer both need the integer id, not the map,
to correctly populate the account typeahead value and the x-hx-val binding.