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()