From 7d7d7177452dd7ffd4d0d6e26b740f914a5472ee Mon Sep 17 00:00:00 2001 From: Andrew Bryant Date: Sat, 26 Sep 2026 15:17:22 -0400 Subject: [PATCH] nixos-module: work around upstream language-list caching bug 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 --- CLAUDE.md | 8 +++++++- nixos-module/invoiceninja.nix | 8 ++++++++ tests/nixos-test.nix | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index c5a4b00..3f52f58 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,7 +97,13 @@ expect Nix to report the correct hash on a failed build (or use `nix-prefetch`/` clears bootstrap/opcode cache, rebuilds `.env`, rsyncs static storage into `dataDir`, symlinks the runtime `app.php`, and guards one-time actions (`migrate`, `db:seed`, admin account creation) behind marker files (`.initial-migration`, `.db-seeded`, `.admin-created`) in `dataDir`. `phpfpm-invoiceninja.service` and - `invoiceninja-queue-worker.service` both depend on it. + `invoiceninja-queue-worker.service` both depend on it. It also unconditionally runs + `invoiceninja-manage cache:forget languages` on every activation, working around an upstream bug: 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) and does `Cache::forever('languages', Language::all())`, + permanently caching just the 3 languages it sees at that moment — without this, the language dropdown in + the app stays stuck at 3 entries forever, since `StaticServiceProvider` checks that cache key before ever + querying the DB again. - `invoiceninja-scheduler.timer`/`.service` run `artisan schedule:run` every minute — this is what drives Invoice Ninja's own internal cron-scheduled jobs (recurring invoices, scheduled reports, etc.); without it the app's dashboard reports "cron jobs are not enabled". Laravel's scheduler is just a dispatcher, so this one diff --git a/nixos-module/invoiceninja.nix b/nixos-module/invoiceninja.nix index 5a0d6c7..83432cd 100644 --- a/nixos-module/invoiceninja.nix +++ b/nixos-module/invoiceninja.nix @@ -696,6 +696,14 @@ in # Necessary for languages, currencies, countries, etc. [[ ! -f ${cfg.dataDir}/.db-seeded ]] && invoiceninja-manage db:seed --force && touch ${cfg.dataDir}/.db-seeded + # Work around an upstream bug: 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) and + # does `Cache::forever('languages', Language::all())`, permanently caching just the 3 languages + # it sees at that moment. Without this, the language list stays stuck at those 3 forever, + # since StaticServiceProvider checks this cache key before ever querying the DB again. + # Unconditional (not marker-gated) so it also self-heals installs that hit this before the fix. + invoiceninja-manage cache:forget languages + # Create Invoice Ninja admin account [[ (! -f ${cfg.dataDir}/.admin-created) && (${ if cfg.adminAccount.enable then "true" else "false" diff --git a/tests/nixos-test.nix b/tests/nixos-test.nix index 1590f3f..bcb81e5 100644 --- a/tests/nixos-test.nix +++ b/tests/nixos-test.nix @@ -21,6 +21,8 @@ pkgs.testers.nixosTest { }; testScript = '' + import json + def check_invoiceninja(machine, proxy_unit): machine.wait_for_unit("mysql.service") machine.wait_for_unit("redis-invoiceninja.service") @@ -65,6 +67,19 @@ pkgs.testers.nixosTest { ) assert "${adminEmail}" in login_response, f"login did not succeed: {login_response}" + with subtest("full language list is available, not just the af/ca/id migration's 3"): + # Regression check: 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, and + # does Cache::forever('languages', Language::all()) - permanently caching just the 3 + # languages it sees at that moment. invoiceninja-data-setup works around this with + # `invoiceninja-manage cache:forget languages`; this confirms the workaround is in place. + token = json.loads(login_response)["data"][0]["token"]["token"] + statics = machine.succeed( + f"curl -sf http://localhost/api/v1/statics -H 'X-API-TOKEN: {token}'" + ) + language_count = len(json.loads(statics)["languages"]) + assert language_count > 40, f"expected ~45 languages, got {language_count}" + nginx.start() caddy.start()