# CLI

Elk commando van ./nimbly: setup, builds, tests, onderhoud, scheduler en statistieken.

Nimbly heeft een CLI in `core/cli/nimbly.php`. Je roept die bij voorkeur aan via de launcher `./nimbly` in de hoofdmap:

```bash
./nimbly init
./nimbly up
./nimbly deps
./nimbly build
./nimbly watch
./nimbly test:run
./nimbly user:create
./nimbly module:install <name>
```

`init`, `deps`, `build` en `watch` gebruiken intern npm. Commando's die PHP gebruiken, draaien op de PHP van de host als die er is, en vallen anders vanzelf terug op Docker. Er zijn ook expliciete varianten:

```bash
php core/cli/nimbly.php system:setup   # require host PHP
./nimbly --docker system:setup         # force Docker Compose
```

Gelijkwaardige directe aanroepen:

```bash
php core/cli/nimbly.php system:setup
php core/cli/nimbly.php user:create
php core/cli/nimbly.php module:install <name>
php core/cli/nimbly.php routes:sync
php core/cli/nimbly.php index:rebuild [resource]
php core/cli/nimbly.php data:cache:clear
php core/cli/nimbly.php user:email-index:rebuild
php core/cli/nimbly.php system:repair
php core/cli/nimbly.php help
```

## Documentatie opzoeken

De documentatiecommando's lezen alleen, en alleen de lokale `NIMBLY.md`. Gebruik ze om tijdens het werk de kleinste bruikbare sectie van de naslag te laden:

```bash
./nimbly docs:list
./nimbly docs:section "Template Syntax"
./nimbly docs:section "Template Syntax > Route templates"
./nimbly docs:search "router_accept"
```

Hoofdletters maken niet uit. Een sectiepad kan een geneste kop aanwijzen; een kopnaam zonder pad moet uniek zijn. Ontbrekende en dubbelzinnige secties geven een fout.

## Commando's

`./nimbly help` toont de commando's. `./nimbly <command> --help` (of `-h`) toont wat één commando doet en welke invoer het neemt, zonder het te draaien.

### `system:setup`
Richt het lokale systeem in. Veilig om opnieuw te draaien: bestaande bestanden en records worden nooit overschreven.

Wat het doet:
- Maakt `.env` met `APP_ENV`, een gegenereerde `PEPPER`, en `BASE_PATH` alleen bij installaties in een submap
- Maakt `.htaccess` uit een sjabloon
- Maakt `.user.ini` uit een sjabloon voor de runtime-instellingen van PHP-FPM
- Maakt de mappenstructuur van `ext/` (`data/`, `static/`, `lib/`, `modules/`, `tpl/`, `uri/`, tijdelijke mappen)
- Geeft een site zonder pagina's een eerste homepage (`ext/uri/index.tpl` en `main.tpl`): een korte, bewerkbare pagina die laat zien waar pagina's, content en data staan, met één voorbeeldresource (`ext/data/messages/.meta`) en één formulier dat erin opslaat (`ext/uri/message.json`), dicht voor bezoekers. Die mag je aanpassen of weghalen; een site die al een route heeft, blijft ongemoeid
- Maakt `.config/site`, de resource `.content`, de `.routes`-records van core en de standaardrollen (`admin`, `editor`)
- Maakt de resource `users` en een eerste admin-gebruiker

Vragen: **Site name**, **Admin email**, **Admin password**. Stappen die al klaar zijn, worden stil overgeslagen.

**Zonder vragen (CI/CD):** Zet omgevingsvariabelen om alle vragen over te slaan:

| Variabele | Beschrijving |
|---|---|
| `APP_ENV` | Naam van de omgeving (`prod`, `stage`, `dev`; standaard `dev`) |
| `BASE_PATH` | Optioneel basispad in de URL bij installaties in een submap, bijvoorbeeld `/mysite/`; weglaten bij installaties in de root |
| `PEPPER` | Pepper voor versleuteling; zet hem om een bestaande waarde te hergebruiken, laat hem weg om er automatisch een te maken |
| `EXT_REPO` | Git-remote-URL van de ext-repository (geschreven in `ext/readme.md`) |
| `SITE_NAME` | Naam van de site, geschreven in `.config/site` |
| `SITE_LANGUAGE` | Gecontroleerde hoofdtaal van twee kleine letters voor een nieuwe site (standaard `en`) |
| `ADMIN_EMAIL` | E-mailadres van de eerste admin-gebruiker |
| `ADMIN_PASSWORD` | Wachtwoord van de eerste admin-gebruiker (minimaal 8 tekens) |

