## Problem
Sentry reporting permission-denied errors on `/open-banking/callback`:
> UnexpectedValueException: The stream or file
"/app/storage/logs/laravel.log" could not be opened in append mode:
Failed to open stream: Permission denied
4 issues, 14 events (PHP-LARAVEL-Z, 10, 11, 12).
## Root cause
Entrypoint runs as root. After the initial `chown www-data`, artisan
commands (`config:cache`, `route:cache`, `view:cache`, `event:cache`,
`migrate`) execute and can create/touch `storage/logs/laravel.log` as
root. Later, php-fpm (www-data) tries to append and fails.
Queue workers + inertia-ssr also run as root via supervisor — same
failure mode from worker side.
## Fix
- `docker/entrypoint.sh`: re-chown `/app/storage` and
`/app/bootstrap/cache` to www-data **after** artisan cache warm-up, just
before `exec supervisord`.
- `docker/supervisor/supervisord.conf`: add `user=www-data` to
`queue-worker`, `queue-worker-emails`, `inertia-ssr`. Nginx and php-fpm
master keep root (need port 80 / pool forking).
## Verification
Deploy and confirm Sentry issues stop firing on next
`/open-banking/callback` hits.
Manual `Release` workflow failed before `release-it` ran because it
installed dependencies with `npm ci` while repository dependency state
is maintained with Bun. This change aligns the release job with the
repo’s actual package manager so manual releases can install
dependencies from the committed lockfile.
- **Problem**
- `release.yml` used `npm ci`, which requires `package.json` and
`package-lock.json` to be perfectly in sync.
- Repo changes had moved dependency resolution forward via Bun, causing
the manual release job to fail during install instead of reaching
release creation.
- **Workflow change**
- Keep Node setup for `release-it`.
- Add Bun setup in the release job.
- Replace `npm ci` with `bun install --frozen-lockfile`.
- **Result**
- Manual release job now follows same package manager path as rest of
repo/workflows.
- Dependency installation uses committed Bun lock state, avoiding
`package-lock.json` drift as release blocker.
```yaml
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: bun install --frozen-lockfile
```
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: victor-falcon <238766+victor-falcon@users.noreply.github.com>
Adds a manually-triggered workflow to cut releases from the GitHub
Actions UI.
## What
New workflow: \`.github/workflows/release.yml\`
- Trigger: \`workflow_dispatch\` with a \`bump\` input (\`patch\` /
\`minor\` / \`major\`, default \`patch\`)
- Runs \`npx release-it <bump> --ci\` → reuses existing
\`.release-it.json\`:
- angular conventional-changelog preset
- updates \`CHANGELOG.md\`
- commits \`chore: release vX.Y.Z\`
- tags \`vX.Y.Z\`
- creates GitHub release with grouped Features / Bug Fixes notes
## Auth
Uses the default \`GITHUB_TOKEN\` (no PAT). Requires:
- \`permissions: contents: write\` (push commit/tag, create release)
- Repo setting: Settings → Actions → General → Workflow permissions →
**Read and write permissions**
If \`main\` has branch protection blocking direct pushes, the bot token
must be allowed to bypass, otherwise the push step will fail and a PAT
will be needed instead.
## How to run
Actions tab → Release → Run workflow → pick bump type.
## Summary
Mirror the real-estate historical-balance pattern for loan accounts.
When a loan is created with a current balance, linearly interpolate
monthly `account_balances` rows between the loan start date
(`original_amount`) and today (current balance).
## Approach
Since we don't know how the user actually paid down the loan, use simple
linear regression between two known points:
- `(start_date, original_amount)` from `loan_details`
- `(today, current_balance)` from the latest `account_balances` row
Rows are placed on the start date, the 1st of each intermediate month,
and today.
## Changes
- **`LoanBalanceGeneratorService`** — linear interpolation + upsert on
`(account_id, balance_date)` so existing rows aren't duplicated.
- **`GenerateHistoricalLoanBalancesJob`** — `ShouldQueue`, 3 tries, 10s
backoff. Takes account, original amount, start date, current balance,
from, to.
- **`Settings/AccountController::store`** (loan branch) — after creating
`loanDetail`, if a starting balance was provided: generate the last 12
months synchronously, dispatch older window to the queue when
`start_date` predates it.
- **`LoanDetailController::update`** — when the detail is first created
for an existing account, use the most recent `AccountBalance` as the
current value and apply the same sync/async split.
## Tests
- Service: interpolation, single-balance edge, future start_date, upsert
dedupe, monthly 1st placement, from/to range.
- Job: implements `ShouldQueue`, respects from/to range.
```
Tests: 8 passed (62 assertions)
```
No dependency or directory-structure changes.
## Summary
Every subscription now gets Stripe tax rates attached automatically via
Cashier.
## Changes
- `config/subscriptions.php`: new `tax_rates` array, env
`STRIPE_TAX_RATES` (comma-separated), default
`txr_1TPfzrLRCmKA3oWMNWmkQeq2`
- `app/Models/User.php`: `taxRates()` reads from config — Cashier picks
it up automatically on `newSubscription()` checkout + subscription
creation
- `tests/Feature/SubscriptionTest.php`: 2 tests
## Applies to
- New checkout sessions (`SubscriptionController::checkout`)
- New subscriptions created via Cashier
## Existing subscriptions
Not updated automatically. To sync:
```php
$user->subscription('default')->syncTaxRates();
```
## Notes
Using hard-coded tax rate IDs (not Stripe Tax auto-calc). Switch to
`Cashier::calculateTaxes()` later if desired.
## Summary
Adds a 15-day trial to the monthly and yearly plans. Configurable per
plan (or disabled) via config.
## Changes
- `config/subscriptions.php` — new `trial_days` key per plan (defaults:
monthly=15, yearly=15). Env overrides: `STRIPE_PRO_MONTHLY_TRIAL_DAYS`,
`STRIPE_PRO_YEARLY_TRIAL_DAYS`. Set to `0` to disable.
- `SubscriptionController::checkout` — applies `trialDays()` on the
Cashier subscription builder when `trial_days > 0`.
- Tests — assert `trial_days` surfaced in pricing props; assert
`trialDays(15)` applied on checkout; assert skipped when `0`.
## Notes
Stripe Checkout enforces a **minimum 2-day trial**. Values of `1` will
fail at Stripe. `0` disables cleanly.
## Test plan
```
php artisan test --compact tests/Feature/SubscriptionTest.php
```
## Summary
Adds a dashed 'Today' vertical reference line on the account balance
chart when a loan amortization projection is rendered. Makes it obvious
which months on the chart are historical (carry-forward of the last
recorded balance) and which are future projections.
## Context
Loan accounts extend their balance-evolution series with 12 months of
projected amortization values (see
`DashboardAnalyticsController::accountBalanceEvolution`). Without a
marker, past carry-forward months and future projected months looked
like a single continuous history.
## Changes
- `resources/js/components/accounts/account-balance-chart.tsx`
- Import `ReferenceLine` from recharts.
- Compute `todayMarker` (`yyyy-MM` monthly, `yyyy-MM-dd` daily).
- Render `<ReferenceLine>` inside the projected `ComposedChart` branch.
- Styling matches the budget spending chart (1px dashed, foreground
stroke, 'Today' label on top).
## Screenshots
_Visual change only — renders a dashed vertical line at the current date
on loan account charts with projection._
## Summary
Follow-up to #317. Portaled tooltip uses `position: fixed`; on scroll it
stayed anchored to stale viewport coords.
- Listen to `scroll` (capture) and `resize`, flip a `hidden` flag to
hide the tooltip.
- Reset flag whenever Recharts reports a new `coordinate` (pointer back
on a bar).
- Replace `visibility` binary toggle with `opacity` + 120ms transition
so the whole tooltip (bg, border, text) fades as one layer instead of
revealing staggered unmount order.
## Test plan
- Hover a bar → tooltip fades in at cursor.
- Scroll the page while hovering → tooltip fades out smoothly, whole
element at once.
- Hover another bar after scrolling → tooltip reappears.
## Problem
On mobile, the sparkline tooltip on the dashboard account cards covered
almost the entire card, making it very hard to dismiss — you had to tap
very specific spots inside the card to clear it.
## Fix
Listen for `pointerdown` events outside the chart wrapper and unmount
the Recharts `<Tooltip>` to dismiss it. Because only the Tooltip is
toggled (not `<Line>`), there's no redraw animation.
Gated behind `matchMedia('(hover: hover)')` so desktop hover behavior is
left alone — tooltip still appears on hover without any click needed.
Works across multiple cards: tapping one dismisses tooltips on all
others.
## Summary
- Portal chart tooltip to `document.body` to escape ancestor
`overflow-hidden` (CardContent, scroll container). Positioned via
Recharts' `coordinate` prop + `.recharts-wrapper` viewport rect, with
viewport-edge flipping.
- Cap tooltip width and truncate long account/liability names with
ellipsis; values stay nowrap.
- Force `grid-cols-[minmax(0,1fr)]` on tooltip grid tracks so `truncate`
actually engages (grid items default to min-width auto).
## Test plan
- Hover net-worth chart: tooltip no longer clipped by container.
- Position follows cursor; flips near viewport edges.
- Long liability/account names show ellipsis within tooltip bounds.
## Summary
- soft-delete users by adding `deleted_at` to `users`
- rename deleted user emails with a timestamp prefix so original email
can be reused
- block email sends and banking follow-up work for deleted users while
preserving data
## Testing
- php artisan test --compact tests/Feature/DeleteUserCommandTest.php
tests/Feature/Settings/ProfileUpdateTest.php
tests/Feature/Auth/RegistrationTest.php
tests/Feature/Auth/AuthenticationTest.php
tests/Feature/Jobs/Drip/SendWelcomeEmailJobTest.php
tests/Feature/Console/SendUpdateEmailCommandTest.php
- vendor/bin/pint --dirty --format agent
## Summary
- add instruction to watch PR checks after PR creation
- include fail-fast command example for CI monitoring
## Testing
- not run (AGENTS.md change only)
## Summary
- redirect guest access to protected routes based on returning-user
cookie
- send new PWA guests to registration and returning guests to login
- persist returning-user cookie after register, login, and 2FA login
- keep explicit login links working via `force=1`
## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/Auth/AuthenticationTest.php
tests/Feature/DashboardTest.php tests/Feature/Auth/RegistrationTest.php
## Summary
- add signed landing links that unlock auth buttons while
HIDE_AUTH_BUTTONS is enabled
- persist the unlock in a secure cookie so desktop and installed PWA
users can still sign up
- add artisan command to generate signed landing auth links and test
coverage for the flow
## Testing
- php artisan test --compact tests/Feature/LandingAuthOverrideTest.php
tests/Feature/Console/GenerateLandingAuthLinkCommandTest.php
tests/Feature/Auth/RegistrationTest.php
- php artisan test --compact tests/Feature/WelcomeBanksOrderingTest.php
tests/Feature/SubscriptionTest.php
- vendor/bin/pint --dirty --format agent
## Summary
- add artisan command to generate N Stripe promotion codes
- default coupon to 0E5fAsXG and enforce single redemption
- add feature test coverage for success and validation
## Testing
- php artisan test --compact
tests/Feature/GenerateStripePromotionCodesCommandTest.php
- vendor/bin/pint --dirty --format agent
## Summary
- save the first-sync email cutoff after the initial import finishes so
onboarding transactions are not counted as later sync activity
- add a regression test that reproduces onboarding imports and verifies
the daily sync email stays silent afterward
## Testing
- php artisan test --compact
tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php
- vendor/bin/pint --dirty --format agent
## Summary
- make the `deploy` job wait for `build-image` so Sentry release
creation finishes before deploy tracking runs
- keep the Sentry deploy marker on the same release name while removing
the race that caused `Release not found`
- add a focused regression test that asserts the workflow keeps this
dependency in place
## Testing
- php artisan test --compact tests/Feature/SentryConfigTest.php
## Summary
- remove the `real-estate` Pennant flag definition, request gating,
local auto-activation middleware, and frontend shared flag plumbing
- make real-estate account creation and onboarding availability the
default path for all users
- update feature and browser tests to cover default availability instead
of flag enable/disable behavior
## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/RealEstateTest.php
- php artisan test --compact
tests/Feature/RealEstateAvailabilityTest.php
- php artisan test --compact tests/Browser/RealEstateAccountTest.php
- php artisan test --compact tests/Browser/OnboardingFlowTest.php
- php artisan test --compact --filter=\"can create a real estate account
linked to an existing loan\" tests/Browser/BankAccountsTest.php
## Notes
- `npm run build` completed the client build, but the Sentry sourcemap
upload step failed with an SSL/TLS error from the configured plugin
- deploy follow-up: run `php artisan pennant:purge real-estate` to
remove stale stored flag values
## Summary
- allow `/register?force=1` to bypass the hidden-auth redirect and
render the registration page
- preserve the `force=1` query on form submit so hidden signup still
works end-to-end
- enforce hidden-signup blocking in the Fortify user creation path and
cover the forced/unforced flows with tests
## Summary
- avoid sending bank transaction synced emails during 23:00-08:00 in the
user's local timezone by re-releasing the job until the next local 08:00
- deduplicate the daily bank transaction email by the user's local date
instead of the UTC date
- add feature coverage for quiet hours, first allowed local send time,
and local-day deduplication
## Testing
- php artisan test --compact
tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php
- vendor/bin/pint --dirty --format agent
## Notes
- existing 6-hour sync slots still leave at least one valid send slot
for every timezone
## Summary
- add concise caveman-mode communication rules at top of `AGENTS.md`
- keep existing agent guidance intact while making terse-response
behavior explicit
## Summary
- lock the transaction row before reconciling `budget_transactions` so
concurrent assignment work serializes per transaction
- retry the assignment transaction on deadlocks using Laravel's built-in
transaction attempts
- add regression coverage that asserts the deadlock retry path remains
configured for `PHP-LARAVEL-D`
## Testing
- php artisan test --compact
tests/Feature/BudgetTransactionServiceTest.php
tests/Feature/Listeners/AssignTransactionToBudgetTest.php
## Summary
- replace wipe-and-reinsert budget assignment with idempotent
reconciliation in `BudgetTransactionService`
- keep the fix scoped to the service layer instead of relying on queued
listener uniqueness or event metadata
- add regression coverage for duplicate reruns, stale assignment
cleanup, historical reruns, and label-only listener updates
## Testing
- php artisan test --compact
tests/Feature/BudgetTransactionServiceTest.php
tests/Feature/Listeners/AssignTransactionToBudgetTest.php
tests/Feature/TransactionTest.php
tests/Feature/BulkUpdateTransactionsTest.php
## Summary
- Fixes
[PHP-LARAVEL-9](https://whisper-money.sentry.io/issues/PHP-LARAVEL-9):
`ReferenceError: window is not defined` during Inertia SSR of
`CashflowPage`.
- `CashflowController` now reads + validates the `period` query param
(`YYYY-MM`) and passes it as an Inertia prop.
- Client `useState` initializer reads from `usePage().props.period`
instead of `window.location.search`, making SSR safe and avoiding
hydration mismatch.
- `useEffect` URL sync now compares against the prop, not
`window.location.search`.
## Why (c) not a `typeof window` guard
Server-provided params avoid hydration mismatch and keep URL as single
source of truth.
## Tests
New `tests/Feature/CashflowPageTest.php`:
- guest redirect
- no query param → `period` prop `null`
- valid `?period=2025-03` → prop `'2025-03'`
- invalid value → `null`
- malformed format (`2025-3`) → `null`
All 5 pass (50 assertions).
## Scope
Cashflow only. Other pages with similar `window.location.search` in
`useState` initializers (`onboarding/index.tsx`, `welcome.tsx`,
`auth/login.tsx`) left for follow-up.
Fixes PHP-LARAVEL-9
## Summary
- `demo:reset` was looking up the `ING` bank, but it was renamed to `ING
Direct` in production.
- The miss fell through to `Bank::factory()->create(...)`, which fails
in prod because Faker's `fake()` helper isn't available (dev-only
autoload).
## Fix
Update the name lookup in `ResetDemoAccountCommand` to `ING Direct`.
## Sentry
- Fixes PHP-LARAVEL-7 (root cause)
- Fixes PHP-LARAVEL-4 (scheduler wrapper)
## Summary
- remove the Pennant-based `open-banking` flag and middleware gating so
open banking is always available for authenticated users
- simplify landing, onboarding, settings, and subscription flows to use
always-on open-banking behavior and remove stale frontend/shared flag
plumbing
- update open-banking tests and purge stored `open-banking` Pennant rows
## Testing
- `php artisan test --compact
tests/Feature/OpenBanking/InstitutionControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/BinanceControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/IndexaCapitalControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/BitpandaControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/AuthorizationControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/ConnectionControllerTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/AccountMappingTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/OpenBankingFeatureFlagTest.php`
- `php artisan test --compact
tests/Feature/OpenBanking/SyncRetryAndLoggingTest.php`
- `php artisan test --compact tests/Feature/SubscriptionTest.php`
- `php artisan test --compact
tests/Feature/WelcomeBanksOrderingTest.php`
- `vendor/bin/pint --dirty --format agent`
## Notes
- `php artisan pennant:purge open-banking` was run to remove stale
stored values
- `php artisan test --compact tests/Browser/OnboardingFlowTest.php`
still has one unrelated real-estate onboarding browser failure (`it
creates a real estate account during onboarding when feature is
enabled`)
## Summary
- store browser-detected IANA timezones on users during registration and
share the value to the frontend
- silently backfill missing timezones for authenticated users without
overwriting existing saved values
- add focused feature coverage for registration, shared props, and the
timezone backfill endpoint
## Testing
- php artisan test --compact tests/Feature/Auth/RegistrationTest.php
tests/Feature/InertiaSharedDataTest.php
tests/Feature/Settings/TimezoneTest.php
- npm test -- resources/js/utils/currency.test.ts
## Summary
- add a per-connection cutoff timestamp so silent first/full sync
imports are excluded from later daily bank sync emails
- keep the existing one-email-per-day user cap while still reporting
transactions created after the silent sync cutoff
- add regression coverage for silent first sync, full sync, and
post-cutoff reporting behavior
## Testing
- php artisan test --compact
tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php
- vendor/bin/pint --dirty --format agent
## Summary
- create and finalize Sentry releases in CI using
`whisper-money@<git-sha>`
- bake the release into the production image so Laravel tags backend
events with the deployed version
- mark production deploys in Sentry and add a focused test covering the
release binding
## Testing
- php artisan test --compact tests/Feature/SentryConfigTest.php
## Summary
- remove the stale `horizon:snapshot` scheduled command that still runs
in production
- add a regression test to ensure scheduled commands do not include
Horizon snapshot
- keep production aligned with the current `queue:work` worker setup
## Summary
- sort bank names before building the daily synced-transactions email
payload
- make the queued mailable payload deterministic so the open banking job
test stops failing intermittently
- keep the email bank list order stable for users
## Testing
- php artisan test --compact
tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php
## Summary
- wire Laravel exception handling into Sentry via `bootstrap/app.php`
- add the `sentry_logs` logging channel and document production Sentry
env defaults
- keep local/example defaults disabled while enabling the production
example for logs, traces, and profiles
## Verification
- `vendor/bin/pint --dirty --format agent`
- `php artisan sentry:test` could not run locally after disabling the
local DSN, which is expected
- `php artisan test --compact tests/Feature/ExampleTest.php` currently
fails because the local Vite manifest is missing at
`public/build/manifest.json`
## Summary
- move bank transaction sync emails from per-connection inline sends to
a unique per-user daily job
- send at most one bank sync email per user per day while still
including all unreported enable-banking transactions since last reported
mail
- keep first-ever connection sync silent and add coverage for same-day
suppression and next-day catch-up emails
## Testing
- php artisan test --compact
tests/Feature/OpenBanking/SyncBankingConnectionJobTest.php
## Summary
- add month-end command to revoke old Enable Banking sessions for users
without a paid plan while keeping their linked accounts as manual
accounts
- extract banking disconnect flow into a reusable action so manual
disconnects and scheduled cleanup share same revoke and detach behavior
- add feature coverage for free-user cleanup, paid-user skips,
under-6-hour skips, non-Enable Banking skips, and revoke failure
fallback
## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact
tests/Feature/CancelFreeEnableBankingConnectionsCommandTest.php
tests/Feature/OpenBanking/ConnectionControllerTest.php
## Summary
- update subscription pricing to the final release amounts for monthly
and annual billing
- keep billing UI formatting aligned with the configured currency so
1.99 €/month displays correctly
- refresh subscription and Stripe sync tests to lock the new values in
place
## Summary
- **Auto-calculates Annual Revaluation %** (CAGR) on the frontend when
purchase price, purchase date, and current market value are all provided
— pre-fills the revaluation field while still allowing manual override
- **Generates historical monthly balance records** via linear
interpolation from purchase date to today when creating a real estate
account with complete purchase data
- New `RealEstateBalanceGeneratorService` handles balance generation
with dates on the 1st of each month (matching the existing
`ApplyRealEstateRevaluationCommand` convention)
## Changes
### Backend
- **`app/Services/RealEstateBalanceGeneratorService.php`** (new) —
Linear interpolation service that builds balance dates (purchase date,
1st of each intermediate month, today) and creates `AccountBalance`
records using `updateOrCreate`
- **`app/Http/Controllers/Settings/AccountController.php`** — Calls the
service after real estate detail creation when all three inputs are
present
### Frontend
- **`resources/js/components/accounts/account-form.tsx`** — Added CAGR
auto-calculation via `useEffect` with a `useRef` flag
(`isRevaluationManuallySet`) to avoid overwriting manual user input
### Tests
- **`tests/Feature/Services/RealEstateBalanceGeneratorServiceTest.php`**
(new) — 7 unit tests covering interpolation, edge cases (same-day
purchase, flat values, single month spans)
- **`tests/Feature/RealEstateTest.php`** — 6 new feature tests covering
historical balance generation through the controller (with/without
purchase data, same-day, flat values)
All 38 tests in `RealEstateTest.php` and all 7 service tests pass.
## Summary
- Adds `leads:resend-verification-emails` artisan command that
dispatches `VerifyUserLeadEmailNotification` to all leads where
`email_verified_at IS NULL`
- Supports `--dry-run` flag to preview the count without sending
- Follows the same pattern as `leads:retry-failed-jobs` (progress bar,
summary table)
## Test plan
- [ ] `--dry-run` shows correct count without dispatching
- [ ] Command dispatches exactly one notification per unverified lead
- [ ] Verified leads are skipped
- [ ] Early exit when no unverified leads exist
## Summary
- Adds `leads:retry-failed-jobs` artisan command to selectively retry
failed `emails`-queue jobs after a Resend rate-limit incident
- Retries jobs for verified leads and `VerifyUserLeadEmailNotification`
for unverified leads; forgets jobs for deleted leads (DDoS cleanup) or
unverified leads receiving waitlist emails
- Adds `deleteWhenMissingModels = true` to all lead mail/notification
classes so future jobs for deleted leads are silently discarded instead
of failing
## Usage
```bash
# Preview (no changes)
php artisan leads:retry-failed-jobs --dry-run
# Execute
php artisan leads:retry-failed-jobs
```
## Test plan
- [x] `leads:retry-failed-jobs` forgets jobs for deleted leads
- [x] `leads:retry-failed-jobs` retries jobs for verified leads
- [x] `leads:retry-failed-jobs` forgets waitlist jobs for unverified
leads
- [x] `leads:retry-failed-jobs` retries
`VerifyUserLeadEmailNotification` for unverified leads
- [x] `leads:retry-failed-jobs` handles mixed job types correctly
- [x] `--dry-run` does not modify `failed_jobs`
## Summary
- gate waitlist signup behind email confirmation so only verified leads
receive a queue position and referral code
- add signed lead verification routes, a check-email page, and a
dedicated verification email for the waitlist flow
- update lead syncing and feature tests so only verified leads are
exported to Resend and referral movement happens after verification
## Testing
- php artisan test --compact tests/Feature/UserLeadTest.php
- php artisan test --compact
tests/Feature/ResendSyncLeadsCommandTest.php
- php artisan test --compact tests/Feature/MailSenderTest.php
- vendor/bin/pint --dirty --format agent
## Summary
- remove the hardcoded Resend leads segment ID from tracked config
- keep `RESEND_LEADS_SEGMENT_ID` environment-driven for production use
- replace leaked test UUID references with a test-only placeholder
## Testing
- php artisan test --compact
tests/Feature/ResendSyncLeadsCommandTest.php
- vendor/bin/pint --dirty --format agent
## Summary
- add a `resend:sync-leads` command that syncs all `user_leads` into the
Resend leads segment
- make lead sync idempotent by creating contacts with the segment and
falling back to adding existing contacts to the segment
- schedule the command daily at `03:00` UTC and cover the
command/fallback behavior with Pest tests
## Testing
- php artisan test --compact
tests/Feature/ResendSyncLeadsCommandTest.php
## Summary
- add safe-area CSS variables for the viewport top and bottom insets
- apply safe-area-aware collision padding to the shared popover
primitive so iOS overlays stay below the notch
- add a focused regression test for the popover safe-area guard
## Testing
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/PopoverSafeAreaTest.php