Commit Graph

37 Commits

Author SHA1 Message Date
Víctor Falcón 27919027fe
chore: harden Inertia boundary, CI type-check, and test isolation (#640)
## Summary

Wave 1 hardening: privacy/security quick wins on the Inertia boundary, a
CI safety net, and stricter test isolation. Four focused changes plus
two review fixes, each in its own commit. No dependency or
product-behavior changes.

## Changes (by commit)

1. **Hide sensitive User fields from serialization** — `User::$hidden`
only covered password / 2FA / remember_token, leaving Cashier billing
columns (`stripe_id`, `pm_type`, `pm_last_four`, `trial_ends_at`) and
the legacy `encryption_salt` exposed in every serialized User, including
the Inertia-shared `auth.user` prop. None are read by the frontend and
Cashier keeps reading them server-side, so hiding them is invisible to
the UI and billing.
2. **Advisory frontend type-check in CI + duplicate `currency_code`
fix** — the CI linter job never ran `tsc`, so type errors accumulated
unseen. Adds a `Type Check Frontend` step running `bun run types`. It is
`continue-on-error: true` on purpose: the codebase already carries ~157
pre-existing `tsc --noEmit` errors, so gating on it now would turn CI
red on unrelated code. It surfaces type output today and should be
flipped to blocking once the backlog is cleared. Also removes a
duplicate `currency_code` member on the `User` TS interface (declared
twice, `CurrencyCode` and `string | null`); the TS language server flags
it, `tsc` masks it under `skipLibCheck`.
3. **Move residual-encryption cleanup out of Inertia `share()` into a
queued job** — `share()` is a shared-data provider and must be
read-only, but it ran a `DELETE`+`UPDATE` against the user on every
non-API web GET to purge the leftover encryption salt /
`EncryptedMessage` once a user had no encrypted data left. The existing
`encryption:*` commands do not cover this case (both target users who
*still* have encrypted data; this finalizes users who *finished*
decrypting). The work now goes to a new idempotent
`PurgeResidualEncryptionArtifactsJob` dispatched from `share()`,
preserving the eventual-cleanup semantics without writing during the
render.
4. **Block stray HTTP in the Feature test suite** — adds
`Http::preventStrayRequests()` in a Feature-scoped `beforeEach` so any
unfaked outbound request fails loudly instead of hitting the network. A
representative HTTP-touching subset (open banking,
exchange-rate/currency, AI categorization + AI/stats reports, analytics,
Discord, Stripe, bank logos) was run with the guard active; no test
relied on a real request, so no fakes had to be added.

### Review fixes (after the two-reviewer pass)

5. **Purge check uses `name_iv` source of truth, not the stale
`encrypted` flag** — the destructive purge decided "no encrypted
accounts" from `accounts.encrypted`, a flag the codebase already treats
as unreliable (see the
`align_accounts_encrypted_flag_with_plaintext_names` migration and
`FindsUsersWithLegacyEncryption`, which key off `name_iv`). An account
with an encrypted name but a stale `encrypted=false` flag could have its
key material destroyed. The job's guard now matches the canonical `*_iv`
predicate exactly; the loose flag gate in `share()` is kept only as a
cheap dispatch filter, and the job re-verifies with the safe predicate
before touching anything.
6. **Deduplicate purge dispatches with `ShouldBeUnique`** — `share()`
runs on every web GET, so an affected user re-enqueued the job on each
page load until a worker cleared the salt. The job is now
`ShouldBeUnique` keyed by user id, collapsing repeat dispatches into one
pending job.

## Test plan

- New/updated tests, all green:
- `InertiaSharedDataTest`: `auth.user` omits all sensitive fields; a web
GET no longer mutates the user inline and instead queues the cleanup
(and does not queue it when there is no salt).
- `PurgeResidualEncryptionArtifactsJobTest`: clears salt + message when
no `*_iv` data remains; keeps them when an encrypted transaction, an
encrypted account name, or a stale-flag-but-encrypted-name account
exists; no-op when salt already null.
- `StrayHttpRequestGuardTest`: an unfaked request throws
`StrayRequestException`; a matched fake still resolves.
- Green locally: `vendor/bin/pint --test`, `bun run lint` (0 errors),
`bun run format:check`, `bun run test` (254 frontend tests), targeted
backend
`--filter=InertiaSharedData|PurgeResidualEncryptionArtifactsJob|StrayHttpRequestGuard|Encryption`
(24 tests). Ran ~700 HTTP-touching Feature tests with the stray-request
guard active with no guard-induced failures.
- `bun run types` still reports the ~157 pre-existing errors (unchanged
set; this PR adds none) — that is exactly why the CI step is advisory
for now.

## Reviewer findings — addressed vs deferred

**Addressed**
- 🟠 Destructive purge keyed off the stale `accounts.encrypted` flag
instead of the `name_iv` source of truth → fixed in commit 5 (job now
mirrors `FindsUsersWithLegacyEncryption`; added a regression test for
the stale-flag case).
- 🟡 Per-request dispatch amplification with no dedup → fixed in commit 6
via `ShouldBeUnique`.

**Deferred (with rationale)**
- 🟠 "Three divergent copies of the legacy-encryption query" —
substantively resolved: the job now matches
`FindsUsersWithLegacyEncryption` exactly. The only remaining
`encrypted`-flag use is the `hasEncryptedAccounts` **UI prop** in
`share()`, which is a separate, pre-existing frontend concern; changing
it would alter which accounts the UI treats as encrypted and is out of
scope here. A full extraction into one shared scope would require
restructuring the trait (it builds a `User` query, not a per-model
boolean) and is not a Wave 1 quick win.
- 🟢 Redundant `->fresh()` / null guard in the job — kept deliberately:
it is the idempotency guard that makes the re-check read committed state
on the sync path (the second reviewer credited it as what makes repeat
dispatches safe).
- 🟢 `continue-on-error` shows the type-check step green — acknowledged;
flipping to blocking (or failing on an increase over a committed
baseline) is the follow-up once the ~157-error backlog is cleared.
- 🟢 (Product review) Theoretical one-request SSR/client
`hasEncryptionSetup` diff from async salt clearing — invisible in
practice (the lock button gates on `hasEncryptedAccounts ||
hasEncryptedTransactions`, false in both SSR and client), and `ssr.tsx`
is untouched. No action.

Product-bug reviewer verdict: no user-facing regressions. Hiding the 5
fields does not affect Cashier (raw attribute access),
`EncryptionController`, notifications, or any API/JSON path; the
`currency_code` dedup is runtime-identical; the cleanup timing change is
client-absorbed.
2026-07-04 18:57:58 +00:00
Víctor Falcón 0ea30a3ce8
ci: remove production deploy job (#574)
Removes the `deploy` job from CI. Deployment is no longer triggered from
GitHub Actions.

The job handled the Coolify webhook trigger and Sentry release tracking
on pushes to `main`. Build/test/lint/static-analysis and the image build
jobs are untouched.
2026-06-20 18:17:14 +00:00
Víctor Falcón 57747ef34c
ci: simplify Coolify deploy trigger to a single curl (#573)
## What

Replace the ~60-line bash retry loop in the deploy step with native curl
flags.

## Why

The old block reimplemented in shell what curl does natively (timeouts,
retries, error handling), plus manual body/timing parsing with a marker
separator.

- `--max-time 15` caps each attempt so the job can't hang for ~20 min on
an unreachable box.
- `--retry-all-errors` turns a timed-out attempt into the next retry (a
plain `--retry` doesn't always treat exit 28 as retryable).
- `--fail` keeps a webhook error (4xx/5xx) from passing the step
silently.

## Notes

- Now uses the `COOLIFY_WEBHOOK_URL` secret instead of
`DEPLOYMENT_TOKEN` + hardcoded uuid URL. **This secret must exist in
GitHub Actions before merge or the deploy breaks.**
- Drops the 10–20 min retry waits that gave a prior rebuild time to
finish. Fine if the webhook queues the deploy; add back if it rejects
requests during an in-progress rebuild.
2026-06-20 17:40:09 +00:00
Víctor Falcón 2a0d6c8258
ci: deploy webhook 3 attempts with 10/20 min backoff (#489)
Reduce deploy webhook retries to 3 attempts with longer waits so a slow
Coolify rebuild can finish before giving up.

- Attempt 1 → fail → wait 10 min
- Attempt 2 → fail → wait 20 min
- Attempt 3 → fail → give up

Total window ~30 min (was 10 attempts / ~7 min). Connection-level
timeouts (curl exit 28) were exhausting retries before the box came
back.
2026-06-04 14:14:22 +02:00
Víctor Falcón e3cbbb2e0d
ci: make deploy webhook curl robust and observable (#484)
## Problem

The deploy step intermittently fails on CI with only:

```
Response:
HTTP Status: 000
Deployment request failed with status 000
```

`000` means curl never completed a connection — the Coolify box (which
rebuilds the image from git on deploy) has its API unreachable while a
prior build runs. But the step gave no way to confirm that: `-s`
silenced curl's error message and `|| true` swallowed the exit code.

## Changes

- **Surface the real cause** — `-sS` plus `--write-out
%{exitcode}`/`%{errormsg}`/timing, so failures print the curl exit code
(7 refused, 28 timeout, …) and message instead of a blind `000`.
- **Robust parsing** — body/metadata split on an explicit
`===CURL_META===` marker instead of fragile `tail`/`sed` line counting.
- **No false timeouts** — `--max-time` raised 120→300s so a
slow-but-alive box doesn't abort mid-trigger and report a phantom `000`.
- **Wider retry window** — 10 attempts with backoff capped at 60s (~8
min) instead of 5 with a 240s dead sleep, to ride out a multi-minute
rebuild.
- **Fail fast on 4xx** — a bad token/uuid is not retryable, so don't
waste the window on it.

## Note

This makes the trigger robust and observable; the structural fix (let
Coolify pull the prebuilt ghcr image instead of rebuilding on the box)
is out of scope here.
2026-06-04 08:46:18 +02:00
Víctor Falcón 364c72db72
fix: build arm64 images on native runners (#419)
## Summary
- build arm64 Docker images on native GitHub arm runners instead of QEMU
- create Sentry release in deploy job so deploy no longer races registry
publishing

## Verification
- php artisan test --compact tests/Feature/SentryConfigTest.php
- vendor/bin/pint --dirty --format agent
- yq parsed .github/workflows/ci.yml
- git diff --check
2026-05-22 16:05:48 +01:00
Víctor Falcón 44116010de
fix: publish arm64 docker images asynchronously (#418)
## Summary
- publish arm64 Docker images after amd64 image build
- keep deploy independent from Docker image publishing
- publish multi-platform manifests for development and production tags

Closes #412

## Verification
- yq parsed .github/workflows/ci.yml
- git diff --check
2026-05-22 15:18:22 +01:00
Víctor Falcón b66f9e29bc
ci: cap Docker image build time (#405)
## Summary
- cap Docker image publish job and build steps
- stop multi-arch QEMU builds; publish amd64 only
- cache production Docker dependency layers before app copy
- add tests that guard CI timeout/platform behavior

## Verification
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/CiDockerBuildTest.php
2026-05-20 09:45:12 +01:00
Mouad Jaouhari 5c74292448
fix: publish and use production Docker image (#393)
## Summary
  - publish a GHCR production image built from `Dockerfile.production`
- point production Docker Compose and Coolify template to `:production`
- add `.dockerignore` so host `.env`, `vendor`, and `node_modules` do
not leak into image builds
  - clarify Docker self-hosting docs and Compose log command

  ## Verification
- `docker build -f Dockerfile.production -t
whisper-money:production-test .`
  - `docker compose -f docker-compose.production.yml config --images`
  - `vendor/bin/pint --dirty --format agent`
  - `vendor/bin/pest tests/Unit/DockerProductionImageTest.php`

---------

Co-authored-by: Víctor Falcón <victoor89@gmail.com>
2026-05-20 07:19:31 +00:00
Víctor Falcón f4ab4a1989
Fix Sentry release commit detection in image build (#371)
## Summary
- add --ignore-missing to Sentry set-commits so build-image does not
fail when previous release SHA is unavailable
- cover the workflow command in Sentry config test

## Tests
- vendor/bin/pint --dirty --format agent
- php artisan test --compact tests/Feature/SentryConfigTest.php
2026-05-09 16:51:51 +01:00
Víctor Falcón e36d6f3e16
Speed up PR CI browser path (#365)
## Summary
- rebalance Browser tests with explicit class-filter shards
- build PR browser assets inside shards, removing the build-assets
dependency gate
- run Browser shards with Pest parallelism ()

## Autoresearch metric
- baseline modeled PR critical path: 406.50s
- final modeled PR critical path: 234.27s
- modeled improvement: 172.23s (42.4%)

## Verification
- ran METRIC ci_total_s=234.270
METRIC actual_recent_pr_total_s=422.000
METRIC build_assets_s=0.000
METRIC tests_s=170.000
METRIC browser_matrix_s=232.270
METRIC linter_s=60.000
METRIC static_analysis_s=26.500
METRIC performance_tests_s=63.000
METRIC job_count=13.000
METRIC browser_shards=6.000 for each experiment
- coverage guard in autoresearch script checks Browser filters cover all
recent Browser classes exactly once

## Notes
- wall-clock faster; runner minutes likely higher due extra shards and
duplicate asset builds
- real CI should validate Browser parallelism flake risk
2026-05-07 20:40:13 +01:00
Víctor Falcón 21b5692174
fix: include production Dockerfile in deploy filter (#350)
## Summary
- include Dockerfile.* in CI changes filter
- ensure Dockerfile.production-only commits build and deploy

## Verification
- python3 assertion confirms Dockerfile.* filter exists and matches
Dockerfile.production
2026-05-04 15:36:56 +01:00
Víctor Falcón 0815548ac9
ci: tier 1 + tier 2 pipeline speedups (#342)
Continues the work merged in #341 with two focused commits.

## Baseline vs result (real numbers from runs on this PR)

| Job | Before (last run on #341) | After | Notes |
|---|---|---|---|
| build-assets | 70s (gates 3 jobs) | 74s | unchanged |
| tests | 215s | 243s | now starts at t=0 (no gate) |
| performance-tests | 72s | 64s | now starts at t=0 (no gate) |
| browser-tests longest shard | 341s | 323s | -18s |
| linter | 61s | 63s | same |
| static-analysis | 29s | 23s | -6s |
| **Wall (critical path)** | **~7m** | **~6.6m** | **-25s on the
critical path** |

The critical path is still `build-assets → browser-tests-matrix`. Real
wall-time wins for that path require either smarter shard balancing or a
Playwright Docker container — both deliberately deferred (see below).

## Tier 1 — wall-time wins (commit 1)

- **Pest:** `withoutVite()` is now applied globally in the `Feature` and
`Performance` suites via `pest()->beforeEach(...)->in(...)`. Those
suites never assert on the JS bundle, so they no longer need a compiled
Vite manifest.
- **Drop `needs: build-assets`** from `tests` and `performance-tests`.
Both now start at t=0 instead of waiting ~70s for the artifact. Browser
tests still gate on it (real Playwright session needs the manifest).
- **Playwright:** install only `chromium` (no firefox/webkit) since
that's all pest-plugin-browser uses. Saves ~15s of system-dep install
per shard.
- **Drop `actions/setup-node`** from browser-tests-matrix;
`oven-sh/setup-bun` already provides node for `npx playwright`.

I also tried bumping browser shards 4 → 6 but reverted (commit 3):
pest-plugin-browser's `--shard=N/M` distributes very unevenly at M=6 in
this codebase — shard 6 ended up running the entire 106-test suite
(13m26s) while other shards ran 23 tests each. Reverted to 4 shards for
predictable balance. Better balancing needs a test-time-aware split
(follow-up).

## Tier 2 — caching + setup dedup (commit 2)

- New composite actions:
- `.github/actions/setup-php-deps`: PHP + composer cache + `vendor/`
cache. On a `vendor/` cache hit, `composer install` is skipped entirely.
- `.github/actions/setup-bun-deps`: Bun + bun cache + `node_modules/`
cache. On a hit, `bun install` is skipped entirely.
- All 6 jobs refactored to use the composite actions. Removes ~150 lines
of duplicated YAML.
- **Bug fix: Pint cache key** dropped `${{ github.sha }}` from the
primary key so it actually reuses across runs (was effectively 0%
reuse).

## Deliberately not included

- **Pest `--parallel`**: paratest's per-process DB suffix conflicts with
Testcontainers user grants (see revert in 82e9a77 on the parent branch).
Worth revisiting in a follow-up that either widens grants or overrides
`ParallelTesting::setUpProcess`.
- **Playwright Docker container** (`mcr.microsoft.com/playwright`):
would eliminate the 34s system-deps install entirely but couples PHP
setup to a custom container; risky for one PR.
- **Test-time-aware browser shard split**: only real way to actually
shrink the critical path further given pest-plugin-browser's current
sharding behavior.

## Verification

- Final CI run on this PR is green (one flaky `Create Category` browser
timeout that passed on rerun — pre-existing flake unrelated to these
changes).
- `php artisan test --filter=InertiaSharedDataTest` and
`--filter=BudgetPeriodDateTest` pass with `public/build` deleted,
confirming the global `withoutVite()` covers feature tests that
previously rendered Inertia pages.
- All YAML files parse cleanly.
- `vendor/bin/pint --dirty` passes.
2026-05-01 19:07:04 +02:00
Víctor Falcón b94a285aad
ci: speed up pipeline (#341)
## Why

Last CI run on a PR took **~16m 36s** wall (browser-tests dominated).
Total CPU ~34 min. Several easy wins identified.

## Baseline (run 25169327223)

| Job | Wall | Critical step |
|---|---|---|
| browser-tests | 16m 36s | Browser Tests 718s, Build 166s, Playwright
50s |
| tests | 9m 38s | Tests 334s, Build 173s |
| performance-tests | 4m 19s | Build 173s |
| linter | 2m 54s | Pint 109s |
| static-analysis | 36s | — |

## Changes (one commit each)

**A. Shard browser tests 4×** — `pest --shard=N/4` matrix. ~12 min → ~3
min critical path.

**B. Build assets once, share via artifact** — new `build-assets` job
uploads `public/build`; tests/browser-tests/performance-tests download
it. Saves ~340s of duplicated CPU.

**C. Disable xdebug in tests job** — `coverage: xdebug` was set but
`--coverage` never passed. Silent ~30-50% slowdown.

**D. Parallel Pest with Testcontainers** — `pest --parallel
--processes=4`, `TESTCONTAINERS=true` so each worker gets isolated
MySQL. Drops job-level mysql service.

**E. Cache composer + bun deps** — `actions/cache` for composer cache
dir and `~/.bun/install/cache` keyed on lockfiles. Saves 30-60s/job
warm.

**F. Cache Playwright browsers** — cache `~/.cache/ms-playwright` keyed
on bun.lock; on hit only runs `install-deps`. Saves ~50s.

**G. Pint parallel + cache** — `--parallel --cache-file=.pint.cache`
with persisted cache. ~109s → ~5-10s warm.

## Expected wall time after

Browser shards become critical path: **~6-7 min** (down from 16-17 min).

## Risks

- **D**: 4 paratest workers each spawn a MySQL container — watch
RAM/runner load. Drop to `--processes=2` if flaky.
- **A**: more concurrent runners. Adjust shard count if MySQL service
init becomes the new bottleneck per shard.
- **B**: `build-assets` is now a hard dep for
tests/browser-tests/performance-tests.

## Test plan

CI itself is the test. Watch this PR run, compare timings to baseline.
2026-04-30 14:52:33 +00:00
Víctor Falcón af7ba727d5
ci: drop tests/** from code paths filter (#336) 2026-04-26 10:48:25 +02:00
Víctor Falcón 405e6bfc29
ci: skip build and deploy when no code changes (#335)
## Why

Pushes to `main` that only modify workflows, README, or docs were
triggering production deploys.

## What

- Adds a `changes` job using `dorny/paths-filter@v3` to detect whether
the push touched application code.
- Gates `build-image` and `deploy` on `needs.changes.outputs.code ==
'true'`.

## Code paths considered

`app/`, `bootstrap/`, `config/`, `database/`, `public/`, `resources/`,
`routes/`, `tests/`, `storage/`, `artisan`, `composer.{json,lock}`,
`package*.json`, `bun.lock*`, `vite.config.*`, `tsconfig*.json`,
`eslint.config.*`, `.prettierrc*`, `phpstan.neon*`, `phpunit.xml*`,
`pint.json`, `Dockerfile`, `docker/**`, `.dockerignore`, `.env.example`.

Anything else (e.g. `.github/**`, `*.md`, `docs/**`) is treated as
non-deployable.
2026-04-26 09:15:06 +01:00
Víctor Falcón bfe1af3c83
fix(ci): order sentry deploy after build (#309)
## 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
2026-04-20 13:35:07 +01:00
Víctor Falcón 64ec047769
chore: track Sentry releases in CI (#294)
## 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
2026-04-16 08:04:59 +01:00
Víctor Falcón 6daa0d7345 ci: increase deploy retry delay to exponential backoff 2026-03-26 15:29:00 +01:00
Víctor Falcón b36197e76b fix(ci): skip outdated production deploys 2026-03-17 12:19:26 +01:00
Víctor Falcón cd40bc75d9
fix(ci): allow deploy retry loop to survive curl timeout (#233)
## Summary

- Adds `|| true` after the `curl` command so `bash -e` does not kill the
script on curl exit code 28 (timeout) before the retry loop can continue
- Guards the HTTP status integer comparison against an empty value,
which occurs when curl times out and produces no output
2026-03-17 09:57:38 +00:00
Víctor Falcón 3cbca1a17d
ci: add retry with exponential backoff to deploy step (#212)
## Summary

- Replaces curl's built-in `--retry` with a shell loop that retries up
to 5 times on any non-2xx response (not just connection failures)
- Delays increase with each attempt: 10s → 20s → 30s → 40s between
retries
- Exits immediately on success so successful deploys are unaffected
2026-03-07 14:18:14 +00:00
Víctor Falcón 3e087bdcd7
fix(static-analysis): clear phpstan-baseline by fixing all suppressed errors (#183)
## 🚪 Why?

### Problem

PHPStan was running with a baseline of 56 suppressed errors, meaning
static analysis was not enforcing type safety across a significant
portion of the codebase. These errors were real type mismatches,
redundant null-safety operators, and incorrect PHPDoc annotations that
could mask bugs and make the code harder to reason about.

## 🔑 What?

### Changes

- Add `@property` PHPDoc annotations to `Account`, `BankingConnection`,
`ExchangeRate`, and `Transaction` models so Enum casts and typed columns
are visible to PHPStan
- Add `instanceof User` guards in `ScheduleDripEmailsListener`,
`SyncUserToResendListener`, and `FortifyServiceProvider` to properly
narrow `Authenticatable` to `App\Models\User`
- Remove redundant `?? false` and unnecessary nullsafe `?->value` in
`HandleInertiaRequests`
- Fix `SyncBankingConnectionJob`: use `->name` instead of `?->name` on
an always-loaded `bank` relation
- Remove `is_countable()` guard in `BalanceLookup` (parameter is always
`Collection|array`, both countable)
- Remove `?? []` / `?? default` fallbacks on fully-typed array keys
across `BalanceSyncService`, `BinanceBalanceSyncService`,
`BinanceClient`, `BitpandaBalanceSyncService`, `BitpandaClient`,
`IndexaCapitalClient`, `IndexaCapitalBalanceSyncService`, and
`AuthorizationController`
- Fix `BinanceClient::publicClient()` `retry()` call: use `when:` named
argument and `\Throwable` type hint to match `PendingRequest::retry()`
signature
- Update `IndexaCapitalClient::getPerformance()` `@return` to include
`portfolios` and `net_amounts` keys; simplify sync service to remove
dead null checks
- Replace nullsafe chain with ternary in `BudgetPeriodService`
- Replace `match` statement in `SetupMainUser` with `if/else` to
eliminate always-true comparison
- Clear `phpstan-baseline.neon` entirely (was 56 suppressed errors, now
0)

##  Verification

### Tests

- Existing tests pass: PHPStan level 5 reports 0 errors with empty
baseline
2026-03-02 12:22:30 +00:00
Víctor Falcón e4243c2eaa
feat: use testcontainers for isolated MySQL in test runs (#153)
## Summary

- Add `testcontainers/testcontainers` to `require-dev` to spin up an
ephemeral MySQL 8.0 container per test process, eliminating the need for
a local database or Docker Compose stack
- Create `tests/bootstrap.php` that starts a `MySQLContainer`, sets
`DB_*` env vars dynamically, and auto-generates `APP_KEY` when missing
(supports fresh worktrees and CI)
- Update `phpunit.xml` to use the new bootstrap file and remove the
hardcoded `DB_DATABASE` env var

## How it works

Running `php artisan test` now automatically starts a short-lived MySQL
container on a random port. Each test process gets its own isolated
database, so multiple agents, worktrees, or CI jobs can run in parallel
without conflicts.

Set `TESTCONTAINERS=false` to bypass containers and use the database
configured in `.env` instead.
2026-02-25 10:14:20 +01:00
Víctor Falcón 93f1f82ac5
test: add performance test suite with query count ceilings (#148)
## Why

### Problem

As new features are added, dashboard and settings pages have been
accumulating extra database queries — risking N+1 regressions and
degraded response times. There was no automated way to detect these
regressions before they reach production.

## What

### Changes

- **New `Performance` test suite** (`tests/Performance/`) with 25 tests
that enforce query count ceilings on every page and API endpoint
accessible from the sidebar
- `PageQueryCountTest.php` — 15 tests covering Dashboard, Accounts,
Transactions, Budgets, Cashflow, and all Settings pages, plus scaling
tests that prove query count stays constant as data volume grows
- `ApiQueryCountTest.php` — 10 tests covering all Dashboard and Cashflow
analytics API endpoints, plus scaling tests
- **Separate CI job** (`performance-tests`) that runs in parallel with
`tests` and `linter`, and gates both `build-image` and `deploy`
- **Excluded from the main `tests` job** to avoid running them twice
(`--exclude-testsuite=Performance`)
- Helper functions (`performanceSeedUser`, `countQueries`,
`assertMaxQueries`) added to `tests/Pest.php` for reuse
- `phpunit.xml` updated with the new `Performance` testsuite entry

### How it works

Each test seeds a user with realistic data (3 accounts, 30 transactions,
15 balances, 5 categories, 3 labels, 1 budget) and asserts the endpoint
executes **at most N queries**. Thresholds have a small buffer (~3
queries) above current counts — tight enough to catch N+1 regressions
(which add dozens of queries) but loose enough to not break on minor
legitimate changes. On failure, every executed SQL query is dumped for
easy debugging.

### Run locally

```bash
php artisan test --testsuite=Performance
```

## Verification

### Tests

All 25 performance tests pass. Existing Feature (633) and Unit (22)
suites unaffected.
2026-02-24 10:47:51 +01:00
Víctor Falcón 39ef165655 Add production Docker setup for easy self-hosting with the CI-built image (#42)
- Add production Docker setup for easy self-hosting with the CI-built
image
- Build multi-architecture Docker images (amd64 + arm64)

### Changes

  Docker Production Setup
- Add docker-compose.production.yml for running the production image
locally
- Add docker/entrypoint.sh with automatic setup (migrations, caching,
storage, APP_KEY generation)
  - Add HEALTHCHECK to Dockerfile for container health monitoring
  - Add .env.production.example with documented production defaults

### CI/CD
  - Add QEMU for multi-arch builds
  - Build Docker images for both linux/amd64 and linux/arm64

### Coolify / WIP
  - Add templates/coolify/whisper-money.yaml one-click template
  - Includes MySQL service with health checks
  - Auto-generates credentials using Coolify's SERVICE_* variables

### Documentation
  - Update README with production Docker instructions
  - Add environment variable reference table
2025-12-30 07:22:19 +01:00
Víctor Falcón 9b5158dec2 docker: add new build-image step on github ci 2025-12-30 07:22:19 +01:00
Víctor Falcón c5df59c285 feat: add multiple chart view modes for net worth evolution (#37)
## Summary

- Add four view modes for net worth and account balance charts:
- **Stacked Accounts**: existing stacked bar chart showing balances by
account
  - **Line**: net worth line chart with linear/log scale toggle
  - **Change**: bar chart showing MoM%, YoY%, Rolling 12M changes
- **Waterfall**: bridge chart showing Start -> Change -> End transition

- Add Vitest for frontend unit testing with 52 tests covering all
calculation functions
- Proper sign handling for liabilities (credit cards and loans subtract
from net worth)
- Log scale disabled with warning when net worth includes zero or
negative values
- Shared components and logic between dashboard and account detail
charts

## New Files

- `vitest.config.ts` - Vitest configuration
- `resources/js/lib/chart-calculations.ts` - Pure computation functions
- `resources/js/lib/chart-calculations.test.ts` - 52 unit tests
- `resources/js/hooks/use-chart-views.ts` - Shared chart view state hook
- `resources/js/components/charts/` - Reusable chart components

## Test plan

- [x] Dashboard net worth chart shows view toggle and all four views
work
- [ ] Account detail page chart shows view toggle and three views work
(no stacked)
- [x] Line chart scale toggle works (log disabled with warning when
applicable)
- [x] Change chart series toggle (MoM, YoY, 12M EUR, 12M %) works
- [x] Waterfall chart month selector works
- [x] All 52 unit tests pass (`bun run test`)
- [x] Build succeeds (`bun run build`)
- [x] Dark mode styling correct
2025-12-30 07:22:19 +01:00
Víctor Falcón 44860941ee undo deploy ci check 2025-12-08 18:31:03 +01:00
Víctor Falcón d4410a67fe fix: deploy ci 2025-12-08 18:27:39 +01:00
Víctor Falcón 4ad965b44e Fix ci deploy 2025-12-08 18:20:22 +01:00
Víctor Falcón aaaba66684 ci: skip deployment if there is another HEAD deploy in queue 2025-12-08 18:14:19 +01:00
Víctor Falcón a13253d32b ci.yml: Remove browser-tests dependency 2025-12-04 10:11:34 +01:00
Víctor Falcón ce3527a2f9 Add GitHub Actions workflow for pull request testing 2025-12-04 10:04:45 +01:00
Víctor Falcón 8a0d27a9df
Execute browser tests on CI (#10) 2025-12-03 16:26:30 +01:00
Víctor Falcón 7773c97b0a Add retry mechanism to GitHub Actions workflow 2025-11-26 13:05:50 +01:00
Víctor Falcón e39336bf76 Add GitHub Actions CI/CD pipeline for Laravel project 2025-11-26 12:35:23 +01:00