comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); // Advance every due provisioning run once a minute (runs in the scheduler service). Schedule::call(fn () => app(TickProvisioning::class)()) ->everyMinute() ->name('provisioning-tick') ->withoutOverlapping(); // Refresh the VPN peer state (handshakes, traffic) so the console does not go // stale while nobody has it open. The job itself runs on the provisioning // queue — that worker owns wg0. Schedule::job(new SyncVpnPeers) ->everyMinute() ->name('vpn-sync'); // Sample network counters and enforce the monthly allowance. Runs on the // provisioning queue, which is where the Proxmox credentials are usable. // Turns the monitoring column from a claim into a measurement. Without it the // console and the public status page report every instance healthy forever. Schedule::job(new SyncMonitoringStatus) ->everyFiveMinutes() ->withoutOverlapping(); Schedule::job(new CollectInstanceTraffic) ->everyFifteenMinutes() ->name('traffic-collect'); // Discard Stripe events that were held for a contract which never appeared. // Most of them never will: Stripe also delivers events for subscriptions // belonging to other environments pointed at this endpoint, and for objects // made by hand in the dashboard. A week is far longer than the moment a // checkout takes to become a contract, and short enough that the table stays // a holding area rather than a second copy of Stripe's event log. Schedule::call(fn () => StripePendingEvent::query() ->where('created_at', '<', now()->subWeek()) ->delete()) ->daily() ->name('stripe-pending-prune'); // Copies of invoices that never reached the archive — a NAS that was rebooting // when the invoice was issued, a mount that had gone away. Copy-on-issue is // right and is not enough: nobody opens an archive to check whether last // Tuesday is in it, so something has to go and look. // // Hourly rather than by the minute: the failure it repairs lasts as long as an // outage lasts, and a retry storm against a mount that is down helps nobody. // The support mailbox. Polling, because IMAP is a mailbox you look in — a mail // server has no way to tell us something arrived — so the interval IS the // answer to "how long until I see it". Two minutes is short enough that an // operator watching the inbox does not reload it by hand, and long enough that // a mail server is not being asked once a second for nothing. // // withoutOverlapping: a slow or unreachable mailbox must not stack runs on top // of each other until a worker is doing nothing but timing out. Schedule::command('clupilot:fetch-mail') ->everyTwoMinutes() ->withoutOverlapping(); Schedule::command('clupilot:archive-invoices') ->hourly() ->withoutOverlapping(); // Plan changes that landed here and never reached Stripe. The change is applied // to the contract and to the machine before Stripe is told, and it is not rolled // back when Stripe is away — so what is left is a customer being billed for a // package they are no longer on, and nobody would ever go and look. // // Hourly, like the archive sweep and for the same reason: the failure it repairs // lasts as long as an outage lasts, and hammering an API that is down helps // nobody. Schedule::command('clupilot:sync-stripe-subscriptions') ->hourly() ->withoutOverlapping(); // Empty a handover directory of folders past their keep-by. Only where a // destination asks for it, and only folders whose name is a date this // application wrote — see the command for why it leaves everything else alone. Schedule::command('clupilot:prune-exports') ->dailyAt('03:20') ->withoutOverlapping(); // One sample of every public status component, into today's row. // // The ninety-day bar on the status page is the one figure a reader checks // against their own memory, so it has to come from something recorded at the // time — there is no way to reconstruct it later. Beside the monitoring sync it // reads from, at the same cadence. Schedule::command('clupilot:sample-status') ->everyFiveMinutes() ->withoutOverlapping(); // Re-read the DNS proof for every custom domain. // // Not a one-off: a token checked once can be taken straight back out, and a // domain that later lapses and is registered by somebody else keeps resolving // to the same instance. Nightly, and at an hour where withdrawing a domain // inconveniences nobody — a withdrawal takes a working Nextcloud off its own // address, which is not a thing to do at eleven in the morning. Schedule::command('clupilot:verify-domains') ->dailyAt('03:40') ->withoutOverlapping(); // Take an available release inside the configured window, if the owner has // switched that on. Every five minutes rather than once at the start of the // window: a scheduler that was down for those sixty seconds would otherwise // skip the whole night. Schedule::command('clupilot:auto-update') ->everyFiveMinutes() ->withoutOverlapping(); // Ask every host whether it is still there. // // hosts.last_seen_at drove the console's health dot and was written exactly // once, at onboarding — so every host read "offline" half an hour later, // permanently. Nothing was measuring host reachability at all, which is also // why it could not appear on the status page. Schedule::job(new PingHosts) ->everyMinute() ->name('host-ping'); // Carry out the downgrades whose term has run out. // // An upgrade lands the moment it is paid for; a downgrade waits, because // somebody on a yearly contract bought a year. Nothing was waiting with it — the // order sat in the cart forever and the customer stayed on the bigger package. // // Every quarter of an hour rather than nightly: a term ends at the second it // ends, and a customer who has asked to pay less should not spend another // afternoon on the old price. Nothing is taken away that they did not ask to // give up, so there is no bad hour for it. Schedule::command('clupilot:apply-due-plan-changes') ->everyFifteenMinutes() ->withoutOverlapping(); // End the modules whose cancellation date has arrived. // // The same rule the downgrade above follows: a module cancelled on the tenth is // kept until the term it was paid for runs out. Stripe stopped billing it at the // moment of cancellation — its item is only ever about the NEXT cycle — so what // waits for this sweep is the module itself: the entitlement, the space on the // disk, and the entry in the register saying it ended. // // Beside the downgrades and at the same cadence, because it is the same // appointment being kept. Schedule::command('clupilot:end-cancelled-addons') ->everyFifteenMinutes() ->withoutOverlapping(); // Keep the appointment a cancellation made. // // ConfirmCancelPackage writes a date and nothing ever went back to it, which is // why TraefikWriter::remove() had no caller in the whole application: a // cancelled instance kept its router, its certificate and its route to a guest // address the host is free to reassign. Only instances whose paid term has // actually run out are touched — a cancellation that is merely SCHEDULED is a // paying customer, and this command is written around that distinction. // // Hourly: a term ends at a moment, and neither giving away a day of service nor // hammering a DNS provider by the minute is a decision worth making by accident. Schedule::command('clupilot:end-due-services') ->hourly() ->withoutOverlapping();