Adds nixConfig substituter/key for the invoiceninja-nixos cachix cache,
a `just push-cache` target to build and push the package and automated
test to it, and exposes the UI frontend build as a passthru output
(`invoiceninja.ui`) so it can be built and pushed independently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The .initial-migration marker file prevented `migrate --force` from ever
running again after the first activation, so migrations shipped by a later
invoiceninja version upgrade would never apply to an already-deployed
instance. `migrate` is idempotent (Laravel tracks applied migrations in the
DB), so just run it unconditionally on every activation instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
snappdf only ever shells out to `$SNAPPDF_CHROMIUM_PATH --headless --print-to-pdf=...`,
so the full browser UI/GPU stack pkgs.chromium ships is unnecessary. Google's official
"Chrome for Testing" headless-only build (packaged by nixpkgs' playwright-driver,
selected via `browsers.override` with every other browser/ffmpeg disabled) does the
same job for a ~1.45GB smaller closure (431MB vs 1.88GB), needs no allowUnfree, and
PDF generation is noticeably faster in the VM test (~2.5s vs ~10s per request).
Drops the now-unnecessary allowUnfree wiring from the interactive VM and automated
test configs, which existed solely to build pkgs.chromium.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
composer.lock and vendor/composer/installed.json record each package's
"dist.url" as the store path of the build-time composer-repository
mirror, which drags that whole ~570MB fixed-output derivation into the
runtime closure even though it's just install provenance -- neither
Laravel's package manifest (name/extra only) nor Composer's runtime
InstalledVersions (reads installed.php, not this file) consult it.
Cuts the package closure from ~1.75GB to ~1.18GB.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extracts small api_get/api_post helpers to cut the repeated curl/header/
json.loads boilerplate, then uses them for a new subtest that creates a
client, invoices it, marks the invoice sent, and pays it in full - checking
the invoice comes back paid with a zero balance. Runs after the PDF preview
check, since PreviewController::blankEntity only builds its mock PDF when
the company has no invoices/invitations yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`just boot-vm` forwards the guest's port 80 to host port 8080
(QEMU_NET_OPTS in the justfile), but the backend builds absolute URLs
(e.g. the stored company logo path returned by the upload API) from
APP_URL, which defaults to plain "http://localhost" with no port. The
SPA's own relative URLs are fine - they inherit whatever port the
browser is already on - but any backend-generated absolute URL then
points at host port 80, where nothing is listening, instead of the
forwarded 8080. That gave a silent net::ERR_CONNECTION_REFUSED for the
logo image specifically, even though the upload itself succeeded.
Verified directly: with APP_URL overridden to "http://localhost:8080",
a freshly uploaded logo's URL is
"http://localhost:8080/storage/.../....png" instead of missing the
port. Automated test checks are unaffected (checks derivation hash
unchanged) since they curl the guest's own localhost directly, with no
host-side port remap involved.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Root-caused a "logo upload hangs, connection refused in browser
console" report: the qemu-vm module's defaults (1 core, 1024MB) are
too tight for this stack (MySQL + Redis + PHP-FPM + chromium for PDF
generation, each PDF request spawning a full multi-process browser
instance) under real concurrent load, and this VM boots with
panic_on_oom - so exhausting memory doesn't degrade gracefully, the
whole VM goes down and every connection starts refusing.
Reproduced directly: at the 1024MB default, even 2 concurrent PDF
generation requests triggered `redis-server invoked oom-killer` and a
kernel panic. Bumped to 2 cores / 2560MB, which comfortably survives
that same load (~1.4GB still free afterward) - verified via the exact
concurrent-request scenario that crashed the 1024MB config, not just
absence of errors.
virtualisation.cores/memorySize aren't options on a plain nixosSystem
in this nixpkgs (only nixosTest nodes get the qemu-vm module
automatically), so this also adds an explicit import of it.
Documented the underlying resource cost (chromium is a full browser
per PDF request, pm.max_children=8 means up to 8 can run concurrently)
in CLAUDE.md as an operational note for real deployments, not just
this test VM.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Exercises the chromium/snappdf pipeline via POST /api/v1/preview with
no entity, which falls back to PdfMock's self-contained mock PDF -
verified this needs no client/invoice test data, since a fresh account
has neither. Asserts the response starts with the %PDF magic bytes.
This was previously the biggest untested path in the module: an actual
binary runtime dependency (chromium) rather than just config, and the
kind of thing that silently breaks in a minimal NixOS closure.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
recommendedProxySettings only adds proxy_* directives (proxy_redirect,
proxy_connect_timeout, proxy_set_header Connection, etc.), which only
take effect on locations using proxy_pass. This vhost only ever uses
fastcgi_pass for PHP, so the setting was inert here.
recommendedOptimisation (sendfile/tcp_nopush/tcp_nodelay/keepalive)
does apply regardless of proxy_pass vs fastcgi_pass, so it's kept.
Verified via the full automated test suite (both proxy backends).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Migration 2025_08_31_224356_additional_languages_af_ca_id.php runs
during `migrate`, before db:seed's LanguageSeeder has populated the
other 42 rows of the languages table, and does
Cache::forever('languages', Language::all()) - permanently caching
just the 3 languages (Afrikaans, Catalan, Indonesian) it sees at that
moment. StaticServiceProvider checks this cache key before ever
querying the DB again, so every fresh install's language dropdown
(e.g. the "Welcome to Invoice Ninja" company setup wizard) was stuck
showing only those 3 entries, in their own native-language spelling
(ctrans() translates each name using the active locale).
Adds an unconditional `invoiceninja-manage cache:forget languages` to
invoiceninja-data-setup.service, and a regression test asserting
/api/v1/statics returns more than 40 languages. Verified directly:
before the fix, a fresh account's statics call returned exactly 3
languages; after, it returns all 45.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Caddy strictly matches vhosts on the exact Host header, unlike nginx
where a single vhost acts as the default regardless of Host. With
hostname = "localhost" (the local-dev default), visiting the VM via
http://127.0.0.1:PORT instead of http://localhost:PORT sent a Host
header matching no vhost, and Caddy silently returned an empty 200 OK
- a genuine white screen with no error anywhere (not in the console,
not in the network tab).
Adds serverAliases for 127.0.0.1:80 and [::1]:80 to the caddy vhost
when hostname is "localhost", so all three ways of reaching the local
VM resolve to the same site block. Verified directly: before this
fix, curl with `Host: 127.0.0.1` got an empty response while `Host:
localhost` got the full app shell; after, all three (localhost,
127.0.0.1, [::1]) return the same 200 response.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Laravel's own CORS middleware already sends a complete, correct set of
Access-Control-* headers on every response. The nginx and caddy vhost
configs were additionally setting a second, static set of the same
headers, producing duplicates with conflicting values (e.g. two
Access-Control-Allow-Methods headers, one "*" and one the app's real
list). Browsers reject responses with duplicate/conflicting CORS
headers - curl-based checks never noticed since only browsers enforce
CORS header validation, but it broke real browser usage (login/API
calls failing silently).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Parameterizes tests/common.nix and tests/default.nix on proxyServer
(default "nginx"), giving flake.nix two nixosConfigurations
(test-nginx/test-caddy) and `just build-vm [proxy]` a way to pick
between them for interactive testing.
The automated nixosTest now boots one node per proxy backend and runs
the full assertion suite (factored into check_invoiceninja()) against
each, so a regression in either nginx or caddy's config gets caught.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Invoice Ninja relies on Laravel's scheduler for recurring invoices,
scheduled reports, and other internal cron-driven jobs. Without a
systemd unit invoking `artisan schedule:run` periodically, the app's
dashboard reports "cron jobs are not enabled" after login.
Adds invoiceninja-scheduler.timer (every minute, matching upstream's
documented cron cadence) and its oneshot service, and extends the
automated test to assert the timer is active and schedule:run succeeds.
Note: the app's own is_scheduler_running flag only flips on a 5-minute
wall-clock boundary (app/Console/Kernel.php's everyFiveMinutes() job),
so the dashboard warning can take a few minutes to clear after boot -
confirmed working via the "Refresh Data" button in that same modal.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a pkgs.testers.nixosTest (checks.<system>.invoiceninja) that boots
the module end-to-end and checks that mysql/redis/phpfpm/queue-worker/
nginx come up, the one-time migrate/seed/admin-create setup completed,
the app shell and /login page are served, invoiceninja-manage works,
and the seeded admin can log in via the API.
Factors the shared services.invoiceninja config used by both the
interactive VM and the new test into tests/common.nix, and satisfies
NixOS's fileSystems/bootloader assertions in the interactive VM config
so `nix flake check` evaluates nixosConfigurations.test cleanly (the
qemu-vm module already overrides both when actually building
config.system.build.vm, so this only affects the toplevel-eval path).
Also adds a `just test` recipe, a CLAUDE.md codebase overview, and a
.gitignore for the result/nixos.qcow2 build artifacts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This makes testing/debugging easier by allowing to login as root
directly. The previous test system configuration added a step to this
process by forcing devs to login as test and use sudo to escalate to
root.
Having the declaration of system.stateVersion just adds more
maintainance for devs. For example, changing the nixpkgs value in
flake.nix requires a change of system.stateVersion in tests/default.nix.