mirror of
https://github.com/we-promise/sure.git
synced 2026-09-09 00:24:15 +00:00
ea4b2402bf35f5aa8afa72bdb537457656b7a503
569
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
568706bff8 |
fix(i18n): localize Wise SCA keypair results (#3413)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
65a950a87b |
fix(i18n): localize Wise SCA guidance (U7) (#3410)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
947fe8327c |
fix(i18n): localize transaction bill links (#3406)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
73abe473b7 |
Add cumulative spending chart dashboard widget (#3404)
* Add cumulative spending chart dashboard widget New dashboard section showing the selected month's running spending total against the previous month's full curve on a shared day-of-month axis, inspired by Copilot Money's Spending card. - IncomeStatement#daily_expense_series: per-day expense totals in family currency, same scoping as the other income statement totals (visible, posted, budget-included transactions; report-included accounts; daily exchange-rate conversion) - PagesController: spending_trend section with month picker (clamped like money_flow), cumulative series builder, delta vs. previous month - spending-chart Stimulus controller (D3): previous month in gray, current month in green with a today marker, gridlines with compact currency labels, shared tooltip - i18n (en) and model/controller tests * Address PR review: locale-safe axis labels, currency/rate-aware cache key - X-axis tick labels are now rendered server-side (I18n.l), one per axis day: when the previous month is longer than the selected one it owns the tail labels, so a tick can no longer roll past the selected month's end (e.g. day 31 of a February view showed "Mar 3"), and labels follow the app locale instead of D3's default English time-format locale. - IncomeStatement#daily_expense_series cache key now includes the family currency and the latest exchange-rate timestamp, since ExchangeRate:: Importer's upsert_all and currency changes leave entries/accounts untouched and previously served stale chart data. * Fix spending trend tests: empty-state month in axis test, dropped start_date key - Axis-label test picked a month pair with no transactions, so the widget rendered its empty state and there was no chart payload to parse; seed spending in both months under test. - The clamp test still asserted on the payload's removed start_date key; assert the clamped month via the current series' first point date instead. |
||
|
|
c6ca99598c |
fix(i18n): localize Trade Republic account setup (#3399)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
05a787330d |
Fix Wise incoming transfers by implementing Strong Customer Authentication (#3391)
* Support Wise Strong Customer Authentication for balance statements The balance-statement endpoint always 403s because it requires a signed one-time-token challenge (SCA) that Sure never implemented, so every sync silently fell back to /v1/transfers — an outgoing-only endpoint — meaning incoming payments into a Wise balance never synced. Adds a per-item RSA keypair (private key encrypted at rest) that signs the SCA challenge and retries the statement request once, plus a settings UI to generate the keypair and register its public key with Wise. Fixes #3384 * Backfill incoming statements past legacy transfers; fix review nits Backfill: once statements start succeeding for an account that already has legacy /v1/transfers rows, the fetch window was clamped to end the day before the oldest legacy transfer, so the window where incoming payments were actually missing (the recent window transfers already "covered" with outgoing-only data) was never re-fetched. Statement rows in that overlap are now kept when they're incoming and dropped when outgoing, since the legacy transfer rows already account for the outgoing side. Also: replace the inline onclick handler on the SCA public key display with the existing clipboard Stimulus controller (copy button, matching the API key reveal pattern), and correct the regenerate-keypair confirmation text, which implied local regeneration revokes the key with Wise -- it doesn't; the old public key stays valid there until removed manually. * Avoid double-booking internal cross-currency conversions on statement backfill The backfilled statement fetch's outgoing/incoming filter only looked at sign: a positive (credit) statement row was always kept in the legacy overlap window. But a legacy transfer row can itself be incoming for this account when it's the target side of a conversion between two of the profile's own balances -- Wise already fully captures both legs of those via /v1/transfers, unlike genuine external payments. Now an incoming statement row in the overlap window is dropped only when it matches a known incoming legacy transfer's date and amount, so internal conversions aren't duplicated while external incoming payments (no legacy counterpart) still backfill correctly. * Never drop an incoming statement row on a date/amount heuristic The previous fix dropped an incoming statement row in the legacy-overlap window when it matched a known incoming legacy transfer's date and amount, to avoid double-booking internal cross-currency conversions. But nothing short of an endpoint-proven correlation id can tell that apart from a genuine external payment that happens to share the same date and amount -- and silently losing a real transaction is worse than an occasional visible, user-correctable duplicate. Incoming rows are kept unconditionally again. Instead, bound the exposure at the source: the /v1/transfers fallback now stops running for an account as soon as it has a successful statement row, since statements alone cover both directions from then on. This leaves only a narrow, one-time window (the initial backfill of historical internal conversions) where a duplicate can occur, rather than an indefinite one. * Gate the transfer fallback per-account, not per-item legacy_transfer_import_needed? decides whether to fetch /v1/transfers at all, but that decision is profile-wide -- true as soon as any one account still needs the fallback. store_transfers_per_account then merged those transfers into every currency-matching account by currency alone, with no check for whether that specific account had already migrated to statements. A still-legacy account in one currency was enough to make an already-migrated account in the same currency re-absorb a movement its own statements already had, double-booked under a different key. account_transfers is now cleared for any account that already has statement rows, regardless of why the profile-wide fetch ran. * Handle SCA controller errors, corrupted keys, and adapter test coverage - generate_sca_keypair now rescues like every other mutating action in this controller, logging and re-rendering the panel with an error instead of a raw 500 if the update ever raises. - sca_configured? now depends on sca_public_key actually parsing, not just sca_private_key being present, so a corrupted/unparsable stored key (encryption misconfig, manual DB edit) falls back to the "generate a keypair" UI state instead of rendering a public key box around nothing. - Added test/models/provider/wise_adapter_test.rb, which had no coverage at all, to cover build_provider's family/wise_item_id resolution and that sca_private_key actually reaches the constructed Provider::Wise. * Add logging to Wise sync --------- Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
ab175cc43c |
fix(ui): make whole list row clickable, not just the name text (#3367)
* fix(ui): make whole list row clickable, not just the name text Fixes #3366. Transaction, split-parent, trade, and account list rows carry hover styling that implies the whole row is clickable, but only the name text actually opened the detail drawer — clicking the avatar, amount, or whitespace between them did nothing. Add a clickable-row Stimulus controller that delegates a click anywhere on the row to its primary link, while leaving real interactive descendants (checkbox, category menu, account link, quick-edit badge, kebab menu) to handle their own clicks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(ui): exclude popover/menu panels from row click delegation, preserve modifier clicks Codex review on #3367 flagged two issues in clickable_row_controller: - Category/account popovers render as position:fixed but stay DOM descendants of the row, so clicking their background (padding, headings, plain text) fell through to the row's link. - linkTarget.click() discarded Ctrl/Cmd/Shift modifiers, so modified clicks opened in the current frame instead of a new tab. * fix(ui): exclude quick-edit dropdown, use window.open for modified clicks CodeRabbit review on #3367 found two more issues: - The investment activity quick-edit dropdown is a hand-rolled absolute-positioned panel (not DS::Popover/DS::Menu), so it wasn't covered by the earlier popover/menu exclusion and background clicks inside it fell through to the row link. - Redispatching a synthetic MouseEvent with modifier flags doesn't actually open a new tab: browsers only honor Ctrl/Cmd/Shift on trusted, native click events, so the previous "fix" was cosmetic. Explicitly call window.open() for modified clicks instead. * fix(ui): make Dividend/Interest quick-edit badge not swallow row clicks jjmata found that the activity-label badge renders as a real <button> even when Dividend/Interest trades have no dropdown/click handler (income_trade), so clickable-row's "a, button, ..." exclusion still treats it as an interactive descendant and swallows the click instead of opening the row — same dead-spot symptom as #3366, relocated to this one badge. Render it as a <span> in that case so the row click delegates normally. * fix(ui): show pointer cursor on delegated Dividend/Interest badge CodeRabbit caught that the badge kept cursor-default after becoming a non-interactive <span> that delegates its click to the row link — the cursor no longer matched the actual (now clickable) behavior. --------- Co-authored-by: GFR <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
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.
|
||
|
|
4f14bc7859 |
feat(transactions): cascade parent/subcategory checkboxes in the category filter (#3356)
* feat(transactions): cascade parent/subcategory checkboxes in the category filter Checking a parent category in the transaction filter sidebar now auto-checks its subcategories, and vice versa. Unchecking a single subcategory also unchecks the parent so the submitted filter never silently includes a category the user just deselected — Transaction::Search#apply_category_filter includes every subcategory whenever a parent name is present, with no way to exclude one individually, so the parent checkbox must reflect exactly what gets submitted. Also fixes the "swipe-to-categorize" pill picker (transactions/categorizes/show.html.erb), which was still a flat alphabetical list with no parent/child indication — now grouped and labeled consistently with the rest of the app (PR #2845, #3292). Adds an :indeterminate style for .checkbox--light (only .checkbox--dark had one). Closes discussion #3149. This code was written by Claude Code (Anthropic). * fix(transactions): preserve parent-only category filters on reopen connect() derived each checkbox's parent/child state independently from server-rendered checked attributes, so a parent-only filter (e.g. an incoming link naming only the parent category) rendered the parent checked with its children unchecked — syncParentState() then read that as "some children unchecked" and cleared the parent, silently dropping the filter on the next Apply. Cascade checked parents to their children before deriving parent state so the picker matches the active query. Also makes the internal helper methods private per review feedback. * fix(transactions): eager-load category parent, fix categorize-pill filter text Addresses PR #3356 review from jjmata: - Current.family.categories.alphabetically caused an N+1 (SELECT per parent) via display_name_with_parent in the categorize-wizard pill loop. Added .includes(:parent) at all three call sites. - data-filter-name still used the bare category name while the pill label showed "Parent > Child" for subcategories, so searching by the visible parent prefix found nothing. Both now share one computed label. --------- Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com> |
||
|
|
4794d4ee15 |
fix(transfers): hide disabled accounts from new transfer modal (#3376)
Disabled accounts were already excluded from the dashboard and the new transaction modal, but the new transfer modal's from/to account selects still listed them. Add the same `.active` filter used by the transaction form to TransfersController#set_accounts. Claude-Session: https://claude.ai/code/session_01P9MCGiD5KEZGy9wB6ySuRA Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
bdc03be23a |
fix(i18n): localize recurring allocation feedback (#3380)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
acb8090dbd |
fix(i18n): localize recurring occurrence feedback (#3382)
* fix(i18n): localize recurring occurrence feedback * Address PR review feedback (#3382) - Localize every due-label state rendered by the occurrence drawer - Cover the German due labels with fallback-disabled and rendered UI assertions --------- Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
4bac6e4422 |
feat(ai): verify AI configuration and provider liveness from worker processes (#3298)
* feat: verify AI configuration and provider liveness from worker processes
Closes #3169.
The AI status page (#3145, PR #3155) proves only that the `web` process
resolved a valid-looking configuration and can reach the configured
provider from its own network context. Most AI workloads -- assistant
responses, PDF processing, embeddings, auto-categorization, and merchant
detection -- actually run in Sidekiq `worker` processes, which can differ
from `web` in environment, DNS, proxy rules, network policy, or even
loaded credentials (workload-specific overrides, an updated Secret without
a pod restart, `web` recreated without `worker`). A passing web check says
nothing about whether a worker can do the same.
## What this adds
`WorkerAiHealthCheckJob`, queued on demand from a new "Verify worker
configuration" button on System health -> AI status. It runs the same
bounded, non-destructive probes `AiHealth` already runs, but from inside
whichever Sidekiq worker process dequeues it, and records the result via
`WorkerAiHealth`: process identity (hostname:pid), checked-at time, a
non-secret configuration fingerprint (effective provider, model, redacted
endpoint, vector-store adapter/embedding config), and probe outcomes.
The AI status tab lists every recorded result -- most recent first, kept
for `WorkerAiHealth::RETENTION` (15 minutes) -- each labeled with a status
pill (`Passing` / `Failing` / `Stale`, the last once older than
`STALE_AFTER`) and a configuration pill comparing it against the web
snapshot (`Matches web` / `Differs from web`), with a failure-reason list
reusing the existing failure-code translations when a probe failed.
## Implementation constraints from the issue, addressed directly
- **Cannot reuse a web-cached probe result, or vice versa.** `AiHealth.new`
gained an injectable `probe_cache:` (default `Rails.cache`, matching
today's behavior). The worker job passes a fresh
`ActiveSupport::Cache::NullStore` instead, so every worker check is a
live call that neither reads a web-cached entry nor leaves one behind.
- **A single job only verifies one worker.** Documented on the button
(`coverage_notice`) and in the docs: with multiple replicas, a passing
result names one process, not the fleet. Queuing again samples another.
- **Never persists or displays a raw credential.** `WorkerAiHealth::Snapshot`
only carries redacted endpoints (AiHealth already redacts these before
they reach the job) and provider/model/status fields -- there is no field
for a token to occupy. A structural test asserts this stays true.
- **Failures land in both places an operator already checks.** Same
destinations as `AiHealth::Probe`'s own failures: `Rails.logger` and
`DebugLogEntry` (new `ai_health_worker` category), tagged with the
process identity.
- **Results carry a clear status**, including the `pending` case implicitly
(no result yet renders an explanatory empty state) and `stale` for a
result whose process may no longer reflect current state.
- **DB-backed vs ENV-backed settings are labeled.** A new info block next
to the worker results explains which UI settings propagate automatically
(rails-settings-cached invalidates the shared cache on write) versus
which require restarting/recreating both `web` and `worker`.
## What this deliberately doesn't do
Full-fleet coverage (every process publishing a periodic fingerprint) --
the issue lists this under "Other options to consider," not the acceptance
criteria, and Sidekiq's normal dispatch doesn't target every process
without an explicit per-process coordination mechanism. This PR implements
the on-demand, single-check design the acceptance criteria actually
describes ("An administrator can request an asynchronous worker-side
check", "does not imply full-fleet coverage"); periodic fleet-wide
publishing is a natural follow-up if operators need it.
## Testing
- `WorkerAiHealth`: recording/reading, same-process replacement vs.
cross-process coexistence, MAX_RESULTS bounding, staleness, status
derivation (failure codes / component statuses / function-calling
refusal), `matches_web?` comparison, and the credential-field structural
guard.
- `WorkerAiHealthCheckJob`: records a passing/failing snapshot naming this
process, writes failures to Rails.logger + DebugLogEntry (and only on
failure), never leaks the access token, and -- the defining property --
is proven to construct `AiHealth.new` with an isolated `NullStore`
rather than the shared web-facing cache.
- `Admin::SystemHealthController`: empty state, a rendered result with
matching/mismatched configuration, a failing result's failure reason, a
stale result, the `verify_worker_ai` action enqueuing the job and
redirecting with a flash notice, and that non-super-admins and
unauthenticated requests cannot trigger it.
I could not run the Rails test suite in this environment (no working
Ruby/Bundler toolchain available locally -- Ruby 2.6 system Ruby vs. the
project's required 3.4.9, no way to install without sudo/Docker access).
Every file was checked with `ruby -c`, YAML files with `YAML.load_file`,
and the ERB view with `ERB.new(...).src`, plus careful manual tracing of
each test against the production code paths it exercises, but CI should
be treated as the first real run of this suite per the repository's own
guidance for exactly this situation.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9494Pxh4LZKnNLTGfFXPw
* fix: correct test setup bugs found by actually running the suite
Set up a working Docker-based Rails environment (this session's shell
had no compatible Ruby/Bundler) and ran the full suite against PR #3298.
Two real bugs surfaced that static checks couldn't have caught:
- WorkerAiHealth::Snapshot.new(**{...}.merge(overrides)) needs the
double-splat -- a bare Hash isn't auto-converted to keyword arguments.
Both test snapshot builders passed a positional Hash instead, which
raised "missing keywords" for every field on every call.
- assert_enqueued_with/assert_no_enqueued_jobs need `include
ActiveJob::TestHelper` explicitly in a plain ActiveSupport::TestCase --
every other model test in this codebase that uses them does the same;
I'd wrongly assumed it was available process-wide.
With both fixed: 7510 runs, 29877 assertions, 0 failures, 0 errors, 30
skips for the full suite; rubocop, erb_lint, and brakeman all clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9494Pxh4LZKnNLTGfFXPw
* fix(worker-ai-health): address feedback on health status and cache handling
- Add checks for 'not_configured' and 'unavailable' states in Snapshot#status
- Include PDF probe failure codes in failure_codes detection
- Fix cache lifetime extension by removing expires_in and filtering expired entries in recent()
Ensures unconfigured workers and missing PDF pipelines are marked as failing,
and stale cache entries don't get indefinite TTL refreshes.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9494Pxh4LZKnNLTGfFXPw
* fix(worker-ai-health): fix stub gaps causing ci/test_unit failure
stub_ai_health in WorkerAiHealthCheckJobTest omitted
pdf_text_extraction_probe/pdf_vision_processing_probe, so
WorkerAiHealthCheckJob#failure_codes raised NoMethodError on nil.
Also fix vector_store_status to :missing (no adapter configured) rather
than :not_configured (adapter configured but unusable) to match the
scenario AiHealth actually returns and Snapshot#status's semantics.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A9494Pxh4LZKnNLTGfFXPw
* fix(worker-ai-health): compare request timeout, fix raw color class, document dev-mode cache caveat
- Add llm_request_timeout to WorkerAiHealth::Snapshot and compare it in
matches_web? (CodeRabbit) -- a worker with a different effective request
timeout than web (e.g. a workload-specific OPENAI_REQUEST_TIMEOUT override)
previously showed as "Matches web" despite a real configuration
difference, exactly the kind of drift this feature exists to catch.
- Replace border-alpha-black-25 with the border-primary functional token in
the worker result card (CodeRabbit nitpick).
- Document the dev-mode cache_store caveat jjmata flagged: bin/dev runs web
and worker as separate OS processes, and development.rb uses a
process-local memory_store/null_store, so a worker check queued locally
writes to a cache the web process never reads from -- "Verify worker
configuration" can appear to silently do nothing. Added a note to
docs/hosting/ai.md rather than changing behavior, since production's
shared Redis store is unaffected.
- Corrected the retention description in the same doc section (CodeRabbit,
most recent review): only the 5 most recently checked-in distinct
processes are retained (MAX_RESULTS), not "kept for RETENTION" -- a 6th
process checking in can evict an older entry before its own 15-minute
RETENTION window is up.
jjmata's four other findings (unconfigured-worker and missing-PDF-probe
states rendering as "Passing", and the cache-retention/TTL-extension issue)
were already fixed in
|
||
|
|
adf44ddf0d |
fix(reports): stop Safari swallowing every click on the reports page (#3365)
* fix(reports): stop Safari swallowing every click on the reports page The category rows in the reports breakdown use a stretched link whose `before:absolute before:inset-0` overlay was anchored to the parent `<tr class="relative">`. `position: relative` on a table row is left undefined by CSS 2.1 §9.3.1 and WebKit does not implement it, so in Safari the row never becomes a containing block. The overlay resolves against a far larger positioned ancestor instead, blankets the reports page, and intercepts every click — including clicks on the period picker popover — navigating to that category's transactions drill-down. Chrome and Firefox do establish the containing block, which is why this is invisible outside WebKit. Anchor the overlay to the flex wrapper inside the cell instead. A `<div>` is an unambiguous containing block in every engine. The click target narrows from the whole row to the category cell, which is still the icon, the name and the entry count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(reports): assert the stretched overlay on the cell wrapper, not the row The regression test still required `tr.relative`, which the previous commit removes, so the suite would have failed. Assert instead that the stretched link sits inside `td div.relative` — the containing block the overlay is now anchored to. Verified the selector against the rendered markup: it matches the clickable row once, does not match a non-clickable row, and the old selector matches nothing under the new markup. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef3d438686 |
feat(ai-health): explain vector store setup failures (#3324)
* feat(ai-health): explain vector store setup failures * fix(ai-health): clarify pgvector dimension recovery * fix(ai-health): prioritize pgvector probe failures |
||
|
|
01fa018b85 |
fix(i18n): localize bills page shell (#3374)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
3c14646d20 |
Localize OpenAI timeout settings (#3370)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
47c46843e1 |
fix(transfers): prevent duplicate creation on double-submit (#3342)
* fix(transfers): prevent duplicate creation on double-submit TransfersController#create -> Transfer::Creator had no protection against a repeated form submission - a double-click, a browser retry, or two near-simultaneous requests could each create a separate, identical transfer (and its 2-4 underlying Entry/Transaction rows). Adds a per-form idempotency key, the same approach already used for TransactionsController#create: a UUID hidden field generated fresh on page load, tagging the outflow/inflow (and fee, with a distinguishing suffix since a fee leg shares its account with its primary leg) entries via the existing entries(account_id, source, external_id) partial unique index. A pre-check handles the sequential double-submit case; rescue ActiveRecord::RecordNotUnique is the authoritative backstop for genuine concurrent requests - the whole Transfer.transaction block rolls back cleanly on conflict, so there's no risk of a half-created transfer. A same-day duplicate transfer can be legitimate (unlike a duplicate valuation, see #3339/PR #3340), so this uses the same per-submission token approach as #3334/PR #3338 rather than a natural-key DB constraint. Fixes #3341. Co-Authored-By: Claude <noreply@anthropic.com> * fix(transfers): store the idempotency key in its own column, isolate the retry with a savepoint Same two review findings as PR #3338 (transactions) and #3340 (valuations), applied here since this branch shares the same mechanism: - Reusing external_id/source for the web-form idempotency token made every leg of a manually-created transfer satisfy Entry#linked?, incorrectly making it look provider-synced. Uses the same dedicated entries.idempotency_key column added in db/migrate/20260902180400_add_idempotency_key_to_entries.rb (cherry-picked identically from PR #3338 - this branch depends on that migration; please merge #3338 first, or merge this after it lands so the duplicate migration file is a no-op). - Transfer::Creator now wraps the actual save in Transfer.transaction(requires_new: true) so a RecordNotUnique only rolls back to a savepoint rather than aborting any transaction the caller might already be in, keeping the rescue's retry lookup usable (mirrors the fix already applied to Account::ReconciliationManager in PR #3340). Added a regression test asserting neither leg of a transfer created via this path is linked? or has external_id/source set. Co-Authored-By: Claude <noreply@anthropic.com> * fix(transfers): distinct idempotency key per leg, rebuild invalid index on retry Two more review findings: - CodeRabbit: the form doesn't prevent selecting the same account as both source and destination. The outflow and inflow legs shared the bare idempotency key, so on that same-account path they'd collide with each other under the same account-scoped unique index (as would both fee legs, which shared a single "-fee" suffix). Every leg now gets a distinct, role-specific suffix (outflow stays bare - that's what find_existing_transfer looks up by - inflow/source_fee/ destination_fee each get their own). - Codex (same finding already fixed once for entries.idempotency_key's sibling migration, recurring here since this branch carries an identical copy): index_exists? alone doesn't distinguish a valid index from an INVALID one left behind by an interrupted CREATE INDEX CONCURRENTLY, so a retry after a failed build would short-circuit and record the migration as applied while the constraint was still missing. Now checks pg_index.indisvalid directly before deciding to skip. Co-Authored-By: Claude <noreply@anthropic.com> * fix(transfers): clear idempotency key on destroy so a retry doesn't 500 Codex flagged that Transfer#destroy! (used by reject!) preserves the outflow/inflow entries but not the Transfer join row - a retried create request with the same idempotency_key would find no Transfer via find_existing_transfer, attempt another insert, hit the stale entry's unique key, and re-raise RecordNotUnique instead of finding a match. Clear the key on the surviving entries when a transfer is destroyed. Also adds a regression test for the CodeRabbit-flagged per-leg key collision concern (already fixed by role-specific suffixes in the prior commit) to lock in that fee legs never share a key with their primary leg. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(transfers): verify idempotency key matches the request, fix stale doc comment jjmata review on #3342: - find_existing_transfer matched on idempotency_key + source_account only, so a stale key from a cached form could silently return a different, older transfer instead of creating the one actually requested. Now verifies destination account, date, and amount before treating a key match as the same request; a genuine mismatch surfaces as a new StaleIdempotencyKeyError (422 + message) instead of a false success or a raw 500. - Removed a comment claiming parity with a TransactionsController#new_transaction_idempotency_key method that doesn't exist in the codebase. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(transfers): preserve from_account_id on error, match fees/exchange rate in idempotency check coderabbitai review on #3342: - All three create rescue blocks (exchange rate unavailable, invalid date, stale idempotency key) failed to set @from_account_id, so the re-rendered form lost the user's selected source account. - matches_request? only compared accounts/date/outflow amount, so a retry with the same key but a different exchange_rate or fee would be reported as success while silently keeping the old inflow amount and fee entries. Now recomputes the request's effective inflow amount and compares derived fee totals too; a mismatch raises StaleIdempotencyKeyError like other stale-key mismatches instead of silently returning the old transfer. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
b62f6035ea |
fix(transactions): prevent duplicate creation on double-submit (#3338)
* fix(transactions): prevent duplicate creation on double-submit TransactionsController#create had no protection against a repeated form submission - a double-click, a browser retry, or two near-simultaneous requests could all create a separate identical transaction. Adds a per-form idempotency key (a UUID hidden field, generated fresh on page load) that reuses the existing entries(account_id, source, external_id) partial unique index, with a pre-check for the sequential case and a RecordNotUnique rescue as the authoritative backstop for genuine concurrent requests - the same pattern already used by mark_as_recurring and the public API's idempotency-key support. Fixes #3334. Co-Authored-By: Claude <noreply@anthropic.com> * fix(transactions): store the idempotency key in its own column, not external_id Codex review finding: reusing external_id/source for the web-form idempotency token made every manually-created transaction satisfy Entry#linked? (external_id.present?), since the form always supplies a key. That incorrectly made manual entries look provider-synced - disabling their date/nature/amount/currency fields in the editor (app/views/transactions/show.html.erb), and hiding them from future provider dedup matching (which filters to external_id: nil). Adds a dedicated entries.idempotency_key column with its own partial unique index scoped by account_id, used only for this de-duplication and with no meaning anywhere else in the app, so it can't collide with provider-linkage semantics. TransactionsController now tags/looks up entries by this column instead of source/external_id. Added a regression test asserting a transaction created via this path is not linked? and has no external_id/source set. Co-Authored-By: Claude <noreply@anthropic.com> * fix(migration): rebuild an invalid index left by an interrupted CONCURRENTLY build Codex review finding: index_exists? alone doesn't distinguish a valid index from an INVALID one left behind by an interrupted CREATE INDEX CONCURRENTLY (e.g. a deploy killed mid-build). A retry after such a failure would short-circuit on the early-return and record this migration as applied, while the actual uniqueness constraint stays missing/broken. Checks pg_index.indisvalid directly before deciding whether to skip the rebuild. Co-Authored-By: Claude <noreply@anthropic.com> * fix(transactions): rotate idempotency token on bfcache/Turbo restore, keep index removal concurrent Codex flagged that a page restored from the browser bfcache or Turbo's snapshot cache (back button, duplicated tab) keeps the already-consumed idempotency token in the hidden field. Submitting a different, edited transaction from that restored page would then match the old committed entry and silently redirect onto it instead of creating the new one. transaction_form_controller now rotates the token on turbo:before-cache so any later restore starts from a fresh, unconsumed value. Also address CodeRabbit's note that the migration's down block did a blocking DROP INDEX instead of DROP INDEX CONCURRENTLY. * fix(transactions): also rotate idempotency token on native bfcache restore CodeRabbit noted turbo:before-cache only covers Turbo's own snapshot cache, not the browser's native bfcache (e.g. a full navigation away and back, not through Turbo drive). Add a persisted-pageshow handler alongside it, and wire both through declarative data-action bindings on the form per this repo's Stimulus convention instead of manual addEventListener/connect/disconnect. * fix(transactions): fall back to manual UUID when crypto.randomUUID is unavailable crypto.randomUUID() requires a secure context, but this app's self-hosted mode is commonly reached over plain HTTP (LAN, reverse proxy without TLS). On such a deployment, calling it inside the cache-restore rotation handlers throws, leaving the stale, already-consumed idempotency token in the hidden field — a later edited resubmission would then silently match the old entry via find_duplicate_manual_entry and drop the user's edits. Build a v4 UUID manually from crypto.getRandomValues (which has no secure-context restriction) when randomUUID is missing. Also drops a stale comment reference to a MANUAL_FORM_SOURCE constant that doesn't exist anywhere in the codebase, and corrects a rescue comment that still described the old (account_id, source, external_id) index instead of the (account_id, idempotency_key) index actually backing this constraint. --------- Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
a5bb248407 |
Localize invalid Binance sync start date (#3369)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
7e26fcb478 |
feat(accounts): group split transactions in account activity feed (#3359)
The account activity tab rendered split transaction children as flat, ungrouped rows, unlike /transactions which collapses them into a parent row with indented children when "Group split transactions" is enabled. Wire the same EntriesHelper.group_split_entries logic into the account activity feed (UI::Account::ActivityDate, the actual render path since the ViewComponent refactor superseded the old accounts/show/_activity partial), sourcing split parents via the same single-query batched lookup pattern already used by TransactionsController#index. Also forward view_ctx through entries/_split_group so split children render correctly regardless of which page renders the group. Fixes we-promise/sure#3227 Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
3035a69d76 |
fix(i18n): localize Lunchflow fallback errors (U7) (#3358)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
cfb499d4ec |
fix(i18n): localize removed SSO identity recovery (U8) (#3357)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
5cd1d6aeba |
fix(i18n): localize Onchain wallet management (U7) (#3354)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
ab787eb823 |
fix(i18n): localize empty CoinStats wallet result (#3355)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
f320f44a40 |
fix(i18n): localize Onchain wallet setup (#3344)
* fix(i18n): localize Onchain wallet setup * Use idiomatic quantity tracking copy (#3344) --------- Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
0cdab9a0bc |
Add first-class Trade Republic support (#3168)
* Add Trade Republic provider integration
Introduce authenticated web and QR login, resilient account synchronization, deterministic financial imports, account discovery, and provider diagnostics. Keep login state encrypted, PINs transient, and incomplete provider responses non-destructive.
* Address Trade Republic review findings
Keep QR-authenticated sessions syncable, preserve historical holding snapshots, correct dividend direction, handle unpriced positions safely, localize repair feedback, and align provider controls with the design system.
* Add Trade Republic translations for supported locales
* Restore German Trade Republic account labels
* Resolve remaining Trade Republic review findings
* Resolve remaining Trade Republic review findings
* Address latest Trade Republic review feedback
* Refactor Trade Republic panel buttons to use DS::Button component and add integration tests
* Fix 100x money inflation and missing positions locale key in TR views
Money.new takes major units, so multiplying by 100 displayed EUR 12.34
as EUR 1234 in the holdings category cards and expense summary. Also
add the pluralized holdings.index.positions key that t(".positions")
resolves to (previously only defined at the unused holdings.positions
root level), across all 18 locales.
* fix(db): repair merge artifacts in schema and migrations
- Remove duplicated icon/progress_basis columns on goals in schema.rb
- Renumber Trade Republic migrations to unique versions (clashed with
main's 20260824120000_add_lifecycle_to_goals)
- Bump schema version to match latest migration
* Address remaining Trade Republic review feedback
* fix(trade-republic): address open PR #3168 review findings\n\n- Reject authenticated sessions without a securities account number so a\n blank account does not mark the item connected on a broken session.\n- Derive a missing trade amount from |quantity| x price, and a missing\n price from the resolved amount, without changing the signed import amount.\n- Regenerate db/schema.rb so the Trade Republic item/account tables and\n indexes are present; a fresh test database was otherwise missing the\n tables even though the migrations were marked up.\n
* fix(trade-republic): localize activity labels in ActivitiesProcessor (i18n)
* fix(trade-republic): localize activity labels in ActivitiesProcessor (i18n)
* fix(trade-republic): add activity labels i18n keys to all locale files
* Fix Trade Republic PR review follow-ups
* fix(trade-republic): i18n-aware category guard and ignore generated graphify cache
- Category matcher skipped core deposit/withdrawal labels; guard now compares
against translated values so German etc skip correctly
- Remove committed graphify-out cache and ignore dir
* Protect holdings from malformed snapshots
* Consolidate Trade Republic migrations
* Address final Trade Republic review comments
* Address final Trade Republic review comments
- Remove hard-coded category matcher (merchant keyword taxonomy) and leave Trade Republic transactions uncategorized when no structured category exists; rely on Sure rules/AI
- Revert shared ProviderImportAdapter# import_trade extra: param; handle Trade Republic trade metadata locally in ActivitiesProcessor via post-import Trade extra merge (preserve existing extra, deep_merge)
- Preserve Trade Republic product distinctions (cash, brokerage/private_markets/interest_products/crypto_wallet via portfolio categories) without collapsing account kinds
---------
Co-authored-by: Aland Baban <snow@iBananaMac.fritz.box>
|
||
|
|
ed6b8b752a |
fix(hosting): consistent provider-block visibility + fix Twelve Data toggle bug (#3333)
* fix(hosting): consistent provider-block visibility + fix Twelve Data toggle bug (#3089) T-Invest was the only provider block always rendered regardless of its checkbox state; now it follows the same pattern as every other provider (shown when tinkoff_invest or moex_public is enabled, since T-Invest also serves as a brand-logo fallback for MOEX-priced securities). Also fixes a related functional bug: unchecking every securities provider tried to clear the legacy securities_provider setting by assigning nil, but rails-settings-cached treats nil as "delete override", which silently reverted the field to its own default ("twelve_data") — re-enabling Twelve Data right after the user disabled it. Assigning "" instead persists the cleared state. Twelve Data and Yahoo Finance settings blocks can still be shown purely because they're the selected FX/exchange-rate provider even when unchecked for securities pricing; added an info notice explaining that instead of leaving it unexplained. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(hosting): address review feedback on #3333 - Drop the FX-only notice for Twelve Data/Yahoo Finance per feedback — the block staying visible while unchecked (because it's still the FX provider) doesn't need extra UI explanation. - Fix Codex finding: T-Invest settings must stay visible/manageable whenever a token is already configured, not just when tinkoff_invest or moex_public is checked. Security::Provided#import_brand_logo calls the T-Invest provider unconditionally for every non-crypto security once a token exists, regardless of price provider — hiding the field in that case would leave an active credential impossible to see, rotate, or clear through the UI. Reworded the notice to reflect the real, provider-independent reason instead of the narrower "MOEX only" framing. - Fix CodeRabbit finding: setting_test.rb's default-fallback test now isolates against SECURITIES_PROVIDER(S) env vars, and the explicit-clear test captures and restores the pre-test values instead of hardcoding a restore target. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * No overexplaining in code --------- Co-authored-by: Gerald <248542187+gfr-free@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
63b0d3f662 |
fix(i18n): localize Lunchflow account setup (#3330)
Translate the remaining provider-owned setup and status copy while reusing Accountable subtype labels before legacy English fallbacks. Cover German rendering, error handling, and pluralized English and German status summaries. Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
ce92b36351 |
feat(bills): assistant and MCP tools for bills (#3203)
* feat(bills): assistant and MCP tools for bills Last of three chunks carved out of #3083, stacked on the UI bundle. Exposes bills to the builtin assistant and to MCP clients. Everything here is gated behind preview features, so the tools are absent from tools/list until a user opts in. Seven tools: - get_bills, get_bill_details and get_paycheck_plan for reads - get_bill_audit, a deterministic review that surfaces likely duplicates, price changes, trials about to convert, upcoming renewals and long-overdue bills - create_bill, update_bill and record_bill_payment for writes Shared argument parsing, permission checks and error shapes live in BillsSupport, so every tool answers with the same {error, hint} contract the existing tools use, and a bad argument never aborts the turn. The write tools mutate financial records on a model's say-so, so they refuse rather than guess: a payment cannot exceed what its cycle still owes, a repeated settle will not quietly close next month, an unrecognized frequency is an error instead of a silent monthly default, and non-finite or negative amounts are rejected before they reach the database. The read tools say what they filtered. An empty result names the statuses that do hold matches, the paycheck plan discloses the unconfirmed series it excluded from spending headroom, and history and price-change windows report their real totals rather than letting a caller sum a truncated list. A not-found no longer returns the scoped relation's SQL, which handed any MCP client the access-control schema for the cost of a guessed id. The in-page AI helpers are not here. Smart fill and smart configuration are buttons on the bills pages, so they ship with the UI bundle along with the provider-side suggester they call. Suite 7,854 runs, 0 failures. Rubocop clean, eager loading verified. * Address the ready-review round * Reject an out-of-range audit lookback out loud * Speak the cycle remainder guard through the allocator locale |
||
|
|
276bdb298a |
fix(i18n): localize Plaid add accounts action (#3323)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
83c5948823 |
fix(i18n): localize import step labels (#3322)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
46f9ae29dd |
Refresh Plaid transactions during automatic syncs (#3320)
* Refresh Plaid transactions during scheduled sync * Refresh Plaid transactions for provider-wide sync * Refresh Plaid transactions on login sync * Document Plaid refresh follow-up sync * Contain automatic Plaid refresh failures |
||
|
|
3a928a0faf |
Add configurable OpenAI request timeout (#3304)
* Add configurable OpenAI request timeout * Address AI timeout review feedback --------- Co-authored-by: Jonathan Kaiser <jaysbeekay@users.noreply.github.com> |
||
|
|
7ffe877531 |
fix(i18n): localize admin SSO headings (#3316)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
a2b9329210 |
fix(i18n): localize budget category dates (#3315)
* fix(i18n): localize budget category dates (U5) * Address PR review feedback (#3315) Preserve abbreviated English budget months while localizing month names without relying on missing locale format keys. --------- Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
fe0d27471d |
feat(bills): the bills pages, calendar feed and in-page AI helpers (#3202)
* feat(bills): the bills pages, calendar feed and in-page AI helpers Second of three chunks carved out of #3083, stacked on the schema and domain core. This is everything a user sees and clicks. The whole surface sits behind the preview flag, so it is unreachable until someone opts in. Pages, all under one nav entry: - the pay run, a month calendar, the full bills table, and the paycheck planner - a detail drawer per bill, with payment history, price changes and cost analytics - create and edit flows for bills, subscriptions, installment plans and income The overview marks pay periods inside the month, so a weekly paycheck no longer reads as one undifferentiated month of bills. Markers appear only when income actually subdivides the month, which means monthly and undeclared income render exactly as before and there is no new setting to configure. Navigation and design system: - one preview-gated nav item shared by the desktop rail and the mobile bar - DS::Sparkline for payment-history charts, replacing raw SVG in views - status badges render through DS::Pill rather than hand-rolled spans - the suggestions panel is a disclosure that remembers being collapsed, per device, the way privacy mode and the sidebar width already do - every surface reflows to phone widths without horizontal scroll Calendar feed: a signed ICS feed per family, served sessionless by token, with a reset that revokes previously shared URLs. In-page AI helpers: smart fill on the bill form and a smart configuration proposal on an existing bill, each reading a bounded slice of charge history. Provider-side prompt assembly sits behind the existing LlmConcept interface, with an implementation for each of the two providers. These belong here rather than with the assistant tools because they are buttons on these pages and lean on the provider suggester, not on the tool registry. Suite 7,776 runs green apart from the pre-existing passkey-session flake, which passes standalone. Rubocop clean, eager loading verified. The hosting guide for the feature ships here rather than with the schema, since its instructions walk pages this PR introduces. * Render the suggested strip through DS::Disclosure The hand-rolled details pair predates the component. The card_inset variant is the same shape, so the strip now inherits the design system chrome, and the persisted-disclosure controller rides along unchanged. * Route the remaining hand-rolled chips through the design system The subscription-state chips, rule-match chips and match-reason chips become DS::Pill, with the state chips extracted to one shared partial so the drawer and the summary tab stop carrying copy-pasted markup. The AI prompt chips become DS::Button and the bills-index filter becomes DS::SearchInput, both of which this PR already uses elsewhere for the same shapes. * Fix erb_lint whitespace offenses in bills views * Address the post-ready review round * Require a writable destination account and gate the feed on preview * Reject an unresolvable declared account out loud |
||
|
|
aba8a8eb3c |
Show exact share count in the holding drawer (#3305)
* Show exact share count in the holding drawer Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YrdY9jvCUtG3GhgZ5Skx1 * Test that the holding drawer does not round the share count Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014YrdY9jvCUtG3GhgZ5Skx1 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
f7a7736e1b |
Surface probe timeout separately from LLM request timeout on System Health (#3276)
* Surface probe timeout separately from LLM request timeout in admin System Health - Expose probe_request_timeout in AiHealth (mirrors Probe#timeout) - Split the ambiguous 'Request timeout' row into 'LLM request timeout' and a new 'Health-check probe timeout' row - Forward AI_HEALTH_PROBE_TIMEOUT / AI_HEALTH_PROBE_CACHE_TTL in compose.example.yml and compose.example.ai.yml - Remove now-orphaned labels.request_timeout locale key (i18n-tasks) - Add regression test asserting both values surface distinctly * Address PR #3276 review feedback - Restore the credential-redaction assertion on the PDF probe status test to match the token the test actually configures (it had been changed to a placeholder that appears in no code path, making the assertion a no-op and silently removing its coverage). Sanitize the new distinct-timeouts test to a non-secret token and assert it is absent. - Consolidate the probe timeout into a single public AiHealth::Probe.timeout class method and have AiHealth#probe_request_timeout_value delegate to it so the two can never drift (rather than re-implementing the same ENV.fetch + positive-check logic). - Add unit coverage for Probe.timeout exercising the env var and the fallback path for missing/zero/negative/non-numeric values. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: jaysbeekay <jaysbeekay@users.noreply.github.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
686205c0ff |
feat(bills): schema and domain core for the bills subsystem (#3201)
* feat(bills): schema and domain core for the bills subsystem First of three chunks carved out of #3083. This one carries the schema and the domain layer: no bills pages, no calendar feed, no assistant tools. Nothing here is reachable from the UI yet, so it changes no user-visible behavior on its own. Schema, in a single migration with a full down: - recurrence_rules, recurring_occurrences, recurring_allocations, recurring_price_changes and recurring_match_rejections - bill columns on recurring_transactions (bill_type, payment_url, autopay, notes, anchor and end conditions, weekend adjustment, dedup scope) - the four data backfills, in their original order Domain layer: - Schedule, the pure date PORO every cadence resolves through, and FrequencyPreset for the labels - OccurrenceGenerator, Matcher, Allocator, PriceChangeDetector, Classifier, DeclaredBill, HistoryBackfiller and PaycheckPlanner - Pipeline, tying detection to generation, plus the nightly job and rake task Existing detection code changed in three places, each a bug this schema exposes: - Cleaner used a flat two-month staleness threshold, which silently retired every quarterly and annual series - SubscriptionAuditGenerator used a flat 45-day overdue threshold, meaningless at both ends of the frequency range - CashFlowWarningGenerator read one projected entry per series, which only equalled the monthly amount because every series was monthly; weekly bills were under-counted fourfold in its 30-day projection The JSON API travels with the model rather than the UI, because the status enum widens here. The API accepts only active and inactive on write; suggested, paused and ended are lifecycle states owned by detection, so the documented enum stays truthful. Uniqueness keys gain dedup_scope alongside amount, never instead of it: a series that is not price-forked carries a blank scope, so amount is what keeps two different prices apart. Suite 7,550 runs, 0 failures. Rubocop and brakeman clean. Eager loading verified, and the migration reverses and re-applies. Includes the first review round: orphan repair matches income and refuses coincidental twins, session imports persist occurrence mappings across chunks, semimonthly anchors canonicalize, classifier keywords match whole words, and the down refuses rather than failing when price-forked rows exist. * Address second review round Bound the cross-currency default allocation by the entry leftover and the occurrence remainder, matching the same-currency path. Let keyword stems carry a suffix again after the word-boundary fix silenced them. Skip an incoherent recurrence rule row instead of rolling back the whole import. Check rollback collisions per restored index so a refusal cannot land after the bills tables are dropped. Replay the closed_at test through a real second import. Preload the orphan repair associations and move the allocator errors to locale keys. * Match index NULL semantics in the rollback collision checks GROUP BY treats NULLs as equal but the restored unique indexes do not: account_id is nullable and indexed, so two accountless rows can never collide under any of them. Excluding NULL accounts keeps the guard from refusing a rollback PostgreSQL can perform. Verified live both ways: accountless duplicates roll back, a real collision still refuses. * Address maintainer review Scope the payable debt-destination subquery to the row and its family instead of scanning every account in the installation. Batch the cash flow generator remaining-amount sums into one grouped query, matching the two sibling sites. Enforce both window bounds in the after_count branch so a future-anchored plan cannot leak past the requested end date. Skip the explicit regeneration when the day column change will fire the model callback anyway. Add the missing locale entry for the allocation currency validation. |
||
|
|
33c3f9f808 | Show category hierarchy in transaction picker (#3292) | ||
|
|
f78303ebbe |
Add function-calling probe to detect tool-use support (#3255)
* feat(ai-health): name the missing function calling behind an opaque chat error The assistant reads accounts, transactions and holdings through function calls, so every chat request carries a `tools` payload. A model without function-calling support rejects it — OpenRouter answers a bare 404 — and the operator sees only that status code, with nothing pointing at the model. Both earlier attempts at this guessed from the chat-time error; the AI status page already runs live probes, so let it answer the question directly instead. `AiHealth::Probe#function_calling` asks the configured model for one trivial tool call the way the assistant asks for its own: chat completions with `tools` for OpenAI-compatible endpoints, the Responses API for hosted OpenAI, and `messages.create` with `tools` for Anthropic, carrying the same strict schema `Provider::Openai` sends. Reading it against the plain LLM probe is what makes the verdict sound rather than a guess at 404s: plain chat passing while the same request with tools fails means the model has no function calling; a response that carries no tool call means the endpoint took the tools but the model ignored them; both failing, or a timeout, stays an ordinary probe failure. The AI status card gains a Function calling (tools) row, an alert naming the fix for each of the two bad outcomes, and a failure reason. The hosting settings model field now says the assistant needs a tools-capable model and links super admins to the check. Refs #830 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWCmRQ1ry26JnhA79s1ZKH * fix(ai-health): only call a refusal a refusal, and probe the route chat takes Two findings from review of the function-calling probe. A tools request can fail for reasons that say nothing about tool support: a 429, a 500, a dropped connection, an unreadable body. Reading any non-timeout failure as `:unsupported` sent the operator hunting for a new model over a transient blip. Only a 4xx the service chose to answer with — excluding the ones that mean "not now" or "not you" — is a refusal of the tools payload; everything else stays an ordinary probe failure. The bare 404 from OpenRouter that this page exists to explain still reads as missing function calling. `Provider::Openai#supports_responses_endpoint?` is the real routing decision and `OPENAI_SUPPORTS_RESPONSES_ENDPOINT` can flip it either way, so choosing the API from "is the endpoint custom" could probe Chat Completions while chat uses Responses, or the reverse — reporting on a path the assistant never takes. Ask the provider instead, and cache the two routes under separate keys. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWCmRQ1ry26JnhA79s1ZKH * fix(ai-health): confirm a tools refusal before blaming the model A client error on the tools request can mean "your tools payload" or "your request, tools or not" — an invalid schema, a route the endpoint does not serve, a model it will not run. Splitting those on the status code alone still put a 422 from an endpoint contract on the model's account and told the operator to go find another one. The probe now confirms it: when the tools request comes back a client error, it asks again with the tools taken off. Only if that lands is the tools payload what was turned down, and the probe says so with its own failure code — provider-agnostic, and no reading of error text for the word "tool", which would only ever fit the provider it was written against. Statuses that mean "not now" or "not you" (401, 402, 403, 408, 429) never get a second ask. `AiHealth` now just reports the probe's verdict instead of inferring one from the status. The troubleshooting fix no longer points at an OpenRouter free tier: those providers commonly log prompts and completions for training, and every assistant tool call carries accounts, transactions, and holdings. It points at the model recommendations already in this doc, and says why free tiers are the wrong place to look. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DWCmRQ1ry26JnhA79s1ZKH --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
c6789a0fed |
fix(i18n): localize new chat default title (U9) (#3256)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
0a8f93cc2d |
fix: localize transaction selectors in German (U4) (#3254)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
f66053f83c |
fix(i18n): localize trade activity labels (U4) (#3282)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
9f7fd6fdef |
fix(i18n): localize SnapTrade device authorization (#3283)
Co-authored-by: Johns <19662585+Rowdy@users.noreply.github.com> |
||
|
|
bb835b9793 |
feat: Super Admins can Delete Users and Modify Families/Groups (#2868)
* Add admin family management features and tests
- Implement FamiliesController with destroy action to delete unused families.
- Add localization for success and error messages related to family deletion.
- Create FamiliesControllerTest to ensure proper functionality of family deletion.
- Update UserPolicyTest to include permissions for super admins to delete users.
- Enhance UsersControllerTest with tests for user family management, including moving users between families and creating new families.
* feat(users): enhance user management with family transfer validation and improved delete warnings
* Simplify user management actions column and combine family options
Move heavy user edit forms from table rows into a DS::Popover action
menu, add role badges to the user column, combine family migration and
creation inputs with a Stimulus controller, enable self-family
transfer for super admins, and add safety guards against demoting the
last super admin in the system.
* feat: add authentication type pills to admin user index to display SSO and local login status
* Add set password feature for local users in admin user management
- Add password field in action popover for users with local password login
- Enforce all registration password criteria (min 8 chars, mixed case, digit, special char)
- Block simultaneous family and password updates with clear error
- Show descriptive success notifications (role, password, both, family)
- Ignore password param for SSO-only users
- Add comprehensive tests for all password validation paths
* Resolve DS Drift Patrol findings and CI scan failures
- Wrap auth-type pills in DS::Tooltip instead of native title= attribute
- Add actions.manage_user key to locale and drop redundant default: fallbacks
- Fix RuboCop style offenses in Admin::UsersController
- Update Brakeman ignore entry fingerprint for Admin::UsersController#user_params
* fix: update badge query to target DS::Pill structure
* Fix DS::Tooltip misuse hiding SSO auth-type pill in admin users view
The SSO pill was passed as a block to DS::Tooltip, which caused it to
render inside the hidden div[role="tooltip"] instead of being visible.
The text: option ("SSO Provider: ...") was also silently ignored because
tooltip_content returns content (the block) over @text when a block is
given.
Fix: render the SSO/Local+SSO pill directly as visible content and pass
DS::Tooltip with no block so text: is used as the tooltip popup. An info
icon now appears next to the pill and shows the provider name on hover.
Fixes test: Admin::UsersControllerTest#test_index_renders_auth_type_pills_for_local_and_sso_users
* Remove redundant default: fallback from role pill i18n lookup
All admin.users.index.roles.{guest,member,admin,super_admin} keys are
defined in the locale file and used elsewhere in the same view without
a default:. The fallback was redundant for every valid role and would
silently mask a missing or renamed key instead of raising in
development.
Drop the default: user.role.humanize argument so that any future
missing key surfaces immediately as I18n::MissingTranslationData.
* Revert unrelated JS/schema/split churn; fix transfer_to_family! default role
- Revert 62 JS files (Biome formatter and unrelated controller changes)
- Revert db/schema.rb dump churn (no new migrations in this branch)
- Revert unrelated split transaction view changes (edit/new.html.erb)
- Fix User#transfer_to_family! role default: role: role evaluates to nil
when omitted; use explicit self.role to read model attribute
Keeps the PR focused on user/family management (~18-20 files).
* Fix last login and session count in admin user management
Store last_login_at and sessions_count directly on the users table
so they remain accurate after a user logs out.
- Add migration to add last_login_at (datetime) and sessions_count
(integer, default 0) columns to users, with backfill from sessions
- Add counter_cache: :sessions_count to Session#belongs_to :user so
the count auto-increments/decrements on session create/destroy
- Add after_create callback on Session to stamp user.last_login_at
- Update Admin::UsersController to read both values from users table
instead of aggregating Session rows (which disappear on logout)
* Fix user management PR pending CI items
* Keep test current session after sign in
* Address PR review comments for user transfers
* refactor: update user removal label to "Delete User" and standardize component attribute naming
* Address PR Review Feedback for User Management
* test: Fix families and users controller tests for user management PR
* Limit PR 2868 schema diff
* Fix PR 2868 user management CI failures
---------
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com>
Co-authored-by: sure-admin <sure-admin@splashblot.com>
Co-authored-by: Juan José Mata <juanjo.mata@gmail.com>
|
||
|
|
7176b4b521 |
fix(goals): stop the first goal being told about earmarks it has none of (#3216)
* fix(goals): stop the first goal being told about earmarks it has none of Linking an account and leaving the amount blank shows "This account will fund whatever is left after the other earmarks". On a first goal there are no other earmarks — so the sentence points at something absent, and it is where a user meets the word for the first time. The row already carries `data-earmarked-by-others`, and it is zero in that case. With the account to itself the hint now says so plainly, and "earmark" appears only where earmarks actually exist — which lets the context do the explaining instead of the vocabulary needing it. Pinned server-side rather than in JS. The branch is a ternary; the failure that matters is the form not handing over the second string, which does not raise — the value reads as undefined and the line renders empty. There is also a test that the two hints stay different, since identical copy would leave the branch doing nothing and the first-time reader back where they started. Nothing runs `test/javascript` — no npm script, no CI step — so a test there would have guarded nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * fix(goals): tell an account claimed in full from one claimed by nobody Review on #3216. The new "this account is yours alone" hint keyed off `earmarked_by_other_goals`, which sums `allocated_amount` — and a whole-account link carries nil, so it contributes zero. An account another goal already claims in full therefore looked exactly like an unclaimed one. The form promised the whole balance, and `whole_account_link_must_be_exclusive` refused the blank allocation on submit. That is worse than the sentence this PR set out to fix: it does not merely describe something absent, it describes something the save then contradicts. The row carries both readings now, because neither can be derived from the other. `whole_account_claimed_by_other_goals?` asks the question the sum cannot answer, and the two share the same row filter so the goal being edited is excluded from both. The two tests added here also move above the first `private`, same as on `define_method`, which is public regardless — but they read as a mistake sitting among the helpers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ff48acd04b |
feat(goals): offer recording a spend where the user is standing (#3215)
* feat(goals): offer recording a spend where the user is standing Adding money had a button on the goal page. Using it had none — the entry lived in the overflow menu, behind three conditions, and nowhere else. The asymmetry bit hardest at the one moment it mattered. "Record pledge" disappears once a goal is reached, so a user who hit the target and then spent some of it arrived at a page offering a single action: "Close this goal". That releases the earmark, and its own hint tells them to do it "once you have actually spent it" — asking for something the page gave them no way to say. The celebration panel now offers it beside closing, in that order, because that is the order the two happen in and closing is the one another click cannot undo. Same condition as the menu entry, which stays where it is: this is a second door, not a move. Beside, never instead. Plenty of goals are closed with nothing recorded, and the offer must not read as a step to clear first — a test pins that closing is still offered whenever it was before. The row is conditional rather than always rendered: a reserve gets neither action, and an empty flex div still carries its top margin, which would open a gap under copy that says there is nothing to do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * fix(goals): hold the spend offer to accounts the reader can reach Review on #3215. `offer_recording_a_spend?` rode on `current_balance`, which counts every linked account — private ones included. A reader backed only by somebody else's private account was shown the link, then sent to a dialog with nothing to pick and a refusal on submit. The dialog has applied this scoping since #3176; the panel that points at it had not. It goes through `backing_within` now, on the reader's own accessible accounts. The component tests gained a session for the same reason: without a reader the offer is correctly withheld, so every assertion about it was measuring the wrong thing. Also from review: the reserve's empty-row test renders the component rather than only asking its predicates. Both could stay false while the template emitted the row anyway, which is precisely the gap that test exists for — it needed `ViewComponent::TestCase` to do it. The two page tests move above the first `private`. They did run where they were — Rails' `test` macro defines methods through `define_method` from a class method, which is public regardless of the surrounding visibility, and `-n` confirms Minitest picks them up — but tests wedged between private helpers read as a mistake whether or not they behave like one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * fix(goals): close the second door to the spend dialog Review on #3215. Scoping the offer to the reader's own accounts fixed the lifecycle panel and left the overflow menu on the old condition, so the two doors to the same dialog disagreed — and the older one still had the bug the newer one was written to avoid. A reader backed only by another member's private account was shown the menu entry, opened a dialog with nothing to pick, and was refused on submit. The question moves to the goal, where both doors ask it, and the reader's accounts come from the list the controller already builds for the dialog. That also removes the component's own broader lookup: it was plucking every accessible account on each goal-show render, duplicating work done upstream in the same request. It now takes the ids in, defaulting to none — a caller that forgets them withholds the offer, which is the safe way to be wrong about a permission. A controller test pins it page-wide, since the point is that neither door may offer it; putting the old condition back makes it fail. Also from review: the panel test claimed to be scoped to the panel while selecting `section`, which DS::Card emits for every card on the page. The action row has an id now and both tests use it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3a4e92c6f8 |
feat(goals): call a reserve's amount what the rest of the app calls it (#3230)
* fix(goals): let a months-of-expenses reserve be created at all The mode could not be used from the UI. The form makes the amount field read-only in months mode — correctly, since the figure is derived — so the form submits it empty. `target_amount` is required and positive, and validations run before every save callback, so the derivation that fills it never got the chance. Creation came back 422 with "can't be blank" on a field the user is not allowed to type in. Reproduced through the controller before changing anything: response 422, no goal created. Every existing test set `target_amount` explicitly, which is why the model looked healthy — the gap was entirely on the path a user actually takes. The derivation moves to `before_validation`, where a derived value belongs: it is computed, then validated like any other. The dirty-state predicates change with it, since `will_save_change_to_*` describes a save that has not been decided on yet at that point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * test(goals): move the new tests out of the private section Review flagged them as never running. They do — Rails' `test` macro goes through `define_method` from a class method, which defines a public method whatever the surrounding visibility, and `-n` confirms Minitest picks both up. Verified before touching anything: 2 runs, 7 assertions. Moved anyway. My insertion targeted the file's last `private` rather than its first, so they landed among the helper methods, where they read as a mistake whether or not they behave like one — three separate reviewers have now stopped on it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * test(goals): put the page tests where nobody has to check they run Review on #3229. The four page tests sat between `private` and a later `public`, and every reader so far has stopped to work out whether they run. They do — Rails' `test` macro calls `define_method` from a class method, and a method defined that way is public whatever the surrounding visibility — but a test whose behaviour has to be reasoned about is a test nobody trusts. They move above the first `private`, where the question does not come up. The second `private` goes with them: everything between it and the first was already private, so it did nothing. The fixed-amount test also gained the assertion it was missing. It named the guard it was protecting and then checked only the status, so a 422 arriving for any other reason would have kept it green. It now asserts the error the form actually puts in front of the user; flipping that paragraph's condition makes it fail, which is the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * feat(goals): settle on target balance, the term this kind of app uses A reserve holds a balance rather than reaching an amount, and the page had three words for that one idea: "floor" twice, "level" seven times, and a field labelled "Target amount". The mode selector said "How the floor is set" three lines above a field called "Target amount". They are all replaced by **target balance** / **solde cible**. That is the term this kind of application uses, and it keeps the noun the rest of the page already leans on — "target" appears 55 times here. My first pass invented "Level to hold", which was internally tidy and standard nowhere: it fought 55 uses of a word that was not actually wrong. A target need not be a finish line; a target balance is one you hold. In months mode the balance also stops pretending to be a field. It is worked out from spending, so it is shown as a result with a line saying where it comes from — "Worked out when you save" on a goal that has none yet, and the balance it currently holds when editing one. That removes the `readOnly` toggle, which existed only to stop people typing into something that should not have been an input. Three smaller corrections while in the file: - `exceeds_earmark` had the actor backwards in both languages. The account does not earmark; the goal earmarks on the account. - The French `not_active` was a comma splice, where the file already uses a colon for that construction. - "se recomplète" is not standard French; a reserve "se reconstitue", and "ce qui lui manque" reads better than "son manque". A test asserts neither locale still says floor or niveau, so the three vocabularies cannot quietly come back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * fix(goals): stop a hidden field blocking the reserve it belongs to Replacing the read-only amount input with a hidden one left it `required`, and a required input is still validated by the browser while `display: none`. Submitting a months-based reserve was refused over a field the user could not see, and could not have filled in either — the reserve became impossible to create, which is the very thing the previous change set out to fix. Disabled rather than hidden, so it is barred from validation and its value stays out of the params, letting the derived figure land. The field also has to come back when there is nothing to derive from: with no spending history the model keeps whatever was typed, so the typed amount is then the only way to set a target at all. The form now asks the family that question up front and keeps the field where the answer is no. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye * fix(goals): stop the label swap deleting the required marker Review on #3230. `_money_field` puts the required-field asterisk inside the label, in a span of its own. Swapping the wording with `textContent = label` replaces every child of that label, so the asterisk went with it — and `refresh()` runs on connect, so this fired on every goal form, one-off and fixed reserve included, not only the derived-months case this PR is about. Nothing put it back for the life of the page. Only the wording changes now: the label's text node is rewritten and the span left alone. A system test covers it, because nothing short of a browser can. It fails on the old code at the first assertion, before anything is clicked, which is where the bug actually landed. Also from review: the months derivation asked for the median twice, building an IncomeStatement each time. It reads it once now — the method stays un-memoized, which is what the refresh job needs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GTNba5qE5NwzaHzbp27ye --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |