## What & why
The dashboard white-screened with React **"Maximum update depth
exceeded"** on mobile (iOS 18.7 / Opera Touch), fingerprinted in Sentry
as **PHP-LARAVEL-47** (`https://whisper.money/dashboard`).
### Root cause (upstream, recharts < 3.9.0)
The crashing first-party frame is inside recharts' internal
`CartesianChart` → `useElementOffset`
(`node_modules/recharts/es6/util/useElementOffset.js`). In < 3.9.0 that
hook:
- memoizes its measuring **callback ref** with `useCallback(...,
[lastBoundingBox.width, .height, .top, .left, ...])`, so **every
`setState` changes the ref identity → React re-invokes the ref → it
re-measures on each render**, and
- reads **viewport-relative** `getBoundingClientRect().top/left` with a
`EPS = 1px` threshold.
On mobile, any mid-render viewport shift (the iOS URL bar showing/hiding
on scroll) moves the box `> 1px` on each commit, feeding an unbounded
`render → measure → setState` loop until React aborts at the
nested-update limit and the dashboard white-screens.
This is the **same crash family** as the `ChartTooltipPortal` loop fixed
in #657, but a **distinct root cause inside recharts** (that first-party
loop is already fixed and covered by its own test; both are present on
this branch).
### The fix
Upgrade **recharts 3.7.0 → 3.9.2**. 3.9.0 rewrote `useElementOffset` to
drive measurement off a **`ResizeObserver` with a stable callback ref**
(deps no longer include the last bounding box). `ResizeObserver` fires
on element **size** changes, not on scroll/viewport-position — so the
iOS URL-bar shift no longer re-triggers measurement. This removes the
loop at its source; it does not merely mask it.
## Commits
1. `fix(chart): upgrade recharts to 3.9.2 to stop the dashboard render
loop` — the fix (bun.lock + a code comment on `ResponsiveContainer`
recording the `>= 3.9.0` requirement).
2. `fix(chart): raise recharts floor to ^3.9.0 so the crash fix can't
regress` — review follow-up: `package.json` was still `^3.7.0` (permits
the crashing versions); bumped to `^3.9.0` and regenerated
`package-lock.json` so both `bun install --frozen-lockfile` and `npm ci`
enforce the floor. Also resolves a pre-existing
bun.lock/package-lock.json divergence.
3. `test(chart): guard recharts >= 3.9.0 against a downgrade regression`
— asserts the installed recharts is `>= 3.9.0` (the honest test for a
"depend on the patched version" fix; a jsdom behavioral repro is
impossible — zero-size rects, no ResizeObserver — and would pass on the
buggy version too).
## Review (two agents, before this PR)
Both reviewed against the actual `node_modules/recharts@3.9.2` source,
not just release notes.
- **No material regression risk.** The `ChartTooltipPortal`'s two hard
dependencies still hold in 3.9.2: `.recharts-wrapper` is still emitted
and is still the default tooltip portal target, and `Tooltip` still
passes `coordinate` as `{ x, y }`. `ResponsiveContainer`'s
`initialDimension` is still supported. No API we use was
removed/renamed.
- **Types & tests:** `bun run types` produces zero recharts-attributable
errors; the chart-related JS tests (tooltip portal/position,
budget-spending-chart, stacked-bar custom-shape path) pass on 3.9.2.
### Lockfile note
The production bundle is built by `bun run build` off **bun.lock**
(pinned to 3.9.2). `package-lock.json` is consumed **only** by
`release.yml`'s `npm ci`, which runs `release-it`
(versioning/changelog/tag) and does **not** build the shipped bundle —
so its large regen diff is mechanical drift-repair and cannot affect
users. Follow-up worth considering (out of scope): consolidate on bun
and drop the vestigial `package-lock.json`.
## Why draft (not auto-merged)
The root-cause fix is high-confidence, but this is a **charting-library
bump touching all chart surfaces** and the original bug is
**mobile-only**, which I can't visually QA here. Please spot-check
before merge:
- [ ] **Mobile dashboard** (ideally iOS Safari + Opera Touch): charts
render, no white-screen, scroll is smooth, tooltips position correctly.
- [ ] **Animations**: recharts 3.9 rewrote the animation system — glance
that Area/Bar/Line still animate acceptably on data refresh (esp.
`account-balance-chart`, `budget-spending-chart`).
- [ ] **New Sentry noise**: watch for `"ResizeObserver loop completed
with undelivered notifications"` — that's a benign warning, not the loop
returning; suppress rather than treat as a regression.
Refs PHP-LARAVEL-47
Patch release **v0.2.5** (0.2.4 → 0.2.5).
### Cambios
- Bump de versión + `CHANGELOG.md` generados con release-it
(conventional-changelog).
- **Autoría de PRs en el changelog**: cada entry muestra `by [@handle]`,
resuelto por número de PR vía la API de GitHub.
- `scripts/enrich-changelog.js`: enriquece solo la sección de release
más reciente; idempotente y no-fatal si `gh` no está disponible.
- Hook `after:bump` en `.release-it.json` para que se aplique
automáticamente en cada release futura.
Tras el merge: creo el tag `v0.2.5` y el GitHub release sobre `main`.
Patch release `v0.2.4`. Bumps version + updates CHANGELOG via
release-it.
Tag `v0.2.4` + GitHub Release created after merge (tag must point at the
merged commit).
## Summary
- Installs `web-haptics` via bun
- Triggers `selection` haptic feedback on all navigation items (mobile
bottom bar and desktop sidebar)
- Triggers `light` haptic feedback on all back buttons
(`connect-account-inline.tsx`, `categorize.tsx`)
## Why
### Problem
Running `bun release -i patch` (or any interactive `release-it`
invocation) always failed with:
```
ERROR stringWidth is not a function
```
### Root Cause
`@inquirer/core` (used by `inquirer@12`, which `release-it@19` depends
on) bundles its own `wrap-ansi@6.2.0` (CJS). That package calls
`require('string-width')`, but the top-level `string-width` in this
project is v8 — pure ESM. When required via CJS, Node returns a module
namespace object instead of a function, causing the crash when
`release-it` tried to render its interactive confirmation prompt.
## What
- Added `scripts/patch-inquirer-string-width.js`: installs
CJS-compatible versions of `string-width@4`, `strip-ansi@6`,
`ansi-regex@5`, and `is-fullwidth-code-point@3` into
`node_modules/@inquirer/core/node_modules/` so `wrap-ansi@6` resolves
them correctly.
- Added a `postinstall` script to `package.json` so the patch is
re-applied automatically after every `bun install`.
## Verification
### Tests
No automated tests — this is a dev tooling fix. Verified manually by
running `node node_modules/release-it/bin/release-it.js --dry-run` and
confirming the interactive prompt renders without the `stringWidth`
error.
### Manual
1. `bun install` — postinstall script runs and outputs `Patch applied.`
2. `bun release -i patch` — interactive prompt now renders correctly.
## Why
### Problem
Cards across the app lacked visual interactivity. Adding a
cursor-tracking glowing border effect improves the UI polish and makes
the dashboard and other card-heavy pages feel more dynamic.
## What
### Changes
- Install `motion` package (required for the `animate()` call in the
effect)
- Add `GlowingEffect` component at
`resources/js/components/ui/glowing-effect.tsx` — tracks pointer
position and renders an animated conic gradient border that follows the
cursor
- Update the base `Card` primitive to include `<GlowingEffect>` as the
first child with `spread=40`, `proximity=64`, `inactiveZone=0.01`,
`glow=true` — all consumers inherit the effect automatically
- Remove `overflow-hidden` from the net-worth chart's `<Card>` (it was
clipping the effect) and move it to `<CardContent>` where it's actually
needed to contain the chart
## Verification
### Tests
No new logic was introduced — this is a purely visual enhancement to an
existing UI primitive. Existing tests remain unaffected.
### Manual
Move the cursor near and over any card (dashboard stats, account
balances, net worth chart, cashflow, budgets) to see the gradient border
glow follow the cursor.