mirror of
https://github.com/we-promise/sure.git
synced 2026-07-13 05:15:19 +00:00
4dbfbf0bc85aa20e179b6c44d6d965727ea2d7d2
2788 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4dbfbf0bc8 |
fix(ds): replace invalid bg-surface-default with bg-surface (#2244)
`bg-surface-default` is not a defined design-system token — Tailwind emits no rule for it, so these surfaces render with no background at all. Five call sites were affected: - rules/index.html.erb — recent-runs table header - settings/llm_usages/show.html.erb — usage table header - settings/ai_prompts/show.html.erb — three prompt-preview boxes Replace with the canonical `bg-surface` token — the same fill the admin/users and settings/debugs table headers already use. Clears a Rule 2 (non-functional token) finding from the weekly DS drift scan. |
||
|
|
034a12f1d8 |
fix(ds): use DS::Disclosure for investment-performance expander (#2243)
The "view details" expander rendered a raw <details>/<summary>, flagged as a Rule 1 (bypassing DS components) finding in the weekly DS drift scan (#2157). Migrate it to the DS::Disclosure :inline variant — the same primitive already used for the goals archived section and provider panels. Gains the canonical focus-visible ring and motion-safe chevron rotation for free; markup, copy, and i18n keys are otherwise unchanged. |
||
|
|
de8cd86f2f |
perf(sync): scope transfer matching after account sync (#2230)
* perf(sync): scope transfer matching after account sync * fix(sync): make transfer lookup index migration reversible |
||
|
|
51f09cdade |
Fix prerelease version-bump job: add PR fallback for protected branches (#2224)
* Fix prerelease version bump workflow
* Fix prerelease version-bump job: add PR fallback for protected branches
### Motivation
- The prerelease version bump job was failing when it could not push to protected release branches, causing the workflow to error instead of progressing.
- The job needs permission and robust push logic so prerelease automation can update `.sure-version` and `charts/sure/Chart.yaml` even when direct pushes are blocked.
### Description
- Grant the `bump-pre_release-version` job `pull-requests: write` permission so it may open an automated PR when direct pushes are disallowed.
- Harden the bump step: enable `set -euo pipefail`, pass `GH_TOKEN` into the job environment, and use fully-qualified branch refs for pushes.
- Add retry + rebase logic for direct pushes and a fallback path that pushes the changes to an `automation/bump-version-after-...` branch and opens a PR with `gh pr create` when direct push attempts fail.
- Preserve existing validations that ensure `.sure-version` and `charts/sure/Chart.yaml` exist and the prerelease portion is parsed before writing the bump.
### Testing
- Verified workflow YAML parses with `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/publish.yml"); puts "YAML OK"'` and it succeeded.
- Confirmed the bump job has the expected permission with `ruby -e 'require "yaml"; workflow=YAML.load_file(".github/workflows/publish.yml"); abort("missing bump job") unless workflow.dig("jobs", "bump-pre_release-version", "permissions", "pull-requests") == "write"; puts "bump permissions OK"'` and it succeeded.
- Ran ActionLint via `go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.8 .github/workflows/publish.yml` and received no actionable errors.
- Ran `git diff --check` to ensure no whitespace/format issues and it succeeded.
* Harden pre-release bump job: validation, retries and PR fallback
### Motivation
- Make the `bump-pre_release-version` job more robust by validating prerelease version formats and failing early on unexpected inputs.
- Handle protected branches gracefully by attempting direct pushes with retries and falling back to creating a pull request when direct push is blocked.
- Ensure the workflow has the permissions required to open PRs when needed.
### Description
- Added `pull-requests: write` to the job `permissions` so the workflow can create PRs when required.
- Hardened the bump script with `set -euo pipefail` and a strict regex that validates and extracts `BASE_VERSION`, prerelease tag, and number using `BASH_REMATCH` before incrementing the prerelease counter.
- Simplified and made file reads safer by using input redirection for `tr` and improved error messages for missing or malformed version files.
- Reworked commit/push logic to set a commit message variable, attempt direct pushes with exponential backoff and detection of branch-protection errors, and when rejected create a dedicated bump branch and open a PR via `gh pr create`.
### Testing
- Validated the modified workflow YAML with a YAML linter; no syntax errors were reported.
- Executed the updated bump logic in a CI dry-run (workflow syntax validation on GitHub Actions) and the workflow file was accepted by the Actions syntax checks.
* Add manual workflow triggers and robust pre-release bump with protected-branch fallback
### Motivation
- Allow manual invocation of PR and chart CI workflows via `workflow_dispatch` for on-demand runs.
- Make the pre-release version bump process more robust and reliable when `refs/tags/v*` releases are published.
- Ensure the bump can proceed even when the target branch is protected by creating an automated pull request and running PR checks.
### Description
- Added `workflow_dispatch` to `.github/workflows/pr.yml` and `.github/workflows/chart-ci.yml` to support manual runs.
- Extended `bump-pre_release-version` job in `.github/workflows/publish.yml` to request `actions: write` and `pull-requests: write` permissions.
- Rewrote the bump script to use `set -euo pipefail` and a single regex validation to parse and increment prerelease versions (e.g. `1.2.3-alpha.4`), update `.sure-version`, and update `charts/sure/Chart.yaml` reliably.
- Improved commit and push flow by adding `GH_TOKEN`, using a consistent commit message variable, attempting direct pushes with retry and rebase, detecting branch-protection failures, and falling back to creating a bumped branch and an automated PR via `gh pr create`; the workflow also dispatches `pr.yml` and `chart-ci.yml` runs for the created branch.
### Testing
- No automated tests were run as part of this change; behavior will be exercised when the workflow creates a bump PR or when workflows are manually triggered via the new `workflow_dispatch` events.
|
||
|
|
3ccb82ef9d |
fix(imports): import Actual rows with blank payee (#2282)
* fix(imports): import Actual rows with blank payee Actual Budget exports reconciliation and starting-balance rows with a blank Payee. ActualImport mapped the row name straight from the Payee column with no fallback (unlike Import and MintImport), so a blank Payee produced a blank Entry name. Entry requires a name, and import! wraps all rows in a single transaction, so one blank-payee row failed validation and rolled back the entire import -- surfacing only a generic "Import failed" while the worker logged "done". Fall back to the Notes column (which carries text like "Reconciliation balance adjustment") and then to the default row name, matching the blank-name handling already used by the base importer and MintImport. Add a blank-payee row to the Actual fixture and regression tests covering the Notes fallback, the default fallback, and an end-to-end import that no longer fails on blank-payee rows. * ci(security): skip calendar-based brakeman Rails EOL check The scan_ruby job fails because brakeman's CheckEOLRails warns that Rails 7.2.3.1 reaches end of life on 2026-08-09. That check fires purely on the calendar -- it warns 60 days before the EOL date and escalates in confidence as the date nears (brakeman/checks/eol_check.rb) -- so it turns `bin/brakeman` (exit code 3) red on every branch and on main regardless of the code being scanned. Add config/brakeman.yml (auto-loaded by `bin/brakeman`) skipping only CheckEOLRails. CheckEOLRuby is left enabled because the current Ruby is not near end of life, so that signal is preserved. A TODO records that the skip should be removed when Sure upgrades off Rails 7.2. |
||
|
|
f25fe30a41 |
feat(ai): honor Setting.llm_provider for batch and PDF flows (#2265)
Auto-categorization, merchant detection/enhancement, and PDF/bank-statement extraction hard-coded Provider::Registry.get_provider(:openai), so selecting Anthropic (or running an Anthropic-only self-hosted install) left those operations using/missing OpenAI rather than the chosen provider. Add Provider::Registry.preferred_llm_provider, which resolves the LLM provider honoring Setting.llm_provider with a configured-provider fallback (mirroring how chat picks its provider), and route all six TODO(#2113) call sites through it: - Family::AutoCategorizer#llm_provider - Family::AutoMerchantDetector#llm_provider - ProviderMerchant::Enhancer#llm_provider - PdfImport (process_pdf + extract_bank_statement) - Assistant::Function::ImportBankStatement Provider::Anthropic already implements auto_categorize / auto_detect_merchants / enhance_provider_merchants (#1984) and process_pdf / extract_bank_statement (#1985), so no provider changes are needed — only the wiring. Closes #2113. |
||
|
|
c375b8bf5c |
feat(ai): self-host settings UI for Anthropic provider (5/5) (#1987)
* feat(ai): add Anthropic provider with chat parity (1/5)
Introduces Provider::Anthropic alongside Provider::Openai, implementing
the LlmConcept chat_response contract over the official anthropic Ruby
SDK. Batch ops, PDF, and RAG land in follow-up PRs.
- Provider::Anthropic uses Messages API for sync and streaming responses
- ChatConfig builds requests with ephemeral prompt-cache markers on the
system prompt and the last tool definition
- MessageFormatter reconstructs multi-turn history (text + tool_use +
tool_result blocks) from raw Message records, including the paired
user-role tool_result turn Anthropic requires after every tool_use
- ChatParser maps Anthropic Message into the shared ChatResponse Data
- Registry, Setting, User, Chat default model wired for ANTHROPIC_*
envs and Setting.anthropic_*; LLM_PROVIDER selects between providers
- Responder forwards raw conversation_history (Array<Message>) so
providers without hosted conversation state can rebuild context
- OpenAI provider accepts and ignores the new kwarg (no behavior change)
Tests cover provider init, model gating, MessageFormatter for all turn
shapes, ChatConfig request building (max_tokens, system cache, tool
conversion), ChatParser for text / tool_use / mixed blocks, Registry
discovery, and mocked chat_response success / error / function_request
paths. Live VCR cassettes recorded in a follow-up with a real key.
Stacked PRs: 2/5 batch ops + cost ledger, 3/5 PDF, 4/5 pgvector RAG,
5/5 settings UI + disclosure.
* fix(ai): address PR review on Anthropic provider foundation
Surface fixes raised by Codex + CodeRabbit on PR 1/5:
- Provider::Anthropic#chat_response now accepts (and ignores) a
`messages:` kwarg. Assistant::Responder passes both `messages:`
(OpenAI-shape) and `conversation_history:` (raw Message records) for
cross-provider parity, so the previous signature raised
ArgumentError on the first chat turn through the Anthropic provider.
- Provider::Anthropic#supports_model? bypasses the `claude` prefix
gate when a custom base_url is configured, mirroring the OpenAI
provider. Bedrock-shaped IDs like
`anthropic.claude-sonnet-4-5-20250929-v1:0` and
`claude-opus-4@20250514` are otherwise rejected by
Assistant::Provided#get_model_provider and the chat dies.
- Setting.anthropic_access_token is now in
EncryptedSettingFields::ENCRYPTED_FIELDS so the Anthropic API key
is encrypted at rest like every other provider secret. Previously
plaintext while siblings (openai_access_token, twelve_data_api_key,
external_assistant_token) were ciphertext.
- Chat.default_model falls back to whichever provider is actually
configured. Previously, with LLM_PROVIDER=anthropic but no
Anthropic credentials, the default model resolved to a Claude ID
that no registered provider supported, so chats failed even when
OpenAI was fully configured. Adds Provider::{Anthropic,Openai}#configured?
class methods for the readable callsite.
- Provider::Anthropic.effective_model uses
`ENV["ANTHROPIC_MODEL"].presence || Setting.anthropic_model` so the
Setting lookup is only performed when the env var is absent — the
previous `ENV.fetch(KEY, default)` evaluated the default arg
eagerly on every call.
- Provider::Anthropic::ChatConfig#anthropic_input_schema strips both
`:strict` and `"strict"` keys so JSON-decoded schemas with string
keys cannot leak the OpenAI-only flag through to Anthropic.
Test coverage added: supports_model? bypass on custom endpoints,
chat_response messages: kwarg compatibility, default_model fallback
in the three credential combinations, configured? against ENV +
Setting, strict-flag stripping for both key types, and a
`Setting.expects(:anthropic_model).never` assertion proving the
ENV-precedence test now exercises the lazy path.
All 4365 tests pass (1 pre-existing libvips env error unrelated).
* test(chat): make default_model tests resilient to ENV model overrides
CodeRabbit flagged on PR review: the new default_model tests asserted
against Provider::*::DEFAULT_MODEL, but Chat.default_model actually
returns Provider::*.effective_model.presence (which reads
OPENAI_MODEL / ANTHROPIC_MODEL from the environment). With either env
var set, the tests would fail intermittently even though routing was
correct.
- New default_model tests now assert against the provider's
effective_model directly, so they verify the routing decision
(which provider's value wins) without coupling to the constant.
- Pre-existing "creates with default model" assertions had the same
brittleness; switch them to compare against Chat.default_model so
the chosen model is whatever the env / Setting cascade resolves to.
Verified by running `ANTHROPIC_MODEL=claude-haiku-4-5 OPENAI_MODEL=gpt-4o
bin/rails test test/models/chat_test.rb` — 16 runs, 0 failures
(previously 2 pre-existing failures + 0 from the new tests).
* fix(ai): address local review on Anthropic foundation
- Provider::Anthropic#supports_pdf_processing? bypasses prefix gate for
custom endpoints, mirroring supports_model?
- Provider::Anthropic#initialize raises Error when custom_endpoint? AND
model.blank?, parity with Provider::Openai
- stream_chat_response captures partial usage on mid-stream errors and
records it via the new on_partial callback so chat_response can skip
the duplicate error row in the outer rescue
- safe_accumulated_message swallows the secondary failure when the SDK
cannot reconstruct a snapshot
- langfuse_client memoizes properly (||= instead of =) so repeated calls
don't churn Langfuse instances
- MessageFormatter sorts tool_calls by created_at then id so the
message array is deterministic across replays; skips tool_calls
missing both provider_call_id and provider_id rather than sending
`id: nil` and getting rejected by Anthropic
- Setting.anthropic_access_token default falls back through
ENV["ANTHROPIC_API_KEY"].presence (was missing .presence, so an
empty-string env value bled through)
- User#openai_configured? / #anthropic_configured? delegate to the
Provider::* class methods — single source of truth
- Assistant::Responder renames the OpenAI-shape history builder
conversation_history → openai_messages_payload so the kwarg name
matches the local method name (messages: openai_messages_payload,
conversation_history: chat_message_records)
- Assistant::Builtin stale-history comment updated to reference both
builders
Adds a streaming chat_response test using ad-hoc subclasses of the
SDK event types so the case/when dispatch matches via is_a? without
stubbing class-level === behavior.
* test(ai): add Anthropic tool_use round-trip + multi-tool turn coverage
Addresses @jjmata's "worth confirming" note on PR #1983: tool-use turns
from prior assistant messages must round-trip correctly when retrieved
from the database.
- New `ChatParser → ToolCall::Function → MessageFormatter` test walks
the full path: Anthropic response with a tool_use block →
ChatFunctionRequest → ToolCall::Function.from_function_request →
persisted on the AssistantMessage → MessageFormatter rebuild on the
next turn. Asserts the original `tool_use.id` is preserved end-to-end
as both `tool_use.id` and the paired `tool_result.tool_use_id`, and
that the original `input` hash and serialized result content survive.
- New multi-tool assistant turn test confirms two tool_use blocks on a
single assistant message render as two tool_use blocks followed by
two paired tool_result blocks in a single user-role follow-up,
matching Anthropic's required alternation.
Both tests exercise the existing PR1 code without behavior changes.
* test(ai): require "ostruct" explicitly in Anthropic provider tests
OpenStruct is moving out of Ruby's default load path (warning in 3.4+,
removed in 3.5+). Tests work today because ActiveSupport transitively
loads it, but that's incidental. Match the existing convention in
test/controllers/settings/hostings_controller_test.rb which explicitly
requires ostruct for the same reason.
* fix(ai): sanitize Langfuse warn logs, normalize tool_use.input, dedup history fetch
Addresses three open CodeRabbit findings on PR #1983.
- Provider::Anthropic Langfuse rescue branches no longer include
`e.full_message` in `Rails.logger.warn`. `full_message` bundles the
backtrace + cause chain and on some SDK error types includes the
serialized request/response payload (prompt, model output). Logs
now report `#{e.class}: #{e.message}` only. Three sites:
create_langfuse_trace, log_langfuse_generation, upsert_langfuse_trace.
Note: Provider::Openai has the same pattern (copy-pasted source) —
harmonization deferred to a follow-up cleanup PR; this commit fixes
only the Anthropic provider to keep PR scope tight.
- MessageFormatter#parse_arguments now coerces any non-Hash parsed
result to `{}`. Anthropic's Messages API requires `tool_use.input`
to be a JSON object (map); a stored ToolCall::Function record whose
arguments parse to a scalar, bool, or array (corrupt row, legacy
data, cross-provider bleed) would otherwise produce a payload the
API rejects. Normal flow stores Hash arguments end-to-end so the
fix is defensive — adds 2 tests covering scalar/array JSON strings
and non-String non-Hash inputs.
- Assistant::Responder dedups the chat-history fetch. The previous
layout fired two near-identical `chat.messages.where(...).includes(
:tool_calls).ordered` queries per LLM turn (one for the OpenAI-shape
payload, one for the raw-records kwarg). A new memoized
`complete_chat_messages` fetches once; `chat_message_records` filters
out the current message via `Array#reject`, `openai_messages_payload`
iterates the cached array unchanged. One SQL query per turn instead
of two. Memoization scope = single Responder instance (per LLM call),
so cache invalidation is not a concern.
All 4370 tests pass (1 pre-existing libvips env error unrelated).
Rubocop + brakeman clean.
* fix(ci): replace sk-ant- prefixed test placeholders
Pipelock secret scanner pattern-matches `sk-ant-*` as a real Anthropic
API key and fails the PR security-scan check. Test stubs and
ClimateControl env values used `sk-ant-test`, `sk-ant-from-setting`,
`sk-ant-x`, `sk-ant-y` as obvious placeholders, but the scanner does
not care about value entropy.
Switched to `fake-anthropic-key-*` / `fake-token-*` strings so the
scanner stops flagging them. No production code touched, no behavior
change — Provider::Anthropic still accepts any non-blank token.
* feat(ai): add Anthropic batch ops + LLM cost ledger (2/5)
Implements auto_categorize, auto_detect_merchants, and
enhance_provider_merchants on Provider::Anthropic via forced tool calls,
plus the cost-ledger plumbing they need.
- Provider::Anthropic::AutoCategorizer, AutoMerchantDetector,
ProviderMerchantEnhancer each define a single output tool whose
input_schema mirrors the desired output, then force the model to call
it via tool_choice: { type: "tool", name: ..., disable_parallel_tool_use: true }.
Anthropic guarantees the tool_use.input matches the schema, so there
is no JSON parsing fragility, no <think> tag stripping, and no
json_object/json_schema fallback ladders.
- Concerns::UsageRecorder mirrors the OpenAI sibling but persists
cache_creation_input_tokens / cache_read_input_tokens to dedicated
columns instead of metadata.
- Migration adds cache_creation_tokens, cache_read_tokens (nullable
integers) to llm_usages. OpenAI rows leave them null.
- LlmUsage::PRICING gains Claude 4.x rows (opus-4-7 $15/$75, sonnet-4-6
$3/$15, haiku-4-5 $1/$5 per MTok). infer_provider returns "anthropic"
for claude-* via the existing exact/prefix lookup.
- Provider::Anthropic#chat_response now persists cache columns directly
rather than stashing them in metadata.
- 25-transaction batch cap mirrors the OpenAI provider so the cost
ledger sees the same shape regardless of which provider ran a batch.
Tests cover the forced-tool-call path, null/None normalization,
case-insensitive merchant matching, the missing-tool_use error path,
and Anthropic-specific pricing + provider inference on LlmUsage.
Stacked on #1983 (PR 1/5). 3/5 PDF + vision next.
* fix(ai): attribute Bedrock model IDs to anthropic + clean nil enum
- LlmUsage.infer_provider now returns "anthropic" for Bedrock /
Vertex shaped IDs (anthropic.* and anthropic/*), so cost-ledger
filtering by provider stays correct even when no per-MTok rate is
stored. Previously these IDs fell through to the "openai" default.
- AutoCategorizer drops the redundant nil sentinel from the
category_name enum — the union type [string, null] already permits
null, and some JSON Schema validators reject nil literals inside
enum arrays.
* test(ai): require "ostruct" in Anthropic batch op tests
Same rationale as the PR1 ostruct fix — explicit require so the tests
don't depend on ActiveSupport's transitive load when Ruby 3.5+ removes
OpenStruct from the default load path.
* feat(ai): Anthropic native PDF processing (3/5)
Implements process_pdf and extract_bank_statement on Provider::Anthropic
using the native `document` content block — no rasterization, no text
pre-extraction.
- Provider::Anthropic::PdfProcessor classifies the document, summarizes
it, and extracts statement metadata via a forced report_document_analysis
tool whose input_schema mirrors the existing Provider::Openai output
(document_type from Import::DOCUMENT_TYPES, summary, extracted_data).
- Provider::Anthropic::BankStatementExtractor returns the same
{ transactions, period, account_holder, account_number, bank_name,
opening_balance, closing_balance } shape via report_bank_statement so
downstream pdf_import code is provider-agnostic.
- Both attach the PDF as
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: <b64> } }
— Claude 3.5+ / 4.x accept this natively (up to 32MB / 100 pages).
No pdf-reader, no pdftoppm, no chunking for typical statements.
- supports_pdf_processing? (introduced in PR 1) already returns true for
claude-* models, gating process_pdf with a clear error otherwise.
- Cost ledger rows are persisted via the shared UsageRecorder concern,
including cache_creation/cache_read tokens.
Tests verify the document block shape, tool_choice forcing, normalized
document_type for unknown classifications, transaction normalization
(date / amount / reference → notes), and the missing-tool_use error
path. Blank pdf_content raises before any client call.
Stacked on #1984 (PR 2/5). 4/5 pgvector RAG next.
* fix(ai): guard PDF size + surface bank-statement truncation
- PdfProcessor and BankStatementExtractor raise upfront when
pdf_content.bytesize exceeds MAX_PDF_BYTES (32 MB, matching
Anthropic's hard limit). Previously a 100 MB PDF would be
base64-encoded (~133 MB) and packed into the JSON body before
the API rejected it — peak heap ~270 MB per Sidekiq worker.
- BankStatementExtractor inspects response.stop_reason; when the
model hit max_tokens it logs a warning and flags result[:truncated]
so downstream callers know the transaction list may be incomplete.
- ISO date pattern added to statement_period_start/end schema in
PdfProcessor so the model can't return "March 2026" — Anthropic
enforces the regex via the tool's input_schema.
Tests cover the size guard (raises before any client.messages call),
truncated-result flagging, and the warning log path.
* test(ai): require "ostruct" in Anthropic PDF tests
Match the explicit ostruct require added in PR1/PR2 — same Ruby 3.5+
load-path reason.
* feat(ai): default Anthropic installs to pgvector RAG (4/5)
The provider-agnostic vector store stack (VectorStore::Pgvector + the
Embeddable concern) already shipped to main. This PR closes the
Anthropic loop:
- VectorStore::Registry.adapter_name now returns :pgvector when
Setting.llm_provider == "anthropic" and no explicit
VECTOR_STORE_PROVIDER override is set. Anthropic has no hosted vector
store, so falling back to the local pgvector adapter is the only
correct default. Explicit VECTOR_STORE_PROVIDER still wins.
- SearchFamilyFiles surfaces a longer message when no adapter is wired
up — calling out pgvector + EMBEDDING_URI_BASE as the supported
Anthropic-only path so the user is not stuck with an "OpenAI required"
hint that is no longer accurate.
The Embeddable concern already pulls embeddings from
EMBEDDING_URI_BASE / EMBEDDING_ACCESS_TOKEN (with OpenAI as fallback),
so Anthropic installs point this at Voyage AI, a local Ollama instance,
or OpenAI embeddings — independent of the chat provider.
Tests cover the new default routing, the existing OpenAI default
staying intact, and explicit VECTOR_STORE_PROVIDER overriding the
Anthropic default.
Stacked on #1985 (PR 3/5). 5/5 settings UI + retention disclosure next.
* feat(ai): self-host settings UI for Anthropic provider (5/5)
Adds the Anthropic panel and the install-wide LLM provider selector to
the self-hosting settings page, plus a shared data-retention
disclosure that covers both OpenAI and Anthropic.
- New _llm_provider_selector partial: select for Setting.llm_provider
(openai | anthropic), respects the LLM_PROVIDER env var (disables the
control + shows the "configured through environment variables" hint
when set, mirroring the existing OpenAI panel behaviour), and renders
a compact data-handling block with one-line retention statements for
each provider.
- New _anthropic_settings partial mirrors _openai_settings exactly:
password-field for the API key with **** redaction, optional
base_url (for AWS Bedrock / GCP Vertex), optional default model. All
three fields disable when their ENV var is set.
- show.html.erb renders provider selector + OpenAI panel + Anthropic
panel under the same "General" section so users can configure either
(or both) without switching pages.
- Settings::HostingsController#update now permits and persists
anthropic_access_token (ignoring the **** placeholder, same pattern
as OpenAI), anthropic_base_url, anthropic_model, and llm_provider
(validated against %w[openai anthropic]). On Setting::ValidationError
the rescue branch preserves anthropic_base_url / anthropic_model
input so the form re-renders with the user's typed values intact —
parity with the issue #1824 fix for OpenAI.
- Locale keys added under settings.hostings.{llm_provider_selector,
anthropic_settings}.
Tests cover token update + placeholder redaction, base_url + model
update, llm_provider switch to anthropic, and rejection of unknown
provider values. The existing GET render test still passes, exercising
all three new partials.
Closes the 5/5 Anthropic series stacked on #1986.
* fix(ai): valid Tailwind token + base_url URL validation
- Data-handling block in _llm_provider_selector swaps the invalid
bg-surface-secondary token for bg-container-inset, matching the
inset-card pattern used elsewhere in sure-design-system/components.css.
bg-surface-secondary is not defined anywhere in the design system —
Tailwind treated it as a no-op, so the block rendered with no
background contrast.
- Settings::HostingsController validates anthropic_base_url as a
URI::HTTP (catches https too) and raises Setting::ValidationError
with a localized message when the input is not parseable.
Previously any string was persisted, surfacing as an opaque
connection error at request time instead of an immediate UX failure.
- Blank base_url now clears the setting (was already the case but
exercised explicitly in tests now).
* fix(ci): replace sk-ant- prefixed token in hostings controller test
Same pipelock secret-scan trigger as PR1 fix on registry/anthropic
tests. The sk-ant-* prefix is matched verbatim by the scanner
regardless of value entropy.
* fix(ai): provision pgvector table when it is the default store
#1986 makes pgvector the default vector store for Anthropic installs, but
CreateVectorStoreChunks only ran when VECTOR_STORE_PROVIDER=pgvector was set
explicitly — so a fresh Anthropic-only install migrated without the
vector_store_chunks table and failed on uploads/searches.
Add VectorStore::Registry.pgvector_effective? as the single source of truth
for "is pgvector active?" (explicit env OR the Anthropic default), and a new
idempotent migration that enables the extension + creates the table whenever
pgvector is effective and the table is missing — covering fresh and
already-migrated installs without drift. Addresses Codex P1.
* fix(ai): provision pgvector table for Anthropic-default installs
Migration gated on raw VECTOR_STORE_PROVIDER==pgvector, so an
Anthropic-default install (which selects pgvector implicitly via
Setting.llm_provider without setting VECTOR_STORE_PROVIDER) skipped
table creation and failed later on a missing vector_store_chunks
relation. Route through VectorStore::Registry.pgvector_effective? —
the single source of truth already shared by the adapter selection.
Addresses Codex P1 review finding.
* fix(ai): provision pgvector chunks table on schema-load installs
The ensure-migration only helps db:migrate upgraders. Fresh installs go
through bin/docker-entrypoint's db:prepare, which loads schema.rb (the
conditional table can't be dumped there — it needs the vector extension)
and marks every migration applied without running it. An Anthropic-only
fresh install therefore selected the pgvector adapter but had no table,
failing with raw PG errors on first upload or search.
Two layers close it:
- VectorStore::Pgvector#ensure_schema! provisions the table idempotently
on first use (mirrors CreateVectorStoreChunks; memoized; failures wrap
in VectorStore::Error, which with_response turns into a clean failed
response).
- VectorStore::Registry#build_pgvector now gates on
VectorStore::Pgvector.available? (table exists, or extension present),
so installs whose Postgres lacks pgvector entirely degrade to the
assistant's provider_not_configured message instead of raising
mid-chat.
Also resolves the schema.rb version conflict against main (keep the
branch's 2026_06_01_120000, on top of main's current tables).
* fix(ai): address review nitpicks on pgvector provisioning
- Registry: update the adapter doc comment to mention the
Anthropic-to-pgvector default alongside the openai fallback.
- ensure_schema!: guard the DDL with if_not_exists instead of a Mutex.
Adapter instances are built per call and never shared across threads,
so the realistic race is two processes (web + Sidekiq) provisioning
concurrently; IF NOT EXISTS makes the loser a no-op where a Mutex
would only serialize threads inside one process.
* fix(ai): address review on Anthropic settings UI
- Require an Anthropic model when a custom base URL is saved, mirroring the
OpenAI branch. Auto-submit-on-blur could persist a base URL with no model,
making Provider::Anthropic raise "Model is required..." on every LLM call.
- Narrow the LLM provider selector copy: only chat honors Setting.llm_provider;
categorization, merchant detection and PDF processing still always use OpenAI.
Stop advertising provider switching for those flows until they are wired.
- Reset global Setting.* in test teardown to prevent state leakage, and add a
test covering the new base-URL-requires-model validation.
* feat(ds): conditional LLM provider settings + merged copy
The self-hosting AI section showed both providers' credential blocks at once
and duplicated near-identical copy. Tidy it:
- Replace the provider <select> with a DS::SegmentedControl driving a new
provider-settings Stimulus controller: only the active provider's panel is
shown; switching reveals the other instantly and persists Setting.llm_provider.
- Merge the two byte-identical data-retention lines into one provider-neutral
Data handling note.
- Scope the token-budget copy to OpenAI-compatible calls (read only by
Provider::Openai) and add an inline 'add a key to activate' hint when the
active provider is unconfigured.
UI-only; no provider behavior change.
* feat(ds): responsive LLM provider picker (tabs >=sm, select on mobile)
The segmented tabs overflow a phone viewport once there are 3+ providers
(measured: 4 labels want ~409px in a 319px column at 390px wide). Below sm,
fall back to a native <select> -- which doubles as the submitted field -- while
keeping the segmented tabs at sm and up.
Both controls bind to the same provider-settings Stimulus controller (the
select reads its value, the tabs read data-provider), so adding a 3rd/4th
provider scales on mobile with no layout math.
* fix(hostings): sanitize llm provider selector
* test(hostings): avoid brittle provider hint assertion
---------
Co-authored-by: sure-admin <sure-admin@splashblot.com>
|
||
|
|
3627c4d2d1 | Respect CoinStats wallet rate limits (#2251) | ||
|
|
8822eab583 |
fix(mobile): make offline transaction replay idempotent (#2232)
* fix(mobile): make offline transaction replay idempotent * test(mobile): prove offline replay id stability |
||
|
|
2fc85345c4 |
feat(mobile): show account detail context (#2231)
* feat(mobile): show account detail context * fix(mobile): show current account holdings * fix(mobile): tidy account detail review states |
||
|
|
210019a3b7 |
feat(i18n): complete Dutch (nl) locale coverage for provider & transaction views (#2218)
Several view namespaces only shipped a subset of the 14-locale baseline
(ca, de, en, es, fr, hu, nb, nl, pl, pt-BR, ro, tr, zh-CN, zh-TW), leaving
Dutch users on English fallbacks for these screens. This adds Dutch
translations for every namespace that had an English source but was missing
`nl`, bringing them to baseline parity:
- account_sharings, account_statements, splits, transfer_matches,
pending_duplicate_merges, securities, messages
- Provider connections: binance_items, brex_items, ibkr_items,
kraken_items, sophtron_items
All files mirror the en.yml key structure exactly (865 keys, verified with
key-parity and %{interpolation} placeholder checks) and reuse existing Dutch
terminology already established in the repo (account, gesynchroniseerd,
inloggegevens, verbinding, koppelen, Saldo; formal "u").
Co-authored-by: ultrahighsuper <alaricmercer@outlook.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
a33b41f98b |
feat(i18n): complete Brazilian Portuguese (pt-BR) locale coverage for provider & transaction views (#2220)
Several view namespaces only shipped a subset of the 14-locale baseline
(ca, de, en, es, fr, hu, nb, nl, pl, pt-BR, ro, tr, zh-CN, zh-TW), leaving
Brazilian Portuguese users on English fallbacks for these screens. This adds
pt-BR translations for every namespace that had an English source but was
missing pt-BR, bringing them to baseline parity:
- account_statements, splits, transfer_matches, pending_duplicate_merges,
securities, messages
- Provider connections: binance_items, brex_items, ibkr_items,
kraken_items, sophtron_items
All files mirror the en.yml key structure exactly (843 keys, verified with
key-parity and %{interpolation} placeholder checks) and reuse existing pt-BR
terminology already established in the repo (conta, sincronizado, credenciais,
conexao, vincular, Saldo; informal "voce").
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
6d27285c03 |
fix(charts): restyle hover tooltips with soft shadow + larger radius (#2029)
* fix(charts): restyle hover tooltips with soft shadow + larger radius Match the elevation pattern of DS::Select dropdown (shadow-lg + shadow-border-xs) and increase radius (rounded-lg → rounded-xl) + padding (p-2 → p-3) for better breathing. Drops the hard border in favour of the soft drop shadow. time_series_chart_controller: - p-2 → p-3, rounded-lg → rounded-xl - remove border border-secondary - add shadow-lg shadow-border-xs (theme-aware drop + border-edge) - add explicit text-primary (was missing per #2011's drift note) - add z-50 (matches goal_projection + sankey controllers) sankey_chart_controller: - bg-gray-700 text-white → bg-container text-primary (theme-aware; was broken in light mode after dark-bg flip) - p-2 rounded → p-3 rounded-xl - add shadow-lg shadow-border-xs - add font-sans for consistency with the other chart tooltips - add privacy-sensitive class (was missing — sankey money values were rendered in the clear with privacy mode on) DS::Tooltip (icon-trigger help, bg-inverse) is intentionally a different primitive and is not touched. Refs #2011 — className consolidation into a shared module is tracked separately and intentionally not closed by this PR; the new string applies to two of the three call sites today (time_series, sankey). The third (goal_projection_chart_controller) lives on the feat/goals-v2-architecture branch and will adopt the same string when goals v2 merges. * fix(charts): align every chart tooltip on the borderless soft-shadow card One visual contract for all three D3 tooltip surfaces, matching the design reference: p-4, rounded-2xl, shadow-xl, no edge ring in light mode. Dark mode keeps a 1px alpha-white ring since a shadow alone disappears against dark surfaces. - goal_projection_chart_controller drops its hand-copied class string (it still carried the old bordered recipe — the drift this util exists to prevent) and builds its two lines through the shared factory: secondary date line, tabular value line. - New content conventions exported alongside the container contract: context line = text-xs text-secondary, values = font-medium tabular-nums. Time-series and sankey adopt them. - Sankey node titles now escape before .html(); user-named categories were previously interpolated raw into the tooltip markup. * fix(charts): match the tooltip surface to the design reference exactly The previous pass approximated the reference with utility guesses (rounded-2xl, p-4, shadow-xl, dark ring). The actual spec is a hairline border ring composed with a soft 0 8px 24px drop shadow, 10px radius, 12x14 padding, and an 80ms left/top glide. Tailwind shadow utilities can't compose a ring with a custom drop shadow, so the surface moves into the design system as .chart-tooltip (theme-aware: dark swaps the ring to alpha-white and lets it carry the edge). Money/numeric figures also pick up the reference's mono treatment: font-mono + tabular-nums on every value across time-series, sankey, and goal-projection, so digits don't jitter while the scrubber moves. * fix(charts): tighten tooltip padding to 10x12 * feat(charts): give sankey and goal tooltips their missing context rows Chart tooltips answer three stacked questions: context (what am I looking at), value (how much), relation (vs what). Time-series already had all three; the other two were missing rows. - Sankey links showed a bare "$X (Y%)" with no indication of which flow was hovered. Links now lead with a swatch-dotted "Source → Target" context line; nodes get the same dot + name treatment, tying the card to the ribbon color. The context builder escapes node names centrally (they're user-named categories). - Goal projection adds a tertiary relation line — "52% of $20K target" — computed from the payload the chart already carries, for both the saved and projected segments. Hidden when the goal has no positive target. Template is i18n-wired like the existing tooltip strings (goals.show.projection.tooltip_target_relation). Verified with Playwright against the running app: all three surfaces pass computed-style and content assertions. * fix(charts): drop the color dot from sankey tooltip context The hover highlight on the diagram already identifies the ribbon; the swatch repeated it inside the card. Names alone keep the context line quieter. * fix(charts): use the app's sans money treatment in tooltips, not mono font-mono in this codebase marks code, keys, and admin surfaces; money is sans + tabular-nums everywhere else (cards, KPIs, tables). Keep the tabular figures for scrub stability, drop the mono. * fix(charts): address review — glide opt-in, no shadowed var, truncate cap - The 80ms left/top transition moved out of .chart-tooltip: it eased the snap-positioned goal tooltip but made cursor-following tooltips (sankey, time-series) trail the pointer by a frame. Goal projection opts back in via inline style; the component comment documents the split. - setRelation reuses _draw()'s targetAmount const instead of declaring a local 'target' that shadowed the target-date const. - Sankey context line gets max-w-64 so truncate has a constraint to fire against on deep flows. - Component comment now says 10x12 padding, matching the declaration. * Revert `schema.rb` changes --------- Co-authored-by: Juan José Mata <jjmata@jjmata.com> |
||
|
|
98791de851 | fix(preview): surface postgres startup failures and reset attempt timings (#2217) | ||
|
|
32c3b92d22 |
feat(mobile): add privacy-safe Sentry instrumentation (#2201)
* feat(mobile): add privacy-safe Sentry instrumentation * fix(mobile): harden auth telemetry handling * fix(mobile): harden Sentry privacy filters * fix(mobile): make auth session persistence atomic |
||
|
|
a3f31fb853 | Bump release | ||
|
|
3b4aa55516 |
test(goals): pledge-delta integration coverage (follow-up to #2178) (#2206)
* fix(goals): match manual_save pledges by contribution delta, not full balance Closes #2177 * fix(goals): clarify depository-only contribution; lowercase test names Address review: document that valuation_contribution's delta is only consumed for goal-linked Depository accounts, so the positive-delta guard is correct and no liability paydown sign case can arise. Lowercase NOT in two test names to match project convention. * test(goals): pin pledge matching through the reconciliation manager Follow-up to #2178, which matched manual_save pledges by contribution delta but only unit-tested the matcher with an injected delta. The delta derivation itself (the balances-table lookup and the nil-to-0 fallback in valuation_contribution) had no coverage, and reconciliation_manager_test never touched pledges. Two manager-level tests pin the wiring end to end: a 2000 -> 2150 reconcile matches an open 150 pledge, and a same-balance reconcile (zero delta) leaves it open, closing the <= 0 boundary. Also documents the staleness window on valuation_contribution: the prior balance reads the balances table, which recomputes async after the valuation saves, so a second same-day reconcile racing that sync derives a stale delta and self-heals on the next save. * fix(goals): derive same-day re-reconcile delta from the prior valuation The balances table recomputes asynchronously after a reconcile, so a second same-day reconcile racing that sync derived its delta from the pre-first-reconcile row. An overstated delta could wrongly close a larger open pledge — and a wrong match, unlike a miss, never self-heals. Prefer the valuation's own pre-save amount as the prior balance when the reconcile updates an existing valuation; fall back to the balances row, then 0. Same-date re-reconciles are immune to the race; only the cross-date window remains, and only in the miss direction. --------- Co-authored-by: galuis116 <galuis116@gmail.com> |
||
|
|
a3d6a7aede |
feat(ds): DS::EmptyState primitive (#2137) (#2146)
Ships DS::EmptyState — a centered icon / title / optional description /
optional action-slot state — plus a preview, tests, and an exemplar migration
of the recurring-transactions empty screen.
Consolidates the repeated `text-center py-12 + icon + title + description + CTA`
markup across the empty / no-data screens (recurring, imports, statements,
exports, and several connector setups). The audit flagged the self-hosted
feature-disabled pages rendering bare unstyled top-left text ("reads as a 500,
not a state") — wiring those through this primitive is the follow-up.
- app/components/DS/empty_state.rb: render DS::EmptyState.new(icon:, title:,
description:) with an optional `with_action` slot for the CTA.
- Migrate recurring_transactions/index empty branch.
Tests + rubocop + erb_lint clean.
Deferred: the remaining ~10 empty screens + the bare-text feature-disabled
states — same primitive, one migration each.
|
||
|
|
4e0da0c237 |
fix(ds): dark-parity for bespoke green status badges (#2142)
* fix(ds): dark-parity for bespoke green status badges (#2134) The status/type badges in recurring transactions, admin SSO providers, and the categorize group title used solid light-green tints (bg-green-50/100 + text-green-700/800) with no dark variant, so they rendered as pale "stickers" on dark. Add theme-dark variants mirroring DS::Pill's soft recipe (bg-green-tint-10 + text-green-200); dark text contrast 9.5-14.2:1. Their neutral siblings were already token-based. Audit-named _status_pill / _maturity_badge already render via DS::Pill; remaining solid-tint spots are alpha icon-containers (bg-*-500/5,/10) that composite fine on dark. Full DS::Pill consolidation tracked separately. * fix(ds): dark-parity for bespoke amber/blue notices + SSO badge (#2134) Continues the bespoke-badge tail: the raw-amber warning notices (securities SSO warning, admin SSO legacy-providers notice) + the env-configured badge, and the AI tool-call info box, were solid light tints (bg-amber-50/100 / bg-blue-50) with no dark variant -> pale "stickers" on dark. Add theme-dark variants mirroring the soft recipe: alpha-tint bg (amber/blue-500/10-15), lightened border, light text/icon (amber-200/400). Verified legible on dark. Pre-auth (sessions/new) + infra (redis error) amber notices render light-only, left as-is. Broader: these warning boxes want a shared DS notice/callout component (#2137). |
||
|
|
d845e44ff8 |
feat(ai): default Anthropic installs to pgvector RAG (4/5) (#1986)
* feat(ai): add Anthropic provider with chat parity (1/5)
Introduces Provider::Anthropic alongside Provider::Openai, implementing
the LlmConcept chat_response contract over the official anthropic Ruby
SDK. Batch ops, PDF, and RAG land in follow-up PRs.
- Provider::Anthropic uses Messages API for sync and streaming responses
- ChatConfig builds requests with ephemeral prompt-cache markers on the
system prompt and the last tool definition
- MessageFormatter reconstructs multi-turn history (text + tool_use +
tool_result blocks) from raw Message records, including the paired
user-role tool_result turn Anthropic requires after every tool_use
- ChatParser maps Anthropic Message into the shared ChatResponse Data
- Registry, Setting, User, Chat default model wired for ANTHROPIC_*
envs and Setting.anthropic_*; LLM_PROVIDER selects between providers
- Responder forwards raw conversation_history (Array<Message>) so
providers without hosted conversation state can rebuild context
- OpenAI provider accepts and ignores the new kwarg (no behavior change)
Tests cover provider init, model gating, MessageFormatter for all turn
shapes, ChatConfig request building (max_tokens, system cache, tool
conversion), ChatParser for text / tool_use / mixed blocks, Registry
discovery, and mocked chat_response success / error / function_request
paths. Live VCR cassettes recorded in a follow-up with a real key.
Stacked PRs: 2/5 batch ops + cost ledger, 3/5 PDF, 4/5 pgvector RAG,
5/5 settings UI + disclosure.
* fix(ai): address PR review on Anthropic provider foundation
Surface fixes raised by Codex + CodeRabbit on PR 1/5:
- Provider::Anthropic#chat_response now accepts (and ignores) a
`messages:` kwarg. Assistant::Responder passes both `messages:`
(OpenAI-shape) and `conversation_history:` (raw Message records) for
cross-provider parity, so the previous signature raised
ArgumentError on the first chat turn through the Anthropic provider.
- Provider::Anthropic#supports_model? bypasses the `claude` prefix
gate when a custom base_url is configured, mirroring the OpenAI
provider. Bedrock-shaped IDs like
`anthropic.claude-sonnet-4-5-20250929-v1:0` and
`claude-opus-4@20250514` are otherwise rejected by
Assistant::Provided#get_model_provider and the chat dies.
- Setting.anthropic_access_token is now in
EncryptedSettingFields::ENCRYPTED_FIELDS so the Anthropic API key
is encrypted at rest like every other provider secret. Previously
plaintext while siblings (openai_access_token, twelve_data_api_key,
external_assistant_token) were ciphertext.
- Chat.default_model falls back to whichever provider is actually
configured. Previously, with LLM_PROVIDER=anthropic but no
Anthropic credentials, the default model resolved to a Claude ID
that no registered provider supported, so chats failed even when
OpenAI was fully configured. Adds Provider::{Anthropic,Openai}#configured?
class methods for the readable callsite.
- Provider::Anthropic.effective_model uses
`ENV["ANTHROPIC_MODEL"].presence || Setting.anthropic_model` so the
Setting lookup is only performed when the env var is absent — the
previous `ENV.fetch(KEY, default)` evaluated the default arg
eagerly on every call.
- Provider::Anthropic::ChatConfig#anthropic_input_schema strips both
`:strict` and `"strict"` keys so JSON-decoded schemas with string
keys cannot leak the OpenAI-only flag through to Anthropic.
Test coverage added: supports_model? bypass on custom endpoints,
chat_response messages: kwarg compatibility, default_model fallback
in the three credential combinations, configured? against ENV +
Setting, strict-flag stripping for both key types, and a
`Setting.expects(:anthropic_model).never` assertion proving the
ENV-precedence test now exercises the lazy path.
All 4365 tests pass (1 pre-existing libvips env error unrelated).
* test(chat): make default_model tests resilient to ENV model overrides
CodeRabbit flagged on PR review: the new default_model tests asserted
against Provider::*::DEFAULT_MODEL, but Chat.default_model actually
returns Provider::*.effective_model.presence (which reads
OPENAI_MODEL / ANTHROPIC_MODEL from the environment). With either env
var set, the tests would fail intermittently even though routing was
correct.
- New default_model tests now assert against the provider's
effective_model directly, so they verify the routing decision
(which provider's value wins) without coupling to the constant.
- Pre-existing "creates with default model" assertions had the same
brittleness; switch them to compare against Chat.default_model so
the chosen model is whatever the env / Setting cascade resolves to.
Verified by running `ANTHROPIC_MODEL=claude-haiku-4-5 OPENAI_MODEL=gpt-4o
bin/rails test test/models/chat_test.rb` — 16 runs, 0 failures
(previously 2 pre-existing failures + 0 from the new tests).
* fix(ai): address local review on Anthropic foundation
- Provider::Anthropic#supports_pdf_processing? bypasses prefix gate for
custom endpoints, mirroring supports_model?
- Provider::Anthropic#initialize raises Error when custom_endpoint? AND
model.blank?, parity with Provider::Openai
- stream_chat_response captures partial usage on mid-stream errors and
records it via the new on_partial callback so chat_response can skip
the duplicate error row in the outer rescue
- safe_accumulated_message swallows the secondary failure when the SDK
cannot reconstruct a snapshot
- langfuse_client memoizes properly (||= instead of =) so repeated calls
don't churn Langfuse instances
- MessageFormatter sorts tool_calls by created_at then id so the
message array is deterministic across replays; skips tool_calls
missing both provider_call_id and provider_id rather than sending
`id: nil` and getting rejected by Anthropic
- Setting.anthropic_access_token default falls back through
ENV["ANTHROPIC_API_KEY"].presence (was missing .presence, so an
empty-string env value bled through)
- User#openai_configured? / #anthropic_configured? delegate to the
Provider::* class methods — single source of truth
- Assistant::Responder renames the OpenAI-shape history builder
conversation_history → openai_messages_payload so the kwarg name
matches the local method name (messages: openai_messages_payload,
conversation_history: chat_message_records)
- Assistant::Builtin stale-history comment updated to reference both
builders
Adds a streaming chat_response test using ad-hoc subclasses of the
SDK event types so the case/when dispatch matches via is_a? without
stubbing class-level === behavior.
* test(ai): add Anthropic tool_use round-trip + multi-tool turn coverage
Addresses @jjmata's "worth confirming" note on PR #1983: tool-use turns
from prior assistant messages must round-trip correctly when retrieved
from the database.
- New `ChatParser → ToolCall::Function → MessageFormatter` test walks
the full path: Anthropic response with a tool_use block →
ChatFunctionRequest → ToolCall::Function.from_function_request →
persisted on the AssistantMessage → MessageFormatter rebuild on the
next turn. Asserts the original `tool_use.id` is preserved end-to-end
as both `tool_use.id` and the paired `tool_result.tool_use_id`, and
that the original `input` hash and serialized result content survive.
- New multi-tool assistant turn test confirms two tool_use blocks on a
single assistant message render as two tool_use blocks followed by
two paired tool_result blocks in a single user-role follow-up,
matching Anthropic's required alternation.
Both tests exercise the existing PR1 code without behavior changes.
* test(ai): require "ostruct" explicitly in Anthropic provider tests
OpenStruct is moving out of Ruby's default load path (warning in 3.4+,
removed in 3.5+). Tests work today because ActiveSupport transitively
loads it, but that's incidental. Match the existing convention in
test/controllers/settings/hostings_controller_test.rb which explicitly
requires ostruct for the same reason.
* fix(ai): sanitize Langfuse warn logs, normalize tool_use.input, dedup history fetch
Addresses three open CodeRabbit findings on PR #1983.
- Provider::Anthropic Langfuse rescue branches no longer include
`e.full_message` in `Rails.logger.warn`. `full_message` bundles the
backtrace + cause chain and on some SDK error types includes the
serialized request/response payload (prompt, model output). Logs
now report `#{e.class}: #{e.message}` only. Three sites:
create_langfuse_trace, log_langfuse_generation, upsert_langfuse_trace.
Note: Provider::Openai has the same pattern (copy-pasted source) —
harmonization deferred to a follow-up cleanup PR; this commit fixes
only the Anthropic provider to keep PR scope tight.
- MessageFormatter#parse_arguments now coerces any non-Hash parsed
result to `{}`. Anthropic's Messages API requires `tool_use.input`
to be a JSON object (map); a stored ToolCall::Function record whose
arguments parse to a scalar, bool, or array (corrupt row, legacy
data, cross-provider bleed) would otherwise produce a payload the
API rejects. Normal flow stores Hash arguments end-to-end so the
fix is defensive — adds 2 tests covering scalar/array JSON strings
and non-String non-Hash inputs.
- Assistant::Responder dedups the chat-history fetch. The previous
layout fired two near-identical `chat.messages.where(...).includes(
:tool_calls).ordered` queries per LLM turn (one for the OpenAI-shape
payload, one for the raw-records kwarg). A new memoized
`complete_chat_messages` fetches once; `chat_message_records` filters
out the current message via `Array#reject`, `openai_messages_payload`
iterates the cached array unchanged. One SQL query per turn instead
of two. Memoization scope = single Responder instance (per LLM call),
so cache invalidation is not a concern.
All 4370 tests pass (1 pre-existing libvips env error unrelated).
Rubocop + brakeman clean.
* fix(ci): replace sk-ant- prefixed test placeholders
Pipelock secret scanner pattern-matches `sk-ant-*` as a real Anthropic
API key and fails the PR security-scan check. Test stubs and
ClimateControl env values used `sk-ant-test`, `sk-ant-from-setting`,
`sk-ant-x`, `sk-ant-y` as obvious placeholders, but the scanner does
not care about value entropy.
Switched to `fake-anthropic-key-*` / `fake-token-*` strings so the
scanner stops flagging them. No production code touched, no behavior
change — Provider::Anthropic still accepts any non-blank token.
* feat(ai): add Anthropic batch ops + LLM cost ledger (2/5)
Implements auto_categorize, auto_detect_merchants, and
enhance_provider_merchants on Provider::Anthropic via forced tool calls,
plus the cost-ledger plumbing they need.
- Provider::Anthropic::AutoCategorizer, AutoMerchantDetector,
ProviderMerchantEnhancer each define a single output tool whose
input_schema mirrors the desired output, then force the model to call
it via tool_choice: { type: "tool", name: ..., disable_parallel_tool_use: true }.
Anthropic guarantees the tool_use.input matches the schema, so there
is no JSON parsing fragility, no <think> tag stripping, and no
json_object/json_schema fallback ladders.
- Concerns::UsageRecorder mirrors the OpenAI sibling but persists
cache_creation_input_tokens / cache_read_input_tokens to dedicated
columns instead of metadata.
- Migration adds cache_creation_tokens, cache_read_tokens (nullable
integers) to llm_usages. OpenAI rows leave them null.
- LlmUsage::PRICING gains Claude 4.x rows (opus-4-7 $15/$75, sonnet-4-6
$3/$15, haiku-4-5 $1/$5 per MTok). infer_provider returns "anthropic"
for claude-* via the existing exact/prefix lookup.
- Provider::Anthropic#chat_response now persists cache columns directly
rather than stashing them in metadata.
- 25-transaction batch cap mirrors the OpenAI provider so the cost
ledger sees the same shape regardless of which provider ran a batch.
Tests cover the forced-tool-call path, null/None normalization,
case-insensitive merchant matching, the missing-tool_use error path,
and Anthropic-specific pricing + provider inference on LlmUsage.
Stacked on #1983 (PR 1/5). 3/5 PDF + vision next.
* fix(ai): attribute Bedrock model IDs to anthropic + clean nil enum
- LlmUsage.infer_provider now returns "anthropic" for Bedrock /
Vertex shaped IDs (anthropic.* and anthropic/*), so cost-ledger
filtering by provider stays correct even when no per-MTok rate is
stored. Previously these IDs fell through to the "openai" default.
- AutoCategorizer drops the redundant nil sentinel from the
category_name enum — the union type [string, null] already permits
null, and some JSON Schema validators reject nil literals inside
enum arrays.
* test(ai): require "ostruct" in Anthropic batch op tests
Same rationale as the PR1 ostruct fix — explicit require so the tests
don't depend on ActiveSupport's transitive load when Ruby 3.5+ removes
OpenStruct from the default load path.
* feat(ai): Anthropic native PDF processing (3/5)
Implements process_pdf and extract_bank_statement on Provider::Anthropic
using the native `document` content block — no rasterization, no text
pre-extraction.
- Provider::Anthropic::PdfProcessor classifies the document, summarizes
it, and extracts statement metadata via a forced report_document_analysis
tool whose input_schema mirrors the existing Provider::Openai output
(document_type from Import::DOCUMENT_TYPES, summary, extracted_data).
- Provider::Anthropic::BankStatementExtractor returns the same
{ transactions, period, account_holder, account_number, bank_name,
opening_balance, closing_balance } shape via report_bank_statement so
downstream pdf_import code is provider-agnostic.
- Both attach the PDF as
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: <b64> } }
— Claude 3.5+ / 4.x accept this natively (up to 32MB / 100 pages).
No pdf-reader, no pdftoppm, no chunking for typical statements.
- supports_pdf_processing? (introduced in PR 1) already returns true for
claude-* models, gating process_pdf with a clear error otherwise.
- Cost ledger rows are persisted via the shared UsageRecorder concern,
including cache_creation/cache_read tokens.
Tests verify the document block shape, tool_choice forcing, normalized
document_type for unknown classifications, transaction normalization
(date / amount / reference → notes), and the missing-tool_use error
path. Blank pdf_content raises before any client call.
Stacked on #1984 (PR 2/5). 4/5 pgvector RAG next.
* fix(ai): guard PDF size + surface bank-statement truncation
- PdfProcessor and BankStatementExtractor raise upfront when
pdf_content.bytesize exceeds MAX_PDF_BYTES (32 MB, matching
Anthropic's hard limit). Previously a 100 MB PDF would be
base64-encoded (~133 MB) and packed into the JSON body before
the API rejected it — peak heap ~270 MB per Sidekiq worker.
- BankStatementExtractor inspects response.stop_reason; when the
model hit max_tokens it logs a warning and flags result[:truncated]
so downstream callers know the transaction list may be incomplete.
- ISO date pattern added to statement_period_start/end schema in
PdfProcessor so the model can't return "March 2026" — Anthropic
enforces the regex via the tool's input_schema.
Tests cover the size guard (raises before any client.messages call),
truncated-result flagging, and the warning log path.
* test(ai): require "ostruct" in Anthropic PDF tests
Match the explicit ostruct require added in PR1/PR2 — same Ruby 3.5+
load-path reason.
* feat(ai): default Anthropic installs to pgvector RAG (4/5)
The provider-agnostic vector store stack (VectorStore::Pgvector + the
Embeddable concern) already shipped to main. This PR closes the
Anthropic loop:
- VectorStore::Registry.adapter_name now returns :pgvector when
Setting.llm_provider == "anthropic" and no explicit
VECTOR_STORE_PROVIDER override is set. Anthropic has no hosted vector
store, so falling back to the local pgvector adapter is the only
correct default. Explicit VECTOR_STORE_PROVIDER still wins.
- SearchFamilyFiles surfaces a longer message when no adapter is wired
up — calling out pgvector + EMBEDDING_URI_BASE as the supported
Anthropic-only path so the user is not stuck with an "OpenAI required"
hint that is no longer accurate.
The Embeddable concern already pulls embeddings from
EMBEDDING_URI_BASE / EMBEDDING_ACCESS_TOKEN (with OpenAI as fallback),
so Anthropic installs point this at Voyage AI, a local Ollama instance,
or OpenAI embeddings — independent of the chat provider.
Tests cover the new default routing, the existing OpenAI default
staying intact, and explicit VECTOR_STORE_PROVIDER overriding the
Anthropic default.
Stacked on #1985 (PR 3/5). 5/5 settings UI + retention disclosure next.
* fix(ai): provision pgvector table when it is the default store
#1986 makes pgvector the default vector store for Anthropic installs, but
CreateVectorStoreChunks only ran when VECTOR_STORE_PROVIDER=pgvector was set
explicitly — so a fresh Anthropic-only install migrated without the
vector_store_chunks table and failed on uploads/searches.
Add VectorStore::Registry.pgvector_effective? as the single source of truth
for "is pgvector active?" (explicit env OR the Anthropic default), and a new
idempotent migration that enables the extension + creates the table whenever
pgvector is effective and the table is missing — covering fresh and
already-migrated installs without drift. Addresses Codex P1.
* fix(ai): provision pgvector table for Anthropic-default installs
Migration gated on raw VECTOR_STORE_PROVIDER==pgvector, so an
Anthropic-default install (which selects pgvector implicitly via
Setting.llm_provider without setting VECTOR_STORE_PROVIDER) skipped
table creation and failed later on a missing vector_store_chunks
relation. Route through VectorStore::Registry.pgvector_effective? —
the single source of truth already shared by the adapter selection.
Addresses Codex P1 review finding.
* fix(ai): provision pgvector chunks table on schema-load installs
The ensure-migration only helps db:migrate upgraders. Fresh installs go
through bin/docker-entrypoint's db:prepare, which loads schema.rb (the
conditional table can't be dumped there — it needs the vector extension)
and marks every migration applied without running it. An Anthropic-only
fresh install therefore selected the pgvector adapter but had no table,
failing with raw PG errors on first upload or search.
Two layers close it:
- VectorStore::Pgvector#ensure_schema! provisions the table idempotently
on first use (mirrors CreateVectorStoreChunks; memoized; failures wrap
in VectorStore::Error, which with_response turns into a clean failed
response).
- VectorStore::Registry#build_pgvector now gates on
VectorStore::Pgvector.available? (table exists, or extension present),
so installs whose Postgres lacks pgvector entirely degrade to the
assistant's provider_not_configured message instead of raising
mid-chat.
Also resolves the schema.rb version conflict against main (keep the
branch's 2026_06_01_120000, on top of main's current tables).
* fix(ai): address review nitpicks on pgvector provisioning
- Registry: update the adapter doc comment to mention the
Anthropic-to-pgvector default alongside the openai fallback.
- ensure_schema!: guard the DDL with if_not_exists instead of a Mutex.
Adapter instances are built per call and never shared across threads,
so the realistic race is two processes (web + Sidekiq) provisioning
concurrently; IF NOT EXISTS makes the loser a no-op where a Mutex
would only serialize threads inside one process.
|
||
|
|
8a38a82c93 |
fix: Savings Goal Marked Reached While Still Short (#2180)
* fix: saving goal is marke while several states * feat(enable_banking): support MFA/decoupled banks and harden session handling (#2174) Decoupled/MFA banks (e.g. VR Bank in Holstein) were hard-blocked because the authorize flow aborted whenever auth_methods[0] was DECOUPLED. Enable Banking's hosted /auth page actually coordinates decoupled SCA and redirects back with a code, so route these banks through it instead: - Provider#start_authorization accepts and forwards an auth_method param - EnableBankingItem#select_auth_method picks the best method (REDIRECT > DECOUPLED > EMBEDDED), filtering by psu_type and skipping hidden methods - Shared begin_authorization! re-fetches ASPSP metadata on each authorize and reauthorize, so the method is always re-derived (no persistence required) - Remove the DECOUPLED block in the controller Also stop the integration from constantly reporting "session expired": - Only a session-level GET /sessions 401/404 flips the connection to requires_update; per-account 401/404 are retried and no longer kill the whole connection - Reconcile session_expires_at from the API's access.valid_until on every sync - Treat an expired session as a graceful requires_update state instead of raising a bare error No schema changes. Adds covering tests. * fix: request change reach --------- Co-authored-by: Tobias Rahloff <rahloff@gmail.com> |
||
|
|
1448128762 |
perf(reports): collapse investment flow aggregates (#2190)
* perf(reports): collapse investment flow aggregates * fix(reports): avoid shared-account flow duplication |
||
|
|
a0f5e56668 |
perf(transactions): preload new form options (#2189)
* perf(transactions): preload new form options * refactor(transactions): reuse new form account scope |
||
|
|
a461ff97bb |
fix(sync-toast): morph refresh, defer behind modals, DS conformance (#2105)
* fix(sync-toast): morph refresh, defer behind modals, DS conformance Follow-up to #1964 (addresses #2071). - Refresh via Turbo morph visit instead of window.location.reload, so scroll position and data-turbo-permanent elements (the AI chat panel) survive and there is no white flash. - Defer the toast while a <dialog> is open and reveal it on close. A refresh mid-modal closes the dialog and discards its in-progress input, which is the exact data loss this toast exists to prevent. Handles stacked modals. - Refresh CTA and close button now use DS::Button (secondary / icon). The close is always visible, inside the card, focusable, and has an aria-label; the old hover-only corner chip was unreachable on touch and not keyboard-focusable. - Add role="status" / aria-live="polite" to the toast. - Fix icon color: "inverse" is not a key in the icon helper color map, so it silently rendered no color class (dark icon on bg-info). Use "white", which maps to the functional text-inverse token. - Tighten copy: "New data available" / "Refresh". - Sync the broadcast comment with the actual replace/morph behavior. * fix(sync-toast): detach deferred dialog listener on disconnect A toast replaced by a newer broadcast_replace_to while a <dialog> was open kept its 'close' listener attached, so the detached controller fired #reveal()/#arm() when the dialog closed — a spurious auto-refresh from a stale toast (and repeated syncs could queue several). Store the dialog + handler refs and remove the listener in disconnect(). Flagged by codex + coderabbit on #2105. * fix(sync-toast): re-check interaction and dialogs at refresh-fire time The interaction check ran once at arm time but the refresh fired two seconds later. The post-dialog reveal made that window matter: the user closes a dialog sitting on a form, resumes typing, and the timer morphs the page — wiping non-turbo-permanent input, the exact data-loss class this toast exists to prevent. A dialog opened during the window had the mirror problem (the refresh would close it). Bail inside the callback instead, leaving the toast visible for a manual refresh, matching the mid-form behavior. Also documents the dialog-removed-without-close edge on the deferred listener. |
||
|
|
7a0665a8f6 |
fix(ds): one height rail for icon and text buttons (#2202)
* fix(ds): put icon buttons on the text-button height rail Icon-only DS::Button containers were 32/44/48px squares while text buttons of the same nominal size render ~28/36/48px tall, so every mixed header row (icon menu trigger next to text buttons — the transactions index, account pages, the app header) sat misaligned. - buttonish SIZES: icon containers now share the text rail (sm w-7, md w-9, lg w-12 unchanged). - DS::Popover's hand-rolled w-11 trigger joins the rail at w-9. - The layout's hand-rolled privacy toggle (mobile + desktop) matches the md icon-button chrome: w-9, rounded-lg, container-inset hover — it sat at w-8 with a different hover next to a DS icon button. - DS::Select's panel adopts shadow-border-lg, the elevation Menu and Popover already use, replacing the weaker shadow-lg+border-xs combo. Measured on the transactions header after the change: 36/38/36px. * fix(ds): keep the 44px touch target on coarse pointers The height rail trades icon-button size for row alignment, which is a pointer-precision tradeoff: WCAG 2.5.5's 44x44 minimum is about fingers, not mice. sm/md icon containers (and the two off-rail consumers: the popover trigger and the layout privacy toggles) gain pointer-coarse:w-11/h-11, so touch devices keep the full target while fine-pointer layouts get the aligned 36px row. Measured via Playwright: desktop 36x36, iPhone emulation 44x44 on both the menu trigger and the privacy toggle. |
||
|
|
0fe81cefe6 |
feat(ds): DS::SegmentedControl — fix invisible dark selected pill (#2145)
* feat(ds): DS::SegmentedControl; fix invisible dark selected pill (#2137) Ships DS::SegmentedControl — a single-select pill group (filters, mode switches) — plus a Lookbook preview, tests, and an exemplar migration of the budget filter tabs. The audit flagged the dark selected pill as invisible: the bespoke controls paired a container-inset track (gray-800) with a gray-700 active pill, barely a step apart. The primitive uses the DS tab-token values (tab-bg-group -> a near-black alpha-black-700 track in dark), against which the gray-700 active pill clearly reads. Values are inlined with @variant theme-dark because @apply-ing the custom tab utilities drops their dark override. - app/components/DS/segmented_control.rb: slot-based with_segment(label, active:, href:, **opts); link or button; full_width: for equal footprint; selected style isolated in .segmented-control__segment--active so a controller can toggle it as one class. - .segmented-control recipe in components.css. - Migrate budgets/_budget_tabs; budget_filter_controller now toggles the single --active class instead of five raw utility classes. Verified in-browser: dark active pill reads (gray-700 on near-black track); filter toggle still works. Tests + rubocop clean. Deferred (follow-up): auth sign-in/up switch, transaction-type tabs, and other bespoke segmented controls — same primitive, one migration each. * fix(a11y): expose segmented-control selection + derive active from filter param - DS::SegmentedControl: set aria-current (links) / aria-pressed (buttons) from the segment's active state so screen readers announce the selection. - budget_filter_controller: mirror aria-pressed when it toggles the active class. - _budget_tabs: compute each segment's initial active: from params[:filter] so a ?filter=over_budget request server-renders the correct pill (no flash before Stimulus runs). Addresses CodeRabbit reviews on #2145. * fix(ds): close unterminated segmented-control CSS rule The --active rule swallowed the table-scroll block's comment opener and never closed, so the Tailwind build died with 'Missing closing } at @layer components' and both test jobs failed at boot. Restore the closing brace and the /* opener. Also add the budgets.show.filter.aria_label locale key the budget tabs view referenced only through its inline default. --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
f7be206c55 |
fix(mobile): redact sensitive diagnostic logs (#2199)
* fix(mobile): redact sensitive diagnostic logs * fix(mobile): tighten diagnostic log redaction * fix(mobile): harden sanitized diagnostics * fix(mobile): clarify offline fallback diagnostics * fix(mobile): refine diagnostic log redaction * fix(mobile): tighten diagnostic log redaction * fix(mobile): harden auth diagnostic failures |
||
|
|
cf60e2c1d6 |
chore(ci): finish Node 24 GitHub Actions migration (#2221)
* chore(ci): finish Node 24 GitHub Actions migration * chore(ci): update preview security check for github-script v8 |
||
|
|
94422955f8 |
feat(merchants): add raw data import (csv) for merchants (#1992)
* feat(merchants): add csv import endpoint for merchants * docs: update endpoint docs * fix(merchant): recommended ai fixes |
||
|
|
d88d6e9e58 |
fix(ds): canonical transaction-row — stop category pills truncating (#2147)
* fix(ds): canonical transaction-row — stop category pills truncating (#2137) The desktop transaction grid gave the name column col-span-8 (67%, usually half-empty) and the category column only col-span-2 (17%), so the category pill's name area was clamped to ~44px and ellipsized nearly every value ("Misc...", "Shop...", "Rest...") despite the empty space the audit flagged. - Rebalance the lg:grid-cols-12 row: name col-span-8 -> 7, category 2 -> 3 (amount unchanged; still 12). Category gains 50% width from the over-wide name column. - categories/_badge: the pill was `flex w-full` (stretched to fill the column); make it `inline-flex max-w-full` so it hugs its content and short names render fully, capping + truncating only when genuinely too long. Verified on /transactions: visible-row category truncation dropped 7/7 -> 2/7 even in the compressed (AI-panel-open) view; Payment / Shopping / Restaurants now render in full. Fixes both the interactive categories/menu and the static transfer categories/badge (menu reuses the badge partial). * feat(ds): lift categories/_badge onto DS::Pill Addresses the Drift Patrol finding (and jjmata's request) properly instead of patching the bespoke span: the badge becomes a DS::Pill in badge mode. The pill primitive grows the three capabilities the badge needed and the bot's one-liner glossed over: - truncate: pills that may shrink inside min-w-0 columns drop their shrink-0/whitespace-nowrap and ellipsize the label instead of overflowing (the transaction-row category cell this PR fixes). - label_testid: stamps data-testid on the label span; five test files target [data-testid='category-name']. - icon_size: passthrough to the icon helper (badge keeps its established sm glyph; default stays xs). custom_color was already the sanctioned escape hatch for user-chosen hues. Owned visual deltas from pill standardization: px-1.5/py-1 -> px-2/py-0.5, gap-1 -> gap-1.5, border mix 10% -> 20%; bg mix, hex text, radius, text scale, icon size, truncation and testid are parity. Label wrapper only renders when truncate/label_testid ask for it, so existing pill DOM (and the find("span", text:) assertions on it) is unchanged. Four new pill tests + a Lookbook case cover the recipe. |
||
|
|
5d0eb7f445 |
fix(goals): UI polish — submit validation, container-responsive cards, picker & filter fixes (#2160)
* fix(ds): disabled buttons use not-allowed cursor
Tailwind v4 preflight sets cursor:pointer on every <button>, including
disabled ones. Add disabled:cursor-not-allowed to the DS button base so
disabled buttons read as non-interactive.
* fix(goals): disable new-goal submit until required fields valid
The submit button was always enabled, and the funding-accounts checkbox
group has no native 'required', so a goal with no linked account could be
submitted and only fail server-side (422). Disable the submit until name,
a positive target amount, and >=1 account are present, and add a
submit-time guard that surfaces inline errors and focuses the first
offending field as a backstop.
* fix(goals): keep color/icon picker from shifting the form
DS::Disclosure wraps its body in an mt-2 div that sits in normal flow, so
opening the absolutely-positioned picker popover nudged the form down ~8px.
Use a raw <details> for the picker so the popover overlays without
reflowing the content beneath it.
* fix(goals): container-responsive cards, meta-line status, scrollable tabs
- Card grids + KPI strip switch from viewport breakpoints to container
queries (@container), so columns track the actual content width when the
account/AI sidebars are open instead of crushing three columns into a
narrow center.
- Move the status pill from the title row to the meta line; the title now
gets the full header width (no more 'Hou...' truncation at 3-up). Drop
the secondary line when the pill already states it (completed / open).
- Card internals harden for narrow widths (ring shrink-0, footer wrap,
shorter 'N days left' secondary).
- Filter tabs scroll horizontally instead of wrapping 'On track' mid-label
or forcing the row wider than the viewport.
* fix(goals): only require a funding account on the create form
GoalsController#edit renders account checkboxes from visible accounts only,
so a goal backed solely by a now-hidden (disabled / pending_deletion)
account renders none checked. The submit-validation then wedged the edit
form — a name/notes change couldn't be saved even though #update preserves
existing links when account_ids is omitted. Gate the account requirement on
a require-account Stimulus value that is true only for the create form.
* fix(goals): give the color/icon picker trigger an accessible name
The hand-rolled <summary> is icon-only (pen), so screen readers announced an
unlabeled control. Add a localized aria-label.
* fix(goals): render form validation errors inline on server re-render
The client-side controller already surfaces name / amount / account errors
inline and disables submit, but a server 422 (JS disabled, or a race that
lets an invalid submit through) fell back to the top run-on banner
("X must be filled and Y and Z."). Make the existing inline error hints
server-aware so a failed submit shows the same per-field red text the
client path does, and drop the base-error banner — base only ever carries
the account requirement, which now renders beside the funding-accounts
list.
* fix(goals): blur projection chart under privacy mode
The projection chart's SVG axis labels and annotations (target, "$X short",
$200K/$400K ticks) rendered in cleartext while privacy mode blurred every
other number on the page. Tag the chart container `privacy-sensitive`, the
same wrapper-level treatment the net-worth and cashflow-sankey charts
already use, so it blurs with the rest.
* fix(goals): stop target-date input stretching when amount error shows
The target-amount / target-date row is a two-column grid. The amount
column carries its inline error <p> underneath, so when that error
appears the column grows and the default `items-stretch` makes the
sibling date input stretch to match — the date box visibly grew taller
than the amount box. Anchor the row with `items-start` so each input
keeps its natural height and the error just extends below its own column.
* fix(goals): make invalid submit announce errors instead of dead-ending
Review follow-ups:
- Swap the submit gate from the disabled attribute to aria-disabled: a
truly disabled default submit also blocks Enter-key implicit
submission, so an invalid form was a dead button with every inline
error still hidden. The button now stays clickable and lets
validateOnSubmit surface the errors; DS buttonish styles the
aria-disabled state (cursor-not-allowed + opacity-50). Side effect:
loading_button_controller submits also dim while busy, which reads as
an upgrade.
- Focus the first funding-account checkbox when accounts are the only
missing field (focus previously went nowhere).
- Drop the now-orphaned goals.goal_card.days_left_by locale key.
|
||
|
|
172301f875 | Fix SSO provider settings updates (#2210) | ||
|
|
85d7695d1f | ci(preview): render Cloudflare config from trusted template (#2207) | ||
|
|
1fd5c2e26d |
Minimalistic SECURITY.md
Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
96079188a2 |
feat(dashboard): unify per-widget period selectors into one picker (#2162)
* feat(ds): elevate dropdown overlays and stabilize selection check gutter Menus and popovers floated at the same elevation as inline cards (shadow-border-xs), so dropdowns blended into the content beneath them. Bump DS::Menu and DS::Popover panels to shadow-border-lg. DS::MenuItem rendered its leading icon only when present, so a selection check shifted the row's text out of alignment with the unselected rows. Add a `selected:` param that reserves a fixed-width check gutter (check when selected, empty otherwise) so row text stays aligned. Apply the same reserved gutter to the bespoke category dropdown row, and add a `selectable` menu preview. * feat(dashboard): unify per-widget period selectors into one picker The dashboard rendered three identical period <select>s (cashflow, outflows, net worth), each writing the same global User#default_period and full-reloading the page via turbo_frame "_top" — so changing one changed all. Replace them with a single shared UI::PeriodPicker (DS::Menu of period links) in a toolbar, and wrap the sections grid in a "dashboard_sections" Turbo frame so a period change swaps only the dashboard (no full-page reload). Reuse the same picker on the account chart, removing its duplicate select. * refactor(dashboard): use DS::MenuItem selected: gutter in period picker Now that DS::MenuItem reserves a check gutter, the period picker passes selected: instead of a conditional leading check icon, so the current period's row stays aligned with the rest. * fix(ds): expose menu selection via menuitemradio + aria-checked Selectable DS::MenuItem rows conveyed selection only visually. Render them as role="menuitemradio" with aria-checked so assistive tech gets the selection state of single-select lists, merging the menu ARIA contract with any caller-supplied aria. Addresses CodeRabbit review feedback. * refactor(dashboard): drop redundant aria-current from period picker DS::MenuItem now exposes selection via menuitemradio + aria-checked, so the period picker no longer needs its own aria-current. Update the component test to assert the new ARIA. * fix(ds): include selectable roles in menu roving-focus query DS::MenuItem selectable rows render as role=menuitemradio, but the menu controller built its roving-focus list from [role=menuitem] only, leaving single-select menus with no keyboard focus/arrow handling. Query the menuitemradio/menuitemcheckbox roles too. Addresses Codex review feedback. * fix(dashboard): keep non-picker links out of frame + fix custom-month picker - turbo_frame_tag "dashboard_sections" now targets _top so ordinary links inside the sections (e.g. Balance Sheet account links) navigate the page instead of failing to resolve inside the frame. - Period.current_month_for / last_month_for carry their semantic key for custom-month families, so the picker shows the right label and checks the right option instead of falling back to 30D. Addresses Codex review feedback. * fix(dashboard): announce selected period in picker trigger's accessible name The static "Select time period" aria-label overrode the visible selected label as the trigger's accessible name, so assistive tech kept announcing the same name regardless of selection. Interpolate the selected short label into the aria-label and pin it with a component test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5372a08788 |
feat(mobile): add transaction metadata editing (#2131)
* feat(mobile): add transaction metadata editing * fix(mobile): preserve explicit metadata clears * fix(mobile): derive persisted tag metadata state * fix(mobile): avoid logging transaction details * fix(mobile): harden transaction edit sync Pass the edited transaction context through provider updates, refresh or fall back after empty update responses, surface field-level API errors, and avoid forced metadata refetches on every edit screen open. * fix(mobile): keep transaction edit selects ci-compatible Use the DropdownButtonFormField API supported by the Flutter version pinned in upstream mobile CI. |
||
|
|
6961c7ef41 |
fix(ds): chart flat-state — hairline trendline, not a bisecting rule (#2137) (#2150)
Accounts with a flat series (a single valuation or an unchanged balance, where dataMin === dataMax) plotted the centered horizontal trendline at the full 2px stroke — a near-black full-bleed rule bisecting the hero card in light, leaving ~half the card a void (dark read it as an acceptable hairline). Detect the flat case (_isFlatSeries) and draw the line as a faint hairline (stroke-width 1, stroke-opacity 0.4) so it reads as "no change", consistent across light + dark. Verified on the Car Loan account (30D, no change): the heavy rule is gone. |
||
|
|
be707bfba3 |
fix(ds): cross-viewport lock — consistent auth mode-switch (#2149)
* fix(ds): cross-viewport lock — consistent auth mode-switch (#2137) The auth sign-in/sign-up mode switch rendered as a segmented pill toggle on mobile (md:hidden) but a plain text link on desktop (hidden md:block) — the audit's cross-viewport inconsistency. Lock to the segmented switch at all widths and drop the now-redundant desktop text links. Verified on /sessions/new at desktop width: the Sign in / Create account segmented toggle now shows (was a text link). Note: the switch still uses the bespoke bg-surface-inset track; migrating it to DS::SegmentedControl (#2145) would also fix its dark-mode contrast — a follow-up once that lands. The account-new icon glyph-vs-chip case the audit grouped here did not reproduce (account_type + method_selector already use consistent DS::FilledIcon / chip icons). * fix(auth): keep the sign-in/up switch after a failed submit A failed sign-in/up POST re-renders :new from the #create action, so the switch (gated on action_name == "new") disappeared and the active tab was derived from current_page?, which breaks on the POST URL. Render the switch on both new and create, and derive the active tab from controller_name. Addresses Codex review on #2149. |
||
|
|
52b9b47f8c |
feat(ds): semantic color — neutralize decorative green/red on reports (#2134) (#2144)
Color should signal good/bad, not decorate. The always-on decorative coloring
diluted that: report assets were always green, liabilities always red (and that
red == destructive, so a mortgage read like "danger"), expense totals always
red, and investment contributions/withdrawals always green/orange.
Neutralize those to text-primary:
- _net_worth: assets-vs-liabilities headline + per-group asset/liability rows.
- _breakdown_table: expense totals (income stays green).
- _trends_insights: avg monthly expenses (income + net-savings stay semantic).
- _investment_summary: contributions + withdrawals.
KEEP genuine signal: transaction income->green, net-worth total by sign,
Trend#color gains/losses/changes, budget over/near/on-track status, period
net-savings/income/expense *deltas*. ("Balanced" per design discussion.)
Separate follow-up: category icons using a user-chosen hex equal to destructive
red (color-picker concern, not amount coloring).
|
||
|
|
683f518780 |
fix(balance-sheet): preserve disabled-account net worth history (#1730)
* fix(balance-sheet): preserve disabled account history * fix(balance-sheet): tighten historical account scope * fix(balance-sheet): stop disabled account carry-forward --------- Signed-off-by: Juan José Mata <juanjo.mata@gmail.com> Co-authored-by: Juan José Mata <juanjo.mata@gmail.com> |
||
|
|
f499a3db01 |
fix(ds): .table-scroll — wide tables scroll instead of clipping (#2148)
* fix(ds): .table-scroll — wide tables scroll instead of clipping (#2137) The LLM-usage "Recent Usage" table wrapped in `overflow-hidden`, so on narrow viewports it pushed the Tokens/Cost columns off-screen with no way to reach them (the audit's "no scroll affordance"). Ship a reusable `.table-scroll`: `overflow-x: auto` plus a pure-CSS theme-aware scroll-shadow (cover gradients scroll with the content and reveal an edge shadow only when there is more to see), giving the affordance with no JS. Migrate the LLM-usage table as exemplar. Verified: at a mobile width the table now scrolls (content 591px in a 329px wrapper) with a scrollbar + edge shadow, instead of clipping. Deferred: roll `.table-scroll` out to the other overflow tables — reports use a nested container-inset / container wrapper, so the cover bg needs per-use tuning via `--table-scroll-bg`. * fix(a11y): make .table-scroll region keyboard-focusable Add tabindex=0 + role=region + aria-label so keyboard-only users can focus the horizontal scroll container and reach overflowed columns with arrow keys. Addresses CodeRabbit review on #2148. |
||
|
|
eb27d36063 |
fix(mobile): parse locale-aware transaction amounts (#2130)
* fix(mobile): parse locale-aware transaction amounts * fix(mobile): tighten localized amount parsing |
||
|
|
fe47c918bb |
feat(enable_banking): support MFA/decoupled banks and harden session handling (#2174)
Decoupled/MFA banks (e.g. VR Bank in Holstein) were hard-blocked because the authorize flow aborted whenever auth_methods[0] was DECOUPLED. Enable Banking's hosted /auth page actually coordinates decoupled SCA and redirects back with a code, so route these banks through it instead: - Provider#start_authorization accepts and forwards an auth_method param - EnableBankingItem#select_auth_method picks the best method (REDIRECT > DECOUPLED > EMBEDDED), filtering by psu_type and skipping hidden methods - Shared begin_authorization! re-fetches ASPSP metadata on each authorize and reauthorize, so the method is always re-derived (no persistence required) - Remove the DECOUPLED block in the controller Also stop the integration from constantly reporting "session expired": - Only a session-level GET /sessions 401/404 flips the connection to requires_update; per-account 401/404 are retried and no longer kill the whole connection - Reconcile session_expires_at from the API's access.valid_until on every sync - Treat an expired session as a graceful requires_update state instead of raising a bare error No schema changes. Adds covering tests. |
||
|
|
6a89efb9c9 |
fix(goals): default goal currency so it survives a failed create (#2171)
When a new goal failed server-side validation (e.g. no funding account selected), the currency was never assigned — `goal_params` doesn't permit `:currency` and it was only derived from the first linked account. On the re-rendered form the money field fell back to its USD default, so a CHF (or any non-USD) family saw their target amount silently reset to dollars, and a spurious "Currency must be filled" error appeared on a field the user can't even set. Always default the currency to the first linked account's currency when present, otherwise the family's primary currency. Currency is now never blank, so the derived-field error never surfaces and the money field keeps the family currency across a failed submit. Fixes we-promise/sure#2170 |
||
|
|
1742f4ef1e |
feat(ds): elevate dropdown overlays and stabilize selection check gutter (#2161)
* feat(ds): elevate dropdown overlays and stabilize selection check gutter Menus and popovers floated at the same elevation as inline cards (shadow-border-xs), so dropdowns blended into the content beneath them. Bump DS::Menu and DS::Popover panels to shadow-border-lg. DS::MenuItem rendered its leading icon only when present, so a selection check shifted the row's text out of alignment with the unselected rows. Add a `selected:` param that reserves a fixed-width check gutter (check when selected, empty otherwise) so row text stays aligned. Apply the same reserved gutter to the bespoke category dropdown row, and add a `selectable` menu preview. * fix(ds): expose menu selection via menuitemradio + aria-checked Selectable DS::MenuItem rows conveyed selection only visually. Render them as role="menuitemradio" with aria-checked so assistive tech gets the selection state of single-select lists, merging the menu ARIA contract with any caller-supplied aria. Addresses CodeRabbit review feedback. * fix(ds): include selectable roles in menu roving-focus query DS::MenuItem selectable rows render as role=menuitemradio, but the menu controller built its roving-focus list from [role=menuitem] only, leaving single-select menus with no keyboard focus/arrow handling. Query the menuitemradio/menuitemcheckbox roles too. Addresses Codex review feedback. |
||
|
|
0d32f7507c |
fix(goals): scope funding-account picker to the current user's accessible accounts (#2172)
* fix(goals): scope funding-account picker to the current user's accessible accounts The new/edit goal funding picker and the linkable-account count queried `Current.family.accounts`, so it listed (and would link/fund from) every depository account in the family — including accounts owned by other members that aren't shared with the current user. Switch the three queries (index count, lookup, picker list) to `Current.user.accessible_accounts`, matching the access boundary used elsewhere. Adds controller tests covering the new-form picker and the create path rejecting a non-accessible same-family account. Fixes #2168 * fix(goals): preserve inaccessible linked accounts on goal edit The funding picker only renders Current.user.accessible_accounts, so a family goal linked to another member's private account renders no checkbox for it. On update, sync_linked_accounts! treated that omission as an intentional removal and destroyed the link the editor could not see. Restrict unlinking to the editor's accessible accounts so links outside their access are preserved. Adds a regression test. |
||
|
|
6e04c6927d |
feat(imports): add SureImport session batches (#1785)
* feat(imports): add SureImport session batches Add first-class SureImport sessions for ordered multi-file NDJSON imports. Persist source mappings across chunks, make session/chunk processing idempotent, expose progress readback, and keep existing single-file import behavior compatible. Includes the devcontainer libvips runtime dependency needed by ActiveStorage variant tests. Addresses #1610. Related to #1458. * fix(imports): avoid scanner-like API key test data * test(imports): assert skipped balances are not persisted * fix(imports): harden session publish retries Validate expected import chunk sequences exactly before publish, and restore session state with error details when enqueueing the publish job fails. * fix(imports): close session retry edge cases Backfill expected chunk counts after client-session insert races and enqueue import-session jobs after the status transition commits. Persist a safe enqueue failure body so API readback does not expose raw queue errors. * fix(imports): address session publish review gaps Remove dead transaction external-id assignment, harden session publish retry/sync behavior, align session chunk status docs, and add regression coverage for partial retries and safe enqueue error readback. * fix(imports): include sessions in family reset Clear import sessions through the family reset job so chunk imports and source mappings do not survive a reset. Expose import session and source mapping counts in the reset status response and regenerated OpenAPI schema so polling reflects the full reset surface. * test(imports): cover split import mapping invariants * test(imports): cover session verification invariants * fix(imports): scope SureImport session reimports * Tighten SureImport session batching * fix(imports): export rule source ids for sessions * test(imports): stabilize rule id export assertion * test(imports): restore reset status session fixture |
||
|
|
76d4c2a2fe |
Pass APP_BUNDLE_ID into iOS archive (#2169)
* Pass APP_BUNDLE_ID into iOS archive * Rewrite bundle identifier for override values |
||
|
|
74f811c334 |
fix(accounts): honor stored return_to after subtype account creation (#2109)
* fix(accounts): honor stored return_to after subtype account creation Closes #1766. The savings-goals empty-state "Add an account" CTA passes ?return_to, which StoreLocation captures into session[:return_to], but account-creation flows didn't always consume it: - AccountableResource#create honored a form-carried return_to but not the session value, so if the param wasn't threaded through the multi-step new-account flow the user still landed on the account page. Added a session[:return_to] fallback (the form param still wins). - PropertiesController is a 3-step wizard (create → balances → address) that never threaded return_to as a form param, and its final redirect went straight to account_path. It now honors session[:return_to] on completion. Rails blocks external-host redirects, so return_to can't open-redirect. valuations#create uses redirect_back_or_to (referer-based) — different flow, left as-is. Tests: depository create prefers the form return_to and falls back to the session value; property wizard completion honors the stored return_to. * fix(accounts): block open-redirect via return_to; consume session value Two AI-review findings on #2109: - Open-redirect (codex): the property wizard's turbo_stream completion uses stream_redirect_to, which the client resolves with Turbo.visit — that full-navigates cross-origin, bypassing Rails' redirect host-guard. A crafted ?return_to=https://evil could walk the user off-site. Filter return_to at the StoreLocation choke point (store time) to internal absolute paths only, and sanitize the separate form-param channel, so an unsafe value can't reach redirect_to / stream_redirect_to. - Stale session (coderabbit): session[:return_to] was read but never consumed. Consume it with delete at redirect time so it can't leak into a later flow. Adds guard tests (external return_to falls back to the account page). * fix(security): guard safe_return_to against non-String return_to A crafted `?return_to[]=foo` makes params[:return_to] an Array, and Array#match? doesn't exist, so safe_return_to raised NoMethodError before the open-redirect hardening could reject it. Add an is_a?(String) check as the first gate. Other CodeRabbit/Codex return_to findings on this PR were already addressed (consume-side re-validation + session.delete). |
||
|
|
5abf9cb537 |
fix(ds): dark-mode token parity — contrast & state fixes (#2139)
* fix(ds): dark-mode token parity — text, borders, checkbox, toggle, over-budget badge Dark-mode contrast/parity pass (design-review epic #2134, follow-up to #1736): - text-subdued (dark): gray-500 -> gray-400; faint/eyebrow text ~3.6:1 -> ~6.7:1 (clears AA), staying below text-secondary so hierarchy holds. - border-primary/secondary/subdued (dark): +1 alpha-white step each; restores group-card edges and in-card row hairlines (border-primary 1.86:1 -> 2.8:1). Alpha keeps them surface-relative across any dark bg. - Dark checkbox: unchecked was a solid white square (read as already-selected) — now a transparent outlined box; checked/indeterminate use a white fill with a #171717 glyph (was #808080, ~2:1); added an explicit indeterminate dash; disabled muted to gray-700. - Toggle (light off-state): track gray-100 -> gray-300 plus a thumb shadow; the white-thumb-on-white-track invisible off-state now reads (dark off-track was already hardened to gray-700). - Over-budget badge: text-red-500 -> text-destructive (theme-aware, matching the on-track/near-limit siblings); in-situ dark contrast 4.18:1 -> 4.55:1 (AA). - Correct invalid icon color keys (red/yellow/green -> destructive/warning/success) on the three budget status badges. Verified in-browser, light+dark: isolated checkbox states, /accounts card borders, /reports muted text, toggle off-state, /budgets over-budget badge (in-situ 4.55:1). * fix(ds): reconcile destructive color + fix filled-pill contrast Continues the dark-parity pass (#2134): - Reconcile destructive: border-destructive and button-bg-destructive were red-500 while the destructive text/icon token was red-600. Unify on red-600 (light) / red-400 (dark) across text, border, and button — the text token can't drop to red-500 (3.96:1 on white, fails AA), so border/button move up instead. White-on-destructive-button 3.96:1 -> 4.36:1 (AA-large); hover red-600 -> red-700. - DS::Pill filled style: deepen the fill tone-500 -> tone-700. White label text on tone-500 failed AA on nearly every tone (amber 2.35:1, green 2.62:1, red 3.95:1); tone-700 clears it (amber 5.43, green 4.30, red 5.86, others 6.4-12) in both themes and removes the dark-surface glare. Date-input calendar glyph in dark verified already-correct (existing invert(1) rules; color-scheme is normal, so no conflict) — no change needed. Verified in-browser: real .button-bg-destructive (red-600) + filled pills, all tones, light and dark. * fix(ds): destructive button consumes the reconciled red-600 Follow-up to the destructive reconcile in this branch: DS::Buttonish's destructive variant still used raw bg-red-500 / hover:bg-red-600, so destructive *buttons* didn't match the reconciled destructive text/border (red-600). Align to red-600 / hover red-700 (light); dark unchanged (red-400 / red-500). White-on-red-600 = 4.37:1 (AA-large), consistent with the rest of the destructive family. * refactor(ds): tokenize budget-category badge + bar backgrounds Status-badge foregrounds already used semantic tokens (text-destructive/ warning/success) but backgrounds + progress-bar fills stayed on the raw palette (bg-red-500/10, bg-yellow-500, ...). Switch to the matching semantic tokens (bg-destructive/10, bg-warning, bg-success) — same value in light, now theme-aware in dark. Mirrors DS::Alert. Addresses CodeRabbit/Codex on #2139. |