From fef9406b632fff8538bc73a54f52395399b0cc38 Mon Sep 17 00:00:00 2001 From: Andrew Bryant Date: Sat, 26 Sep 2026 13:06:00 -0400 Subject: [PATCH] nixos-module: add scheduler timer for artisan schedule:run 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 --- CLAUDE.md | 4 ++++ nixos-module/invoiceninja.nix | 26 ++++++++++++++++++++++++++ tests/nixos-test.nix | 5 +++++ 3 files changed, 35 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 758399e..f0e6ca8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -92,6 +92,10 @@ expect Nix to report the correct hash on a failed build (or use `nix-prefetch`/` `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-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 + timer is sufficient regardless of how many scheduled tasks Invoice Ninja itself defines. - `invoiceninja-manage` is a generated wrapper script (`invoiceninja-manage `) that `cd`s into the package output and runs `php artisan` as the configured service user (via `sudo` if invoked as another user). It's added to `environment.systemPackages`, so it's the normal way to run artisan commands diff --git a/nixos-module/invoiceninja.nix b/nixos-module/invoiceninja.nix index 23817ca..a2c0584 100644 --- a/nixos-module/invoiceninja.nix +++ b/nixos-module/invoiceninja.nix @@ -623,6 +623,32 @@ in }; }; + systemd.services.invoiceninja-scheduler = { + description = "Invoice Ninja scheduled tasks (artisan schedule:run)"; + after = [ "invoiceninja-data-setup.service" ]; + requires = [ "phpfpm-invoiceninja.service" ]; + # Ensure chromium is available, for any scheduled tasks that generate PDFs. + path = extraPrograms; + + serviceConfig = { + Type = "oneshot"; + ExecStart = "${invoiceninja-manage}/bin/invoiceninja-manage schedule:run"; + User = user; + Group = group; + StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/invoiceninja") "invoiceninja"; + }; + }; + + systemd.timers.invoiceninja-scheduler = { + description = "Run Invoice Ninja's scheduled tasks every minute"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "minutely"; + Persistent = true; + Unit = "invoiceninja-scheduler.service"; + }; + }; + systemd.services.invoiceninja-data-setup = { description = "Invoice Ninja setup: migrations, environment file update, cache reload, data changes"; wantedBy = [ "multi-user.target" ]; diff --git a/tests/nixos-test.nix b/tests/nixos-test.nix index c444d99..d06d1e8 100644 --- a/tests/nixos-test.nix +++ b/tests/nixos-test.nix @@ -52,6 +52,11 @@ pkgs.testers.nixosTest { with subtest("invoiceninja-manage wrapper can run artisan commands"): machine.succeed("invoiceninja-manage migrate:status") + with subtest("scheduler timer is enabled and schedule:run succeeds"): + # Without this, the app's dashboard reports "cron jobs are not enabled". + machine.succeed("systemctl is-active invoiceninja-scheduler.timer") + machine.succeed("systemctl start --wait invoiceninja-scheduler.service") + with subtest("admin account can log in via the API"): login_response = machine.succeed( "curl -sf -X POST http://localhost/api/v1/login "