whisper-money/resources/js/components
Víctor Falcón d55c3f41df
fix(security): scope job-status endpoints to owner + feature-area fixes (#627)
## Summary

Starts from a security finding on the background-job status endpoints
and folds in the most important issues surfaced by a follow-up review of
the same feature area (categorization backfill, bulk rule re-evaluation,
automation-rule apply).

Each change is its own commit.

## Changes

### 1. `fix(security)` — scope job-status cache keys to the owning user
The categorization, bulk re-evaluation, and apply status endpoints
looked jobs up by a bare job UUID with no user scoping. Any
authenticated user who obtained another user's job id could poll its
progress payload. Cache keys now include the owning user's id, so a
status request keyed by the polling user's id resolves only that user's
own jobs — a mismatched owner falls through to the existing 404. No
ownership store or extra lookup. Cross-user isolation tests added for
all three endpoints.

### 2. `fix(automation-rules)` — re-check `only_uncategorized` at apply
time
The apply flow cached a match snapshot for up to 15 min (keyed only by
`rule.updated_at`) and applied the rule's category to every id in it
without re-checking eligibility. A transaction categorized *after* the
snapshot (by the user, a sibling rule, or a concurrent AI backfill) was
silently re-categorized and stamped `category_source=Rule`. Now
re-filtered through `shouldSkipForOnlyUncategorized()` at apply time;
skipped rows are no longer counted as changed.

### 3. `perf(automation-rules)` — memoize rule set per user in
`applyRules()`
Bulk re-evaluation calls `applyRules()` once per transaction, and each
call re-queried the user's whole rule set + labels — an N+1 scaling with
transaction count (the sibling apply job already loaded rules once).
Memoized per user for the service instance's lifetime (resolved fresh
per job, so rules created mid-run are intentionally not seen).
Query-count test asserts one `automation_rules` query regardless of
transaction count.

### 4. `fix(transactions)` — stop polling when the job never starts
The re-evaluate and apply pollers rescheduled on any non-terminal
status, including `pending`. If the queue worker is down the job never
runs, `failed()` never fires, and the client polled for the full
hour-long TTL with a stuck spinner. Now gives up after 30 consecutive
`pending` ticks (~30s), mirroring the guard the AI-categorization poller
already has. Long `processing` runs are unaffected.

### 5. `test(automation-rules)` — cover apply job execution and failure
branches
`ApplySingleAutomationRuleJob` was only asserted to be *pushed*; its
`handle()` body and `failed()` branch had no coverage, and
`ReEvaluateTransactionRulesJob::failed()` was untested too. Added direct
`handle()`/`failed()` tests pinning the progress-cache payloads.

## Deferred follow-ups (surfaced by review, not in this PR)

- **Consolidate the three frontend pollers onto `usePollJobStatus`** —
they hand-roll `setTimeout` loops and don't tear down on unmount (can
`setState`/`onClose` after the dialog closes). The hook already exists
and is unmount-safe; routing all three through it would also make the
`pending` cap unit-testable. The cap in change #4 currently mirrors the
already-shipped AI poller and is not separately unit-tested.
- **`noteAlreadyPresent()` uses a substring match**
(`AutomationRuleService`) — a rule note that is a substring of an
existing note is silently not appended. Should compare note lines
exactly.
- **No dedup/lock on concurrent apply/re-evaluate jobs** — note appends
are a non-atomic read-then-write, so two concurrent jobs can duplicate a
note. Category/label writes are already idempotent.
- **Transient 404 → false "failed"** — a mid-run cache
eviction/TTL-expiry (or the deploy window of change #1, where an
in-flight job's pre-deploy key is orphaned) makes the poller surface a
false failure. Retry a few times on transient errors before giving up.
- **`matches()` pagination** advances `next_offset` by fetched rows, not
page-window size — deleted ids can stall infinite scroll before reaching
`total`.
- **"Apply to N" count / encrypted-skip counts** can be stale or
under-report vs what's actually processed.
- **Job-trait convention drift** between the three jobs (modern
`Queueable` vs legacy trait stack).

## Testing
- `vendor/bin/pint --test` — pass
- `bun run format` / `bun run lint` — pass (one pre-existing unrelated
warning in `chart.tsx`)
- Affected Pest suites (apply, re-evaluate, evaluation, rule,
categorization, apply-rule-suggestions) — 94 tests pass
2026-07-03 14:49:32 +02:00
..
accounts feat(accounts): reorder accounts with drag-and-drop (#575) 2026-06-21 11:17:45 +02:00
automation-rules fix(security): scope job-status endpoints to owner + feature-area fixes (#627) 2026-07-03 14:49:32 +02:00
budgets feat: add catch-all budgets (#527) 2026-06-15 16:07:19 +00:00
cashflow feat(analysis): per-category 12-month spending drawer (#519) 2026-06-11 09:52:53 +02:00
categories feat(analysis): per-category 12-month spending drawer (#519) 2026-06-11 09:52:53 +02:00
charts feat: single-open Sankey expand, fit overflowing children (#487) 2026-06-04 11:35:26 +02:00
dashboard feat(dashboard): add accounts manager dialog with visibility toggle and reorder (#604) 2026-06-27 16:11:25 +00:00
icons Subscriptions (#15) 2025-12-06 19:09:56 +01:00
integration-requests feat(integration-requests): add done status and fix review command crash on orphaned author (#601) 2026-06-27 14:42:09 +00:00
labels refactor(js): use InputError for inline form errors (#483) 2026-06-03 19:01:21 +02:00
landing feat(landing): add go now button to redirect modal (#506) 2026-06-08 09:51:35 +02:00
onboarding feat(onboarding): auto-enable AI for connected banks, ask the rest (#618) 2026-07-01 07:26:52 +00:00
open-banking feat(banking): enable Interactive Brokers for all users (#593) 2026-06-26 11:03:21 +02:00
partials fix(header): keep mobile logo on one line, compact auth buttons (#512) 2026-06-09 14:44:35 +02:00
shared feat(ai): auto-categorize transactions with AI (behind flag) (#535) 2026-06-15 16:35:20 +02:00
transactions feat(ai): learn from category corrections so the AI stops repeating the same mistake (#608) 2026-06-29 19:12:15 +02:00
ui feat(transactions): highlight new transactions since last visit (#609) 2026-06-29 16:08:14 +00:00
alert-error.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
app-content.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
app-logo-icon.tsx refactor(encryption): strip client-side transaction encryption (#514) 2026-06-20 16:13:26 +00:00
app-logo.tsx Add animated logo icon that transitions based on encryption key state 2025-12-12 11:24:42 +01:00
app-shell.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
app-sidebar-header.tsx fix: keep iOS content below the notch (#280) 2026-04-13 13:59:05 +01:00
app-sidebar.tsx fix: move community link to user menu (#442) 2026-05-27 17:39:26 +02:00
appearance-tabs.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
bank-logo.tsx feat: Integrate EnableBanking as open banking provider (#106) 2026-02-12 09:09:28 +01:00
breadcrumbs.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
delete-user.tsx fix(account): block deletion while subscription or trial is active (#531) 2026-06-14 20:46:44 +02:00
encryption-key-button.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
heading-small.tsx Add Budgeting Feature to Track and Manage Spending (#36) 2026-01-21 15:25:50 +01:00
heading.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
icon.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
input-error.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
mobile-back-button.tsx fix(haptics): use a local WebHaptics wrapper (#225) 2026-03-16 11:26:04 +00:00
nav-footer.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
nav-main.tsx fix(haptics): use a local WebHaptics wrapper (#225) 2026-03-16 11:26:04 +00:00
nav-user.tsx feat(integration-requests): community board to request & vote bank integrations (#550) 2026-06-17 12:50:51 +00:00
sortable-grid.test.tsx fix(accounts): stop second long-press haptic on drag handle (#578) 2026-06-22 11:20:57 +02:00
sortable-grid.tsx fix(accounts): stop second long-press haptic on drag handle (#578) 2026-06-22 11:20:57 +02:00
support-dialog.tsx feat(support): add support link with community-first help modal (#542) 2026-06-16 13:16:04 +02:00
text-link.tsx Set up a fresh Laravel app 2025-11-07 12:01:36 +00:00
two-factor-recovery-codes.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
two-factor-setup-modal.tsx feat: Spanish localization (#74) 2026-02-08 11:58:08 +01:00
unlock-message-dialog.tsx feat(auth): show/hide toggle on password fields (#499) 2026-06-06 11:42:20 +02:00
user-info.tsx feat: Update facehash and enable blink 2026-02-24 15:54:49 +01:00
user-menu-content.test.tsx feat(integration-requests): community board to request & vote bank integrations (#550) 2026-06-17 12:50:51 +00:00
user-menu-content.tsx feat(integration-requests): community board to request & vote bank integrations (#550) 2026-06-17 12:50:51 +00:00