# Jobs and scheduler operation

Run, inspect and maintain scheduled work.

## Scheduler and jobs

For manual VPS deployments, install the scheduler orchestrator once per server.
Register every deployed project; scheduling is required even without application-specific tasks:

```bash
sudo php /var/www/site/core/cli/nimbly.php scheduler:install
sudo php /var/www/site/core/cli/nimbly.php scheduler:project:add site /var/www/site
sudo php /var/www/site/core/cli/nimbly.php scheduler:cron:install --user=www-data
```

This creates `/etc/cron.d/nimbly-scheduler`, which runs
`/usr/local/bin/nimbly-scheduler-orchestrator` every minute as `www-data`, and
`/etc/logrotate.d/nimbly-scheduler`, which rotates the scheduler log weekly. The
orchestrator then dispatches registered project schedulers independently. In Docker
containers the scheduler runs automatically, so no host cron setup is required.

Run `schedule:init` once to create project schedule files in `ext/cli/`:

```bash
./nimbly schedule:init
```

This generates three files:

| File | Used when |
|---|---|
| `ext/cli/schedule.inc` | `APP_ENV=dev` or no env-specific file found |
| `ext/cli/schedule.prod.inc` | `APP_ENV=prod` |
| `ext/cli/schedule.stage.inc` | `APP_ENV=stage` |

The prod and stage schedules include `ext:sync`, which commits and pushes data changes in `ext/` back to the repository every hour. The dev schedule does not. Customize each file independently for environment-specific tasks.

When migrating an existing VPS, first install the orchestrator, register the
projects, run `scheduler:run --dry-run`, run the wrapper once
manually as `www-data`, and confirm `/var/log/nimbly-scheduler.log`. Back up the
old `www-data` crontab under `/etc/nimbly/backups/`, then remove only migrated
per-project `schedule:run` lines. Do not remove unrelated cron jobs.

Admin `git pull` remains available under Settings as a simple self-managed update option. For release-managed sites, prefer a repeatable deploy path: pull the intended git revision, run setup/build/lint, and make sure the scheduler is active.

---

## Mandatory maintenance and session expiry

Every `schedule:run` invocation includes the Core task definitions before it
loads the application's schedule. Application schedules add tasks; they cannot
remove or reschedule these mandatory invocations:

| Task ID | Command | Interval |
|---|---|---|
| `sessions-prune` | `sessions:prune` | 30 minutes |
| `jobs-run` | `jobs:run 10` | 1 minute |
| `jobs-prune` | `jobs:prune --days=30` | 24 hours |
| `history-prune` | `history:prune --days=90` | 24 hours |
| `agent-prune` | `agent:prune --days=30` | 24 hours |
| `stats-rollup` | `stats:rollup` | 1 hour |

Existing application entries with these IDs or commands are deduplicated.
Override an **implementation**, not its schedule: define the same command in
`ext/cli/commands.php`, using the normal command registry contract. Core trusts
the implementation's exit status. No inspection of its internal work occurs.
The host registry lists projects, not task choices; legacy `enabled` flags no
longer suppress registered projects. Remove retired deployments from the
registry. Before registering an older unscheduled project, review its queued
jobs and application schedule: enabling processing may deliver pending email
or execute other queued work.

`./nimbly schedule:status` reports successful mandatory task completion and
exits nonzero when a task failed, never succeeded, or is overdue by more than
two intervals. A successful run with an empty queue is healthy. The admin
**Needs attention** band and host audit independently read this state, so they
also detect a stopped scheduler. Check this command after every deployment;
installing cron alone is not proof of working maintenance. The installed host
audit ships a root-owned `nimbly-maintenance.php` helper beside its collector.

For VPS installations, use the existing host orchestrator and register **all**
active projects. Container images already invoke the project scheduler every
minute. Other cloud platforms must invoke the same CLI every minute using a
single scheduler with access to the application's persistent `ext/data` and
runtime configuration. Monitor `schedule:status` independently of that runner.
No extra session-cleanup daemon or per-project cleanup registration is needed.

Session functions, including file inspection and cleanup, live in
`core/lib/session.php`. Defaults are rolling inactivity limits: anonymous
4 hours, authenticated users 3 days, admins 7 days. There is **no absolute
login-age limit**: regular activity keeps an authenticated session alive.
Session ID rotation does not reset its identity or sign the user out.
Anonymous reads and permission checks do not create sessions; forms and other
stateful features may do so. Authenticated cookies persist across browser
restarts; anonymous cookies remain browser-session cookies.

Every cookie core sets has the same options (`session_cookie_options()`): the
site's base path, `HttpOnly`, `SameSite=Lax`, and `Secure` on https. That goes
for the session cookie, the form key of a visitor without a session (`key`) and
a variable kept with `[#set session ...#]` when there is no session; the last
two last 30 days (`persistent_cookie_options()`). Scripts in the page cannot
read them.

Projects can configure durations in seconds under `session` in `.config/site`:

```json
{
  "session": {
    "anonymous": 14400,
    "authenticated": 259200,
    "roles": { "admin": 604800, "editor": 259200 }
  }
}
```

The longest matching assigned-role override wins; otherwise the authenticated
default applies. Temporary role switching does not change the assigned-role
snapshot. Legacy sessions retain their original 24-hour validity until their
first valid live request upgrades them; an already-expired session is never
revived. Login, cookies, and cleanup use the same policy. Core disables PHP's
request-triggered garbage collection for its private session directory.

`./nimbly sessions:prune --dry-run` reports aggregate counts without changing
sessions. Normal execution streams files, skips locked files and symlinks, and
removes expired sessions only. Malformed, unsupported serialized objects or
references, and files larger than 1 MiB are reported as failures rather than
deleted. The reader accepts scalar/array `php` and `php_serialize` encodings.
The separate **Sign out everyone** action intentionally removes every session.
Remember-me UI and its longer timeout are deferred.

## Jobs and Site settings

`/nb-admin/jobs` (`core/modules/admin/lib/jobs-panel/`) is a diagnostic view over the job queue and scheduler - queued/running/done/failed counts, a table of jobs with their last error, and "Run due jobs now" / "Prune old jobs" actions, gated on `view-.jobs` / `manage-.jobs`. "Prune old jobs" removes both `done` jobs (aged off `completed_at`) and terminally `failed` jobs with no retries left (aged off `_modified`) older than 30 days - the same rule the scheduled `jobs:prune --days=30` task applies, so a fixed-and-forgotten failure eventually drops off the **Needs attention** band on its own. Each `done`/`failed` row also has its own **Delete** button (gated on `manage-.jobs`) for clearing an individual job immediately regardless of age - queued/running rows show it disabled, since deleting an in-flight job could orphan a lock or a running process.

`/nb-admin/settings` (`core/modules/admin/lib/site-settings/`) is one flat page gated on `edit-.config`. It edits the site-wide `.config/site` record: name, description, languages, text direction (`direction`, `ltr`/`rtl`, rendered as `<html dir>`) and admin sidebar position (`nimblybar.side`). Name, description, direction and side are per-language values, and each field has its own language switch. On save, direction and side are filled in for every site language: Arabic, Persian and Hebrew start `rtl` with the sidebar on the right, and other languages start `ltr` and left. For users with `manage-system`, it also holds the custom pages switch with its page types, and the navigation editing switch (`.config/managed_pages`). Each switch shows only when there is a `pages` resource or navigation slots to switch on. Pages appear under Resources in the Nimbly bar; the Navigation admin tab is hidden for everyone while its switch is off. This is distinct from the per-page title editor behind the nimblybar gear icon, which edits `page_settings.page_title` for the current page only.