```bash
SITE_NAME="My Site" ADMIN_EMAIL=admin@example.com ADMIN_PASSWORD=secret123 ./nimbly system:setup
```

Zonder terminal (bij het starten van een container, vanuit een scheduler) wacht de setup nooit op een antwoord. Een ontbrekende `SITE_NAME` wordt `My Nimbly Site`. Een site zonder gebruikers heeft `ADMIN_EMAIL` en `ADMIN_PASSWORD` nodig: ontbreekt er één, dan stopt de setup met één regel die hem noemt en exitcode 1, zodat een productiecontainer op een leeg volume pas start als beide zijn gezet.

### `user:create`
Maakt een extra gebruikersaccount. Vraagt interactief om e-mailadres, rol en wachtwoord. De beschikbare rollen komen uit `ext/data/roles/`. De gebruiker krijgt altijd ook de rol `user`. Vereist dat de setup al heeft gedraaid.

### `module:install`
Draait het script `.install.inc` van een module. Zoekt eerst in `ext/modules/`, daarna in `core/modules/`. Vereist dat de setup al heeft gedraaid.

```bash
./nimbly module:install event
```

### `routes:sync`
Doorzoekt `ext/uri/**/route.inc` en maakt ontbrekende records in `.routes` voor dynamische routes. Gebruik dit nadat je een nieuwe routemap met `(param)`-segmenten hebt toegevoegd.

```bash
./nimbly routes:sync
```

### `index:rebuild`
Bouwt de indexregels van een geïndexeerde resource opnieuw op. Gebruik dit nadat je `index` aan de `.meta` van een bestaande resource hebt toegevoegd, of nadat je records buiten de gewone API om hebt geïmporteerd.

```bash
php core/cli/nimbly.php index:rebuild             # interactive: lists indexed resources, prompts for choice
php core/cli/nimbly.php index:rebuild articles    # direct: reindex the 'articles' resource
```

Het commando loopt alle records van de resource langs en maakt ontbrekende indexbestanden. Je kunt het veilig vaker draaien; bestaande regels blijven onaangeroerd.

### `data:cache:clear`
Gooit de gecachete queryresultaten van elke resource weg; de volgende keer lezen bouwt ze opnieuw op. Gebruik dit nadat je een recordbestand met de hand ter plekke hebt aangepast (zie Datacaching). Opslaan via de site, de API, Git of `ext:sync` vraagt geen legen.

```bash
php core/cli/nimbly.php data:cache:clear
```

### `user:email-index:rebuild`
Voegt metadata voor het opzoeken op e-mailadres toe aan de resource `users` en bouwt de e-mailindex opnieuw op.

```bash
php core/cli/nimbly.php user:email-index:rebuild
php core/cli/nimbly.php user:email-index:rebuild --yes
```

Deze migratie hoort bij het model waarin de UUID van een gebruikersrecord vast is en het e-mailadres een geïndexeerd opzoekveld. Ze voegt `email` toe aan `users/.meta.index`, voegt `email` toe aan `users/.meta.unique` als er geen dubbele e-mailadressen zijn, en maakt indexregels voor alle bestaande gebruikers, ook oudere gebruikers waarvan de UUID oorspronkelijk `md5(email)` was.

Worden er dubbele e-mailadressen gevonden, dan meldt het commando die en voegt het `unique=email` pas toe als ze zijn opgelost. De e-mailindex wordt wel opnieuw opgebouwd, zodat inloggen waar mogelijk snel blijft.

### `test:run`
Draait de E2E-tests. Het commando maakt tijdelijke testdata met `test:setup`, installeert zo nodig de Chromium-browser voor Playwright, draait Playwright met `core/tests/playwright.config.js`, en verwijdert daarna de tijdelijke data met `test:teardown`.

```bash
./nimbly test:run
./nimbly --docker test:run
```

Gebruik `--docker` in CI of wanneer PHP-databestanden via hetzelfde Docker-gebruikersmodel moeten worden geschreven als de webruntime. Core-tests roepen geen betaalde externe diensten aan. Vervang de provider door een mock en test het request, de verwerking van het antwoord, de validatie en het gedrag van de interface van Nimbly voorspelbaar. Of een provider bereikbaar is, is een operationele integratiecheck, geen onderdeel van de core-tests.

### `test:setup` / `test:teardown`
Onderliggende commando's die `./nimbly test:run` gebruikt. `test:setup` maakt de rol `test`, de gebruiker `test@nimbly.dev`, de resource `test-records` en startrecords. `test:teardown` verwijdert die records en de tijdelijke routeregels `.test`.

### `system:repair`

Brengt gegenereerde bestanden en systeemrecords gelijk met de huidige core. Een core-update herschrijft ze nooit, dus een oudere site kan iets missen wat een nieuwe installatie van `system:setup` krijgt. Elke controle meldt eerst; zonder `--yes` verandert er niets.

| Controle | Gerepareerd als |
|-------|---------------|
| `.htaccess` in de root | Die afwijkt van `core/cli/setup/htaccess.tpl`. De pepper en de rewrite-basis komen uit het bestaande bestand; het vorige bestand blijft bewaard als `.htaccess.before-repair`. |
| Mapbeveiligingen | Een `.htaccess` onder `ext/` of `core/` ontbreekt of nog de oude vorm `<files *.*>` heeft. Een beveiliging met eigen inhoud wordt gemeld en blijft staan. |
| `ext/.gitignore` | Die de thumbnailcache, de jobwachtrij, de status van de scheduler of de chats en runs van agents niet negeert. |
| `.config/.meta` | Die geen `upsert` heeft, zodat pagina-instellingen niet kunnen worden opgeslagen voor een pagina die er nog geen heeft. |
| Core-routes | Een core-route die na de setup is toegevoegd, niet is geregistreerd. |
| E-mailindex van gebruikers | `users/.meta` het veld `email` niet indexeert. `unique` wordt alleen toegevoegd als geen twee gebruikers hetzelfde adres hebben. |

```bash
./nimbly system:repair         # report only
./nimbly system:repair --yes   # apply
```

De genegeerde mappen veranderen op elke server vanzelf (de status van de scheduler elke minuut), dus als je er één volgt, krijgt `ext:sync` conflicten en kan `ext/` op de server in een rebase blijven hangen. De negeerregel haalt een map die al wordt gevolgd, niet uit Git:

```bash
git -C ext rm -r --cached data/.state
```

### `schedule:run`
Draait de geplande commando's die aan de beurt zijn voor één project. Maak op handmatige VPS-servers met meerdere Nimbly-projecten niet één cronregel per project. Gebruik in plaats daarvan de scheduler-orchestrator op serverniveau en laat die de `schedule:run` van elk geregistreerd project na elkaar aanroepen.

Projecten kunnen schema's definiëren in `ext/cli/`. De scheduler kiest bestanden in deze volgorde:

1. `SCHEDULE_FILE` uit `.env` of de omgeving van het proces, als expliciet bestandspad.
2. `ext/cli/schedule.<env>.inc`, waarbij `<env>` uit `SCHEDULE_ENV`, `APP_ENV` of `NIMBLY_ENV` komt.
3. `ext/cli/schedule.inc`.
4. `core/cli/schedule.inc`.

Namen van omgevingen worden gelijkgetrokken: `production` → `prod`, `staging` → `stage`, `development` en `local` → `dev`.

Gebruik schemabestanden per omgeving als staging of ontwikkeling achtergrondjobs moet draaien, maar geen taken die alleen voor productie zijn, zoals herinneringen aan leden.

Taken in het schema kunnen `every_minutes` gebruiken voor intervallen langer dan één minuut:

```php
[
    'id' => 'uptime-poll',
    'command' => 'uptime:poll',
    'every_minutes' => 2,
]
```

De waarde moet een positief geheel getal zijn. Een nieuwe taak is meteen aan de beurt; na het draaien is hij weer aan de beurt als de klok het volgende interval ingaat. Bestaande schema's met `every`, `daily_at` en de optionele `timezone` blijven ondersteund.

De status van de laatste runs van de scheduler staat in `ext/data/.state/schedule`. Bestaande installaties met de oudere status in `ext/data/.config/schedule` worden als terugval gelezen tot de scheduler het nieuwe statusbestand schrijft.

### Verplicht onderhoud en het verlopen van sessies

Elke aanroep van `schedule:run` neemt de taken van core op voordat het schema van de applicatie wordt geladen. Schema's van de applicatie voegen taken toe; ze kunnen deze verplichte aanroepen niet weghalen of verplaatsen:

| Taak-id | Commando | Interval |
|---|---|---|
| `sessions-prune` | `sessions:prune` | 30 minuten |
| `jobs-run` | `jobs:run 10` | 1 minuut |
| `jobs-prune` | `jobs:prune --days=30` | 24 uur |
| `history-prune` | `history:prune --days=90` | 24 uur |
| `agent-prune` | `agent:prune --days=30` | 24 uur |
| `stats-rollup` | `stats:rollup` | 1 uur |

Bestaande regels in de applicatie met deze id's of commando's worden ontdubbeld. Vervang een **implementatie**, niet het schema ervan: definieer hetzelfde commando in `ext/cli/commands.php`, volgens de gewone afspraak voor het register van commando's. Core vertrouwt op de exitstatus van de implementatie en kijkt niet naar het werk daarbinnen. Het register op de host noemt projecten, geen keuzes van taken; oudere `enabled`-vlaggen houden geregistreerde projecten niet meer tegen. Haal deployments die niet meer draaien uit het register. Bekijk voordat je een ouder project zonder schema registreert de jobs in de wachtrij en het schema van de applicatie: als verwerking aangaat, kan wachtende e-mail worden verstuurd of ander werk uit de wachtrij worden uitgevoerd.

`./nimbly schedule:status` meldt of de verplichte taken zijn gelukt en stopt met een fout als een taak mislukte, nooit is gelukt, of meer dan twee intervallen te laat is. Een geslaagde run met een lege wachtrij is gezond. De band **Needs attention** in de admin en de host-audit lezen deze status elk zelf, dus ook zij merken een gestopte scheduler op. Controleer dit commando na elke deployment; alleen cron installeren bewijst niet dat het onderhoud werkt. De geïnstalleerde host-audit levert naast zijn collector een hulpbestand `nimbly-maintenance.php` dat van root is.

Gebruik voor VPS-installaties de bestaande orchestrator op de host en registreer **alle** actieve projecten. Containerimages roepen de scheduler van het project al elke minuut aan. Andere cloudplatforms moeten dezelfde CLI elke minuut aanroepen, met één scheduler die toegang heeft tot de blijvende `ext/data` en de runtime-instellingen van de applicatie. Bewaak `schedule:status` los van die runner. Er is geen extra daemon voor het opruimen van sessies en geen registratie per project nodig.

De functies voor sessies, inclusief het bekijken en opruimen van bestanden, staan in `core/lib/session.php`. De standaard is een doorlopende limiet op inactiviteit: anoniem 4 uur, ingelogde gebruikers 3 dagen, admins 7 dagen. Er is **geen absolute limiet op de leeftijd van een login**: regelmatige activiteit houdt een ingelogde sessie in leven. Het wisselen van de sessie-id verandert de identiteit niet en logt de gebruiker niet uit. Anoniem lezen en rechten controleren maakt geen sessies aan; formulieren en andere functies met status kunnen dat wel. Cookies van ingelogde gebruikers blijven bewaard na het herstarten van de browser; anonieme cookies blijven sessiecookies van de browser.

Elke cookie die core zet, heeft dezelfde opties (`session_cookie_options()`): het basispad van de site, `HttpOnly`, `SameSite=Lax`, en `Secure` op https. Dat geldt voor de sessiecookie, de formuliersleutel van een bezoeker zonder sessie (`key`) en een variabele die met `[#set session ...#]` wordt bewaard als er geen sessie is; die laatste twee blijven 30 dagen (`persistent_cookie_options()`). Scripts op de pagina kunnen ze niet lezen.

Projecten kunnen de duur in seconden instellen onder `session` in `.config/site`:

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

De langste passende uitzondering voor een toegewezen rol wint; anders geldt de standaard voor ingelogde gebruikers. Tijdelijk van rol wisselen verandert de vastgelegde toegewezen rollen niet. Inloggen, cookies en opruimen volgen dezelfde regels. Core zet de opruiming door PHP bij requests uit voor zijn eigen sessiemap.

`./nimbly sessions:prune --dry-run` meldt totalen zonder sessies te veranderen. Gewoon draaien leest de bestanden één voor één, slaat vergrendelde bestanden en symlinks over, en verwijdert alleen verlopen sessies. Kapotte bestanden, niet-ondersteunde geserialiseerde objecten of verwijzingen, en bestanden groter dan 1 MiB worden als fout gemeld in plaats van verwijderd. De lezer accepteert de coderingen `php` en `php_serialize` met waarden en arrays. De aparte actie **Sign out everyone** verwijdert bewust elke sessie. Een "onthoud mij"-optie met een langere duur is uitgesteld.

### Requeststatistieken (`stats:*`)

Core legt elk request vast dat `index.php` bereikt, zonder cookies of JavaScript. Vastleggen staat aan voor `APP_ENV=prod` en `stage`; zet `STATS_ENABLED=1` of `0` om dat te veranderen. Bestanden die `.htaccess` direct levert (`ext/static`, `core/static`, thumbnails onder `/img/`) en geblokkeerde `.php`-probes bereiken PHP nooit en worden niet vastgelegd.

Een shutdownfunctie voegt per request één JSON-regel toe aan `ext/data/.tmp/stats/running-YYYY-MM-DD.log`: UTC-tijd, methode, host, pad met query (ten opzichte van de basis-URL), status, contenttype, duur, IP-adres, user agent, verwijzer, de eerste tag van `Accept-Language` en een vlag voor ingelogd. Boven `STATS_MAX_DAY_BYTES` (standaard 200 MB) telt een dag verdere requests alleen nog als overloop.

`stats:rollup` (verplicht onderhoud, elk uur) archiveert afgesloten dagen onder `ext/data/.stats/<env>/`, die `ext:sync` commit:

| Pad | Inhoud |
|---|---|
| `raw/YYYY/YYYY-MM-DD.log.gz.enc` | de volledige dag, gecomprimeerd met gzip en versleuteld met AES-256-GCM |
| `raw/YYYY/YYYY-MM-DD.apache.log.gz.enc` | optionele aanvulling vanuit Apache, uit `apache-YYYY-MM-DD.log` |
| `months/YYYY-MM.json` | volledige tellingen per dag op status, route, soort bezoeker, apparaat, browser, taal, bot, verwijzer en pad, plus `hours` (requests, paginaweergaven, bezoekers, bots, scanners per UTC-uur) |
| `years/YYYY.json` | dezelfde tellingen opgeteld per maand, plus een jaartotaal |

Alle datums, bestandsnamen en dagovergangen zijn in UTC. Interfaces rekenen om naar de tijdzone van de kijker door `hours` opnieuw te groeperen; een bezoeker telt mee in het uur van zijn eerste paginaweergave van de UTC-dag. Zoom in van jaar naar maand naar dag naar de ruwe requests; weken en totalen over alle tijd worden uit deze bestanden afgeleid. Boven dagniveau wordt `visitors` `visits`, de som van de unieke bezoekers per dag. De JSON-bestanden bevatten alleen tellingen, nooit ranglijsten of IP-adressen; interfaces leiden toplijsten daaruit af. Het indelen (mens, redacteur, bot, scanner, verdacht, monitor, tool) gebeurt bij de rollup, dus `stats:rollup --rebuild` rekent elke maand opnieuw uit `raw/` als de regels beter worden.

`stats:import-apache` leest regels uit het toegangslog van Apache (`combined` of `vhost_combined`) van stdin, zodat alleen het lezen van het log root nodig heeft:

```bash
zcat -f /var/log/apache2/site-access.log.* | sudo -u www-data ./nimbly stats:import-apache --mode=full --before=2026-09-25T02:02:00Z
zcat -f /var/log/apache2/site-access.log.1 | sudo -u www-data ./nimbly stats:import-apache
```

`--mode=full` vult elke regel aan vóór `--before` (dagen voordat het vastleggen begon). De standaard `--mode=enrich` voegt alleen toe wat PHP nooit bereikt: bestanden uit `ext/static` of `core/static`, thumbnails onder `/img/` en geblokkeerde `.php`-probes. Regels worden geschreven naar `apache-YYYY-MM-DD.log` (UTC-datums) en bij de volgende rollup gearchiveerd; een watermerk in `.tmp/stats/apache.watermark` slaat regels over die al zijn geïmporteerd. Gebruik `--base=/subdir/` voor installaties in een submap en `--host=` als `SITE_URL` niet is gezet. Regels van Apache hebben geen `Accept-Language`, contenttype of inlogstatus: contenttypen worden geraden uit het pad en redacteuren tellen als bezoekers.

`STATS_KEY` (64 hexadecimale tekens, gemaakt door `system:setup`) versleutelt het ruwe archief. Maak er een back-up van: zonder de sleutel is de ruwe geschiedenis niet te lezen. Zolang hij ontbreekt, mislukt de rollup en blijven de lopende logs bewaard. `stats:raw YYYY-MM-DD [--apache]` toont een ontsleutelde dag.

### `scheduler:*`
Draait de schedulers van meerdere projecten vanuit één cron op serverniveau. De schemabestanden van een project blijven in het project zelf, in `ext/cli/`; het register op de server is de enige bron voor welke projecten worden aangestuurd.

De orchestrator gebruikt standaard deze bestanden, beheerd op de server:

| Pad | Doel |
|---|---|
| `/usr/local/bin/nimbly-scheduler-orchestrator` | Wrapper die cron draait |
| `/etc/nimbly/scheduler-projects.json` | Geregistreerde projecten en standaarden |
| `/etc/cron.d/nimbly-scheduler` | Cronregel van het systeem |
| `/var/log/nimbly-scheduler.log` | Log van de scheduler |
| `/etc/logrotate.d/nimbly-scheduler` | Wekelijkse rotatie van het log, acht weken bewaard |

Installeer het één keer per server vanuit een actuele checkout van Nimbly core:

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

Handige commando's voor beheer:

```bash
php core/cli/nimbly.php scheduler:project:list
sudo -u www-data php core/cli/nimbly.php scheduler:run --dry-run
sudo -u www-data /usr/local/bin/nimbly-scheduler-orchestrator
php core/cli/nimbly.php scheduler:cron:status
```

`scheduler:run` neemt kort een globale lock en start twee workers per geregistreerd project: één draait `jobs:run 10` om de wachtrij vlot te verwerken, de andere draait de `schedule:run` van het project. Workers schrijven hun afronding en exitcode in het schedulerlog van de host. Een lange taak van een applicatie houdt dus de minuuttaken van een ander project, of de eigen meldingen in de wachtrij, niet op. Jobrunners gebruiken een lock per project, zodat de directe wachtrijworker en de verplichte taak `jobs-run` niet dezelfde job tegelijk oppakken. De projectscheduler houdt zijn eigen lock, zodat latere cronticks een lange run niet dubbel starten. Oudere waarden `default_delay_after_seconds` in het register worden genegeerd.

`scheduler:run` start de workers van elk geregistreerd project altijd meteen (het wacht nooit voordat het start), zodat een traag project het starten bij de volgende crontick niet ophoudt. Het aantal gelijktijdige workers wordt in plaats daarvan binnen elke `scheduler:worker` begrensd: voordat een worker de echte `jobs:run`/`schedule:run` van het project draait, wacht hij op een vrije plek in een gedeelde semafoor, en geeft die vrij als hij klaar is. Zo duwt een grote vloot niet alle workers tegelijk op een server met één CPU. De grens is standaard 5 gelijktijdige workers voor de hele host en in te stellen met `NIMBLY_SCHEDULER_MAX_CONCURRENCY`; de lockbestanden voor de plekken staan in `NIMBLY_SCHEDULER_SEMAPHORE_DIR` (standaard: de tijdelijke map van het systeem).
