Commit Graph
10 Commits
Author SHA1 Message Date
Nate Mendes 20b15442b4 feat(forms): parse pasted formatted amounts in money fields (#3300)
* feat(forms): parse pasted formatted amounts in money fields

Number inputs silently reject pasted values like "20,000 " or
"1.234,56", leaving the field blank. Money fields now intercept the
paste, run it through parseLocaleFloat (spaces stripped, thousands and
locale decimal separators handled), and insert the plain number — so
amounts copy cleanly out of statements and spreadsheets.

* fix(forms): reject non-amount pastes and honour currency precision

parseLocaleFloat coerces unparseable text to 0, so the finite check in
pasteAmount never fired. Pasting "$1,234.56" — or any stray text — wrote
0.00 over the field instead of falling through to the browser, which is
worse than the blank field the feature set out to fix. The clipboard text
is now validated first by a new parse_amount_paste util, which also strips
a leading or trailing currency symbol so statement formats parse, and
reads parenthesised amounts as negative rather than dropping the sign.

The precision fallback was hard-coded to 2 while the field renders with
the selected currency's default precision, so pasting into a BTC or KWD
amount lost digits. It is derived from the input's step instead, which
already tracks the live currency selection.

Dispatch change alongside input: auto_submit_form listens for change on
number inputs, so an auto-submitting money field never saved a pasted
value.

* fix(forms): read the sign before stripping the currency symbol

The currency strip ran before the sign was read and accepted any
non-digit run, so "$-500" parsed as +500 and "USD (1,200.00)" as +1200.
Since the handler also dispatches change, an auto-submitting money field
could post a debit as a credit. The same permissive strip turned prose
like "memo 500" into 500.

The text is matched against a strict grammar instead: an optional sign,
an optional currency symbol or code of at most three characters on either
side, and a number. The sign is read first so it survives the strip, and
parentheses still mark a negative with the currency allowed outside them.
Anything that does not fit returns null and the browser handles the paste.

* fix(forms): accept only symbols and ISO codes as currency markers

The currency token matched any one-to-three character non-digit run, so
"fee 500" and "500 tax" parsed as 500 rather than falling through to the
browser. With the change event this handler dispatches, an auto-submit
form could save an amount lifted out of prose.

A marker is now either a letter-free symbol or a three-letter uppercase
ISO code. Lowercase prose no longer matches. The cost is that markers
containing letters, "R$" and "kr" among them, are no longer stripped and
those pastes fall through untouched — the safe direction, since accepting
them means accepting "fee 500" too. Telling them apart would need the
server's currency list, which a paste event cannot wait for.

* fix(forms): strip only currency symbols from config/currencies.yml

Matching a currency marker by shape accepted anything shaped like one:
"TAX" satisfied the three-uppercase-letter branch and "***" satisfied the
symbol branch, so "TAX 500" and "*** 500" both parsed as 500 and could be
written into the field.

The marker is now one of the 29 letter-free symbols this app already
declares in config/currencies.yml, so the allowlist is the supported set
rather than a shape. Lettered markers, "USD" and "kr" and "R$" among them,
are no longer stripped: telling them from prose needs the currency list,
which a paste event cannot wait for, and leaving the field untouched is
the safer of the two failures.

* fix(forms): reject multi-cell pastes and keep step="any" precision

Three parsing gaps, all reachable from a normal paste.

Internal whitespace was allowed anywhere inside the digit run, so two
adjacent spreadsheet cells arrived as one amount: "100\t200" parsed as
100200 and "1,234.56\t500" as 1234.565. Only the spaces locales actually
use to group digits are accepted now (space, no-break space, narrow
no-break space), so "1 234,56" still parses and a tab- or newline-joined
paste falls through to the browser.

#pastePrecision fell back to two decimals whenever the step was not
numeric, and Number("any") is NaN. Four money fields render step="any"
with no precision — the trade amount, price and fee on trades/show and
the fee on trades/_form — so a sub-cent crypto price pasted there was
truncated to "0.00". A step that declares no precision now writes the
parsed value unrounded rather than rounding it to a guess.

The doc comment still used "USD (1,200.00)" as its worked example, which
stopped parsing when currency markers were narrowed to letter-free
symbols.

The cases now import the shipped parser instead of a hand-copied
duplicate, rewriting its importmap specifier to a file URL so Node can
resolve it. That deletes the copy that could drift, and the three new
multi-cell cases fail against the previous parse_amount_paste.js with
"actual: 100200, expected: null".

Verified with node --test test/javascript/. Ruby and lint checks are
unchanged by this commit; biome runs in CI.
2026-09-05 06:11:56 +02:00
Juan José MataandClaude 9dfcf9c823 Remove unfinished Cloudflare Workers PR preview deployments (#3074)
The Cloudflare Containers preview stack was never finished and no open PR
carries the `preview-cf` label, so nothing is actively using it. Meanwhile
the deploy workflow fires on every completed "Pull Request" run and spins
up a runner just to evaluate its gate and skip.

Removed entirely:

- `workers/preview/` — the Worker (Durable Object container orchestration),
  wrangler config, npm manifests, and the `deploy/` resolver, config
  renderer, and log redaction helpers
- `Dockerfile.preview` — Rails image with embedded PostgreSQL/Redis and the
  inline preview entrypoint
- `.github/workflows/preview-deploy.yml` and `preview-cleanup.yml`
- `bin/preview_deploy_security_check.rb` — CI guard for the above workflows
- `test/javascript/preview_deploy/` — tests for the deleted deploy helpers

Workflow cleanup:

- `pr.yml`: drop the `preview_image` job. Also drop the `labeled` trigger
  type, which existed only so labelling `preview-cf` re-triggered the build
  and would otherwise re-run full CI on every label change.
- `ci.yml`: drop the preview hardening validation step.
- `pipelock.yml`: drop the exclude-paths entries for the deleted files.

Deliberately untouched: the per-user preview *feature* gate
(`PreviewGateable`, `config/locales/views/preview/`), Cloudflare R2 Active
Storage config, and the Cloudflare Workers AI demo banner strings — all
unrelated to preview deployments.


Claude-Session: https://claude.ai/code/session_017TrvqNZqqzN74X7PmznJ5X

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-18 06:41:16 +02:00
Guillem Arias Fauste e56a1825b2 feat: resizable accounts & assistant sidebars (#2482)
* feat(layout): make the accounts and assistant sidebars resizable

Add draggable dividers on the left (accounts) and right (assistant)
sidebars. Width is driven by a `--{side}-sidebar-width` CSS variable on
the layout root and persisted per-device in localStorage; it composes
with the existing show/hide toggle (collapse still wins via `w-0`,
reopening restores the last width).

Clamping keeps each sidebar within its bounds (min 240px, max 480/560px)
and guarantees the center column never drops below 400px, so the
dashboard stays legible no matter how wide a sidebar is dragged.

- sidebar_resize_controller.js: pointer drag, keyboard nudge
  (arrows / Home), double-click reset, localStorage persistence
- utils/sidebar_resize.js: pure, unit-tested clamp helper
- layouts/shared/_sidebar_resize_handle.html.erb: accessible
  separator handle (role=separator, aria-label, focusable)
- test/javascript/utils/sidebar_resize_test.mjs: clamp logic tests

* fix(sidebar-resize): free a collapsed sidebar's space and guard localStorage read

- #clamp now uses the opposite sidebar's rendered width (0 when collapsed)
  instead of its stored CSS variable, so closing one sidebar lets the other
  use the freed space (addresses Codex review).
- #applyStoredWidth wraps the localStorage read in try/catch and falls back
  to the default, so connect() can't bail in storage-blocked browsers
  (addresses CodeRabbit review).
2026-06-26 06:45:08 +02:00
Will WilsonandGuillem Arias Fauste d908560ed9 feat(cashflow): deep-link category labels to filtered transactions (#2083)
* feat(cashflow): deep-link category labels to filtered transactions

Clicking a category's text label in the dashboard cashflow Sankey chart
now navigates to the transactions page filtered by that category and the
cashflow's active period date range. The colored node bar keeps its
existing zoom-into-subcategories behavior; structural nodes (Cash Flow,
Surplus) do not navigate.

URL-building lives in a pure, unit-tested utils/transactions_filter_url
module (mirroring utils/sankey_zoom) pinned in the importmap. Period dates
are threaded from the dashboard view into the Stimulus controller via data
values. This matches the existing donut chart's click-to-filter behavior.

* Update app/javascript/controllers/sankey_chart_controller.js

Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
Signed-off-by: Will Wilson <will@willwilson.uk>

---------

Signed-off-by: Will Wilson <will@willwilson.uk>
Co-authored-by: Guillem Arias Fauste <gariasf@proton.me>
2026-06-11 21:21:56 +02:00
ghost 85d7695d1f ci(preview): render Cloudflare config from trusted template (#2207) 2026-06-06 05:54:40 +02:00
ghost 92fa73ef00 ci(preview): fix Cloudflare registry image deployment (#2124)
* ci(preview): fix Cloudflare registry image deployment

Keep the preview workflow's secret-bearing deploy path on trusted tooling while
rewriting Wrangler config through registry-shaped image refs for push and deploy.
Centralize preview log redaction and extend resolver/security guard coverage for
artifact identity conflicts.

* ci(preview): keep failure diagnostics resilient

* ci(preview): redact private key diagnostics
2026-06-03 00:11:36 +02:00
ghost 5f8452d63b ci(preview): stabilize Cloudflare preview deployments (#2062)
* ci(preview): stabilize Cloudflare preview deployments

* ci(preview): bound diagnostics and cover artifact fallback

* ci(preview): isolate artifact deploy permissions

* ci(preview): tidy deployment comment rendering

* ci(preview): harden preview manifest generation

* ci(preview): fail on preview diagnostics failure
2026-05-31 13:30:03 +02:00
Michal TajchertandClaude Opus 4.7 e21ab9819f feat(dashboard): zoom into cashflow sankey categories (#1807)
* feat(dashboard): zoom into cashflow sankey categories

Click a category node on the dashboard cashflow Sankey to focus on it and
its descendants only; a back button restores the full view. Clicking the
Cash Flow node zooms to the expense (outbound) side.

- Pure utility (app/javascript/utils/sankey_zoom.js) computes the
  descendant subgraph from a clicked node, with direction inferred by
  reachability from the cash flow node (outbound for expense, inbound
  for income).
- Stable node ids emitted from the controller so the JS can identify
  nodes across re-renders.
- Stimulus controller adds chart + zoomOutButton targets, fade
  transition, and only sets a pointer cursor when a node has children.
- Node:test coverage for expense, income, cash-flow, and malformed-data
  cases; \"type\": \"module\" added to package.json so the .js util is
  ESM-compatible under Node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(dashboard): extract cashflow sankey chart partial

Deduplicate sankey chart markup between inline and expanded dialog views,
and reset zoom state when chart data changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(js): rename sankey_zoom util to .mjs to drop project-wide ESM flag

Removes "type": "module" from package.json to avoid implicitly switching
every .js file in the project to ESM (a future footgun for any .js config
file added by Biome, Vite, etc.). Renames the utility to .mjs so node --test
can import the ES module directly, and adds an explicit importmap pin since
pin_all_from only globs .js/.jsm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(assets): register .mjs MIME type for Propshaft

Propshaft derives Content-Type from Mime::Type.lookup_by_extension, which
returns nil for :mjs by default. Browsers refuse to execute ES modules
served with an empty Content-Type, breaking the sankey_zoom util loaded
via importmap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:17:35 +02:00
soky srmandJuan José Mata c7b9bc48bc Adapt holdings to number inputs (#1258)
* Adapt holdings to number inputs

* Reviews

* FIX a small provider hardcoded name

* PR l10n request

---------

Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
2026-03-23 18:27:53 +01:00
sokiee 67b502f3bf FIX comma 2026-03-23 13:39:57 +01:00