# OpenRekening — desktopclient

Native client bovenop de bestaande REST-API (`backend/api.php`), volgens
**Architectuur A** uit [`docs/BOUWPLAN-DELPHI.md`](../docs/BOUWPLAN-DELPHI.md):
de database blijft achter PHP, de client is gewoon een tweede API-consument.
Eén gedeelde broncode die zowel onder **Delphi (VCL)** als **Lazarus (LCL)**
compileert.

## Stand

**Fase 0 (fundament)** — de keten werkt: server-URL → ping → login → token →
beveiligde aanroep.

**Gebrand inlogvenster (`uLogin`, `uMerk`)** — na de splash verschijnt een
modaal inlogscherm met dezelfde huisstijl-kop (accentbalk + logo). Drie
pagina's die elkaar afwisselen:

- **Inloggen** — gebruikersnaam + wachtwoord, plus een knop **Instellingen**.
- **Instellingen** — de API-URL en een *secret* (voor toekomstig gebruik),
  opgeslagen in het `.ini` via `uConfig`.
- **2FA** — zes losse invulvelden voor de TOTP-code met **automatische
  doorloop** (na een cijfer springt de cursor naar het volgende veld; na het
  zesde wordt automatisch *OK* gekozen) en **backspace-gedrag** (cijfer weg en
  terug naar het vorige veld, tot het begin). Alleen cijfers worden aangenomen.

De merk-kop (logo + naam) zit in `uMerk` en wordt gedeeld met de splash. Lukt
inloggen bij het opstarten niet, dan sluit de app; na een geslaagde login
toont het hoofdvenster de beheertabs.

**Fase 1 (auth compleet)** — MFA-stap, vertrouwde werkplek, wachtwoord
wijzigen en nette afhandeling van een verlopen sessie:

- Vraagt de server om een 2FA-code, dan toont het inlogvenster de zes
  code-velden (zie hierboven).
- Na een geslaagde code geeft de server een **apparaat-token**. Dat bewaart de
  client per gebruikersnaam in `OpenRekeningClient.ini` en stuurt hij mee bij
  de volgende login — dan slaat de server de MFA-stap over. Hetzelfde gedrag
  als de webapp, die dit in `localStorage` zet.
- Het apparaat-token is **geen wachtwoord**: de server eist er nog steeds
  gebruikersnaam + wachtwoord bij en bindt hem aan het IP en aan een
  geldigheidsduur uit de Masterinstellingen.
- Verloopt het token, dan gaat de client terug naar de inlogstaat in plaats van
  een kale foutmelding te tonen.

**Fase 2 (SDK)** — alle 177 acties gegenereerd uit de OpenAPI-spec, zie onder.

**Fase 3 (beheer)**, **Fase 4 (debiteurenproces)** en **Fase 5 (import en
rapportage)** — na inloggen verschijnt een tabbladpaneel met veertien tabs:

| Tab | Wat | Unit |
|---|---|---|
| Organisatie | klantbedrijven: lijst + detailformulier + opslaan (`klanten_list` / `klant_save`) | `uBeheer` |
| Gebruikers | lijst, alleen lezen (`gebruikers_list`) | `uBeheer` |
| Instellingen | sleutel/waarde bewerken (`instellingen_get` / `_save`) | `uBeheer` |
| Debiteuren | zoeken + lijst; selectie filtert de facturen | `uDossier` |
| Facturen | bedragen en datums, filterbaar op debiteur | `uDossier` |
| Werkvoorraad | wat er volgens het proces aan de beurt is | `uDossier` |
| Dossiers | alle dossiers + tijdlijn van uitgevoerde acties; **herinnering-PDF openen** in de standaardviewer (`dossiers_list` / `dossier_acties` / `actie_pdf`) | `uInzage` |
| Klantreacties | correspondentie met debiteuren, filter "alleen ongekoppeld", berichtinhoud lezen (`correspondentie_list`) | `uInzage` |
| Import | importbatches met hun tellingen | `uRapport` |
| Bank | ingelezen bankbestanden en transacties | `uRapport` |
| Openstaand | ouderdomsanalyse (0-30/31-60/61-90/90+) + **Excel-export** | `uRapport` |
| Klantenbestand | **CSV-import/export** van debiteuren (tier + verzuim) + kolomuitleg + lege template | `uRapport` |
| Rapportage | **grafisch**: KPI-tegels, staafgrafiek per maand, fase-donut — klikbaar met drill-down (`rapportage` / `rapportage_details`) | `uRapportage` |
| Dashboard | **grafisch**: KPI-tegels + logins- en maandgrafiek, met PNG-export 1×/2× (`dashboard` / `dashboard_png`-compositie client-side) | `uDashboard` |

Bedragen komen als hele centen binnen en datums als ISO; `CentenNaarEuro` en
`DatumNL` (in `uApi`) maken daar `1.250,00` en `18-07-2026` van — dezelfde
conventie als de webapp sinds v4.0.

**Excel-export.** De knop op het tabblad Openstaand haalt het dashboard op als
écht `.xlsx` (`dashboard_excel`) en zet het naast de exe. De bestandsnaam komt
uit `Content-Disposition` en wordt als onvertrouwde invoer behandeld: alles wat
een pad kan vormen (`\ / : * ? " < > |`, leidende punten) gaat eruit vóór hij
aan de map wordt geplakt. Komt er JSON terug in plaats van een bestand, dan is
dat een foutantwoord en wordt het als zodanig gemeld — niet als "bestand"
weggeschreven.

**Klantenbestand (CSV import/export).** Het tabblad Klantenbestand spiegelt de
webapp-functie: *Exporteren* en *Lege template* schrijven een CSV naast de exe
(via `klantenbestand_export` / `klantenbestand_template`, met UTF-8-BOM zodat
Excel het `;`-scheidingsteken juist leest), en de lijst toont per kolom de
conventie/betekenis/voorbeeld uit `klantenbestand_kolommen`. *Importeren* kiest
een `.csv`, leest hem binair in en stuurt hem **base64** mee als
`{"csv_base64":…}` naar `klantenbestand_import`; de encoder-regeleindes worden
gestript zodat de PHP-kant `base64_decode(strict)` accepteert. Het rapport
(nieuw / bijgewerkt / overgeslagen / totaal) verschijnt in de statusregel én in
een dialoog. De export is 1-op-1 te herimporteren (round-trip geverifieerd op
staging).

Rijen met een ontbrekend verplicht veld worden overgeslagen (niet fataal). Die
komen ná de import in de lijst **Overgeslagen rijen** onder aan het tabblad, met
het regelnummer, de **reden** en de kern-cellen (naam/e-mail/debiteurnummer) —
zodat je weet welke records je in de CSV moet aanvullen om opnieuw te
importeren. De geneste `rapport.overgeslagen_rijen[*].cellen.*` worden gelezen
met `JsonSubRijen` (geneste array + puntnotatie), geverifieerd tegen een echte
serverrespons onder dcc32 én fpc.

**Interne notities (INTERN GEBRUIK).** Op het tabblad Debiteuren toont de client
onder de lijst de interne notities van de gekozen debiteur — nadrukkelijk
gemarkeerd als *INTERN GEBRUIK* (vet/rood label) en met de tekst dat ze **nooit
naar de debiteur** worden verzonden. Toevoegen (meerregelig memo) en verwijderen
lopen via `notitie_toevoegen` / `notitie_verwijderen`; de lijst komt uit
`notitie_lijst` (top-level array, gelezen met `JsonRijen`, regeleindes
platgemaakt voor de listview). Dit is de enige schrijfactie in `uDossier`.
Geverifieerd tegen een echte serverrespons onder dcc32 én fpc; de endpoints zelf
zijn end-to-end getest op staging.

> **Fase 4 en 5 zijn grotendeels lezen.** De schrijfacties zijn de Excel/CSV-
> downloads (raken de administratie niet) en de **CSV-import** op het tabblad
> Klantenbestand, die debiteuren aanmaakt/bijwerkt via `klantenbestand_import` —
> net als in de webapp een bewuste, expliciete actie met een rapport achteraf.
> **Statusovergangen** (herinnering versturen, dossier pauzeren, incasso) lopen
> via `DossierEngine` en hebben in de webapp bevestigingsstappen en
> waarschuwingen. Die overslaan in een desktop-MVP zou betekenen dat één misklik
> een échte aanmaning verstuurt. Uitvoeren blijft daarom voorlopig in de webapp.

Alles geverifieerd tegen `pre.openrekening.nl` met **beide** toolchains
(34 controles, identieke uitkomst) — inclusief de volledige MFA-keten:
login vraagt om een code → code geaccepteerd → apparaat-token (64 tekens) →
met dat token wordt MFA overgeslagen → zonder dat token weer een code nodig.

> **Geen TMS FNC.** Het bouwplan noemde de FNC UI Pack, maar dat pakket staat
> niet op deze machine — en een afhankelijkheid die maar in één van beide IDE's
> aanwezig is breekt de dual-compile. Daarom standaard VCL/LCL-controls
> (`TPageControl`, `TListView`, `TEdit`). Komt FNC er later bij, dan zijn
> `uBeheer.pas`, `uDossier.pas` en `uRapport.pas` de enige plekken die
> veranderen.

> **Bank-tab nog niet met echte rijen geverifieerd.** Op staging staan (nog)
> geen bankbestanden of transacties, dus die twee lijsten kwamen leeg terug.
> De code volgt exact hetzelfde patroon als de andere lijsten, maar dat is
> geen bewijs — zodra er een bankbestand is ingelezen moet dit tabblad
> alsnog nagekeken worden.

## WebApp versus desktopclient — functievergelijking

Legenda: ✓ = volledig · ◑ = beperkt/alleen-lezen · ✗ = niet aanwezig.
De kern: de **WebApp** is het complete pakket (beheer + uitvoering +
communicatie + AI); de **client** is een native companion voor raadplegen plus
gerichte acties. Het daadwerkelijk *uitvoeren* van processtappen blijft bewust
in de webapp — daar zitten de bevestigingsstappen, zodat één misklik nooit een
echte aanmaning verstuurt.

### Toegang & beveiliging

| Functie | WebApp | Client |
|---|---|---|
| Inloggen (gebruiker/wachtwoord) | ✓ | ✓ (gebrand inlogvenster met merk-kop) |
| 2FA (TOTP, 6-cijferig) | ✓ | ✓ (6 losse velden, auto-doorloop + backspace) |
| Vertrouwd apparaat onthouden | ✓ | ✓ |
| Wachtwoord wijzigen/vergeten (resetmail) | ✓ | ✓ (hyperlink op het inlogscherm, neutrale melding) |
| Server-instellingen (API-URL + secret) | n.v.t. | ✓ (Instellingen-knop op het inlogscherm) |
| Uitloggen wist alle klantdata + toont direct het inlogvenster | n.v.t. (browser) | ✓ (sinds 1.5; tabs worden volledig afgebroken, lege applicatie blijft over) |
| Werkplek-registratie + ondertekende API-aanroepen | n.v.t. (browser kan geen secret geheimhouden; token+MFA+rate-limiting) | ✓ (sinds 1.6): eigen secret per installatie (DPAPI/0600), HMAC-handtekening met nonce+tijdvenster op élke aanroep, token gebonden aan de werkplek; goedkeuren/intrekken in Beheer ▸ Werkplekken |
| Update-melding bij inloggen (nieuwere versie beschikbaar) | n.v.t. (altijd actueel) | ✓ (sinds 1.5; leest `/downloads/versie.txt`, popup in huisstijl met downloadknop; best-effort) |
| Weergavethema's (standaard/contrast-licht/blauw/donker/contrast-donker + compact) | ✓ (🎨-kiezer) | ✓ (sinds 1.7): menu Weergave; zelfde sleutels, keuze per gebruiker via `thema_save` — webapp en client volgen elkaar; standaard = licht groen venster |
| Bewerkbare UI-teksten per bedrijf (catalogus + overrides) | ✓ (zelfde catalogus via `teksten_catalogus`; beheer-UI volgt) | ✓ (sinds 2.0): Beheer ▸ **Teksten** — standaard + eigen tekst per sleutel, herstellen = leeg; wijzigen door platform of bedrijfsbeheerder; client past overrides toe via `T()` (popup-, dialoog-, notitie- en AI-teksten; groeit per ronde) |
| Navigatiemenu's + rechten per rol | ✓ (menustructuur) | ✓ (sinds 1.8): menubalk-menu's **DebiteurenBeheer / Administratie / Rapportage / Beheer** springen naar de tabs; items en tabs buiten de rol zijn inactief/verborgen (Organisatie+Gebruikers = admin, Instellingen+Werkplekken = beheerder); DebiteurenBeheer-tabs staan vooraan en zijn iets donkerder; merkbalk toont rechts `[gebruiker, rol]` |
| MFA zelf instellen/herstellen | ✓ | ✗ |
| Meekijken (alleen-lezen impersonatie) | ✓ | ✗ |

### Debiteuren, facturen & klanten

| Functie | WebApp | Client |
|---|---|---|
| Debiteuren bekijken/zoeken | ✓ | ◑ (alleen-lezen) |
| Debiteur/factuur aanmaken, bewerken, PDF-viewer | ✓ | ✗ |
| Facturen bekijken (filter op debiteur) | ✓ | ◑ |
| Klant (bedrijf) bewerken/opslaan | ✓ | ✓ |
| Gebruikers per klant | ✓ (beheer) | ◑ lijst mét **bedrijfsrol** (`klant_rol`: beheerder/gebruiker) + **rol wijzigen** (sinds 1.8; platform-beheerder/-manager of de bedrijfsbeheerder van het eigen bedrijf — server bewaakt óók de laatste-bedrijfsbeheerder) |

### Herinnerings- & incassoproces

| Functie | WebApp | Client |
|---|---|---|
| Werkvoorraad bekijken | ✓ | ◑ |
| Dossiers bekijken (status, voortgang, openstaand) | ✓ | ◑ (tab Dossiers, sinds 1.4) |
| Dossier-tijdlijn: verzonden herinneringen + herinnering-PDF openen | ✓ | ◑ (PDF opent in de standaardviewer, sinds 1.4) |
| Herinnering versturen, regeling, incasso, betaling registreren | ✓ | ✗ (bewust in de web) |

### Import & data

| Functie | WebApp | Client |
|---|---|---|
| Batchimport / UBL / bankbestand / OCR | ✓ | ◑ (alleen inzien) / ✗ |
| Klantenbestand CSV import/export (tier + verzuim) | ✓ | ✓ (incl. overgeslagen-rijen-lijst) |

### Communicatie

| Functie | WebApp | Client |
|---|---|---|
| Klantreacties/correspondentie lezen (filter ongekoppeld, berichtinhoud) | ✓ | ◑ (tab Klantreacties, sinds 1.4; antwoorden in de webapp) |
| E-mail versturen, WhatsApp, templates | ✓ | ✗ |
| Interne notities (INTERN GEBRUIK) | ✓ (debiteur én factuur) | ◑ (alleen per debiteur) |

### Inzicht & rapportage

| Functie | WebApp | Client |
|---|---|---|
| Rapportage grafisch (KPI's, grafieken, klikbare drill-down) | ✓ | ✓ (canvas: KPI-tegels, staafgrafiek, donut, ouderdoms-stapelbalken) |
| Rapportage exporteren als afbeelding (PNG 2× 1720×936, presentatiekwaliteit) | ✓ (sinds v4.4) | ✓ (plus keuze voor 1× 860×468) |
| Dashboard met grafieken | ✓ | ✓ (KPI-tegels + grafieken; drill-down alleen in de webapp) |
| Dashboard exporteren als afbeelding (PNG 2×, grafieken + KPI-tegels) | ✓ (sinds v4.5, naast de Excel-download) | ✓ (plus keuze voor 1×) |
| Openstaand (ouderdomsanalyse) + Excel-export | ✓ | ✓ |
| AI-assistent (dossier-samenvatting + advies) | ✓ (lokaal Ollama in de browser óf platform-Claude; A/B-patroonfeedback) | ✓ (sinds 1.9): tab AI-assistent met beide routes — **lokaal via Ollama** (native = geen CORS/OLLAMA_ORIGINS; dossierdata blijft op de werkplek) of **platform-Claude** (server-side); statuscheck + modellenlijst + **Installatiehulp per platform** (Windows/Linux/macOS). A/B-patroonfeedback blijft webapp-only |
| Handleiding | ✓ | ✗ |

### Platform & distributie

| Aspect | WebApp | Client |
|---|---|---|
| Draait op | elke browser / PWA | Windows Win32 + Win64 én Linux x86_64; macOS voorbereid |
| Splash met logo/versie/weblink | n.v.t. | ✓ |
| Installeren | alleen de URL | `setup.exe` (per machine, optionele firewallregel), `install.ps1`, portable zip, Linux-tarball |
| Upgraden | n.v.t. | ✓ (sinds 1.5): `setup.exe` én `install.ps1` deïnstalleren een oude versie eerst automatisch; de gebruikersconfiguratie (.ini) blijft behouden |
| Distributie | n.v.t. | **downloadknop op openrekening.nl** (sinds webapp v4.6; pakketten in `/downloads/` op de server, buiten de deploy-set) + GitHub Release `client-v2.0` (privérepo) |
| Config-locatie | server | naast de exe (portable) of `%APPDATA%` / `~/.config` |
| Code-signing / SmartScreen-vrij | n.v.t. | pijplijn klaar, wacht op EV-certificaat |

> Deze tabel hoort mee te bewegen met de code: krijgt de client (of de webapp)
> er een functie bij, werk dan ook deze vergelijking bij.

## Bestanden

| Bestand | Rol |
|---|---|
| `uApi.pas` | API-laag. **Enige** unit met platformverschillen (achter een FPC-IFDEF): Delphi `System.Net.HttpClient`+`System.JSON`, FPC `fphttpclient`+`fpjson`. |
| `uMerk.pas` | Gedeelde huisstijl: merkkleuren, speciale tekens, het getekende logo (schaalbaar), de kop (splash/login) én de slanke merkbalk met activiteitsnaam (hoofdvenster). |
| `uSplash.pas` | Splash-/over-venster (kop uit `uMerk` + versie + weblink + OK). |
| `uLogin.pas` | Gebrand modaal inlogvenster: login, instellingen (API-URL + secret) en 2FA met zes velden (auto-doorloop + backspace). |
| `uConfig.pas` | Waar staat het `.ini` (portable naast de exe, of `%APPDATA%`), plus ini-/apparaat-token-helpers en de standaard-server-URL. |
| `uMain.pas` | Hoofdvenster: standaard **pulldown-menu** (Account: in-/uitloggen/afsluiten; Help: website + Over) en de **groene merkbalk** met de actieve schermnaam. Delegeert de login aan `uLogin`. |
| `uBeheer.pas` | Beheertabs: Klanten, Gebruikers, Instellingen. Levert ook de PageControl waar `uDossier` zijn tabs aan hangt. |
| `uDossier.pas` | Debiteurenproces-tabs: Debiteuren (+ **interne notities** van de gekozen debiteur), Facturen, Werkvoorraad. Grotendeels lezen; notities toevoegen/verwijderen is de enige schrijfactie. |
| `uInzage.pas` | Inzage-tabs (1.4, volledig alleen-lezen): Dossiers (lijst + tijdlijn + herinnering-PDF openen via `actie_pdf` naar de temp-map) en Klantreacties (correspondentie met ongekoppeld-filter en berichtinhoud). Uitvoeren/antwoorden blijft in de webapp. |
| `uUpdate.pas` | Versiecheck bij inloggen (1.5): haalt `<site-root>/downloads/versie.txt` op (volgt automatisch de eigen omgeving, ook staging), vergelijkt numeriek met `CLIENT_VERSIE` en toont bij een nieuwere versie een popup in huisstijl met downloadknop. Best-effort: elke fout = stil overslaan. Getest via `test_update.dpr`. |
| `uSha256.pas` | Eigen SHA-256 + HMAC-SHA256 (FIPS 180-4/RFC 2104, dependency-vrij, dual-compile). Correctheid bewezen met FIPS- en RFC 4231-vectoren (`test_sig.dpr`) én byte-exact tegen PHP `hash_hmac` (E2E). |
| `uWerkplek.pas` | Lokale werkplek-registratie (1.6): client_id + secret, op Windows versleuteld met **DPAPI** (gebonden aan het Windows-account, niet kopieerbaar), op Linux een 0600-bestand. Het secret gaat na registratie nooit meer over de lijn. |
| `test_werkplek.dpr` | E2E-test van de hele signing-keten tegen staging (13 checks): registreren → wacht → goedkeuren → gebonden token → sabotage/replay/intrekking geweigerd → opruimen. Gebruik: `test_werkplek.exe <cron-secret> [url]`. |
| `uThema.pas` | Weergavethema's (1.7): recursieve kleur-engine (vensters/tabbladen = Venster-kleur, lijsten/invoervelden = Vlak-kleur, labels = LabelTekst; rode waarschuwingslabels en de merkbalk blijven ongemoeid). Zelfde thema-sleutels als de webapp; standaard = licht groen. VCL-gotcha: `ParentBackground := False` (via crack-class, protected op TWinControl) anders wint de systeemthema-achtergrond. |
| `uOllama.pas` | Lokale LLM-koppeling (1.9): detectie (`/api/tags`) + chat (`/api/chat`, stream=false) tegen `127.0.0.1:11434`. Native = geen CORS, dus geen `OLLAMA_ORIGINS` nodig (anders dan de webapp). Bevat de installatiestappen per platform (`OLLAMA_INSTALL_HULP`). Best-effort: niet gevonden = status, nooit een blokkade. |
| `uAiTab.pas` | Tab AI-assistent (1.9): status server (`ai_status`/bronnen per klantbedrijf) + lokale detectie, dossier-samenvatting via de lokale route (`ai_gegevens_dossier` → lokale Ollama; `gegevens_met` is een **object** → als ruwe JSON doorgeven via `JsonSubJson`) of de platformroute (`ai_samenvatting`, server-side Claude). Installatiehulp-venster in huisstijl. Getest via `test_ai.dpr` (echte lokale chat + staging-keten). |
| `uTeksten.pas` | Bewerkbare teksten (2.0): `T('sleutel', 'standaard')`-helper met cache die na de login uit `teksten_catalogus` wordt gevuld (bedrijfs-override wint; zonder cache geldt de standaard — teksten blokkeren nooit iets). Cache hoort bij de sessie (gewist bij uitloggen). |
| `uTekstenTab.pas` | Beheer ▸ Teksten (2.0): catalogus met groep/sleutel/standaard/eigen, bewerken + "Standaard herstellen" (leeg = override weg); platform-rollen kiezen eerst een bedrijf, de bedrijfsbeheerder werkt automatisch in het eigen bedrijf. Server bewaakt sleutels en %s-plaatshouders. |
| `uRapport.pas` | Import, Bank, Openstaand (ouderdomsanalyse) + Excel-export, en **Klantenbestand** (CSV-import/export van debiteuren met tier + verzuim). |
| `uGrafiek.pas` | Canvas-grafiekengine: KPI-tegels, staafgrafiek en donut, getekend met kale `TCanvas`-aanroepen (geen externe chart-componenten — die zouden de dual-compile breken). Hover + klikbare staven/segmenten/legenda. |
| `uDashboard.pas` | Grafisch Dashboard-tabblad: KPI-tegels (werk + gebruik), logins-per-dag- en maandgrafiek, en **Exporteren als afbeelding** (PNG 1×/2×, zelfde compositie als de webapp-export). Alleen-lezen; drill-down blijft in de webapp. |
| `uRapportage.pas` | Grafische Rapportage-tab: 4 KPI-tegels, staafgrafiek per maand, fase-donut, ouderdoms-stapelbalken en dezelfde **drill-down** als de webapp (`rapportage_details`). Plus **Exporteren als afbeelding**: de grafieken + KPI-samenvatting als PNG (offscreen `TekenOp` → `BewaarPng`; VCL pngimage / LCL ingebouwd, dus ook op Linux/macOS). |
| `uOrApiSdk.pas` | **Gegenereerd** — alle 177 API-acties (zie onder). |
| `OpenRekeningClient.dpr` / `.rc` / `.manifest` | Delphi-project (VCL, Win32). |
| `openrekeningclient.lpr` / `.lpi` | Lazarus-project (LCL, Win64). |
| `uVersie.pas` | Eén bron voor het versienummer (app + bouwscript lezen hem). |
| `test_api.dpr` | Console-smoketest van de API-laag (zonder UI). |
| `build.py` | Bouwt en verpakt alles; zie "Bouwen en uitgeven". |

Instellingen komen in `OpenRekeningClient.ini` naast de exe: server-URL en
gebruikersnaam. **Het wachtwoord wordt bewust niet opgeslagen.**

## Gegenereerde SDK (`uOrApiSdk.pas`)

`uOrApiSdk.pas` wordt **gegenereerd** uit `docs/api/openapi.json` (die op zijn
beurt uit `backend/api.php` komt) en bevat alle 177 API-acties:

- een constante per actie (`ACT_DEBITEUR_GET = 'debiteur_get';`) — een tikfout
  in een actienaam wordt daarmee een compileerfout in plaats van een 400 op
  productie;
- een methode per actie op `TOrApiSdk`, met de invoervelden, antwoordvelden en
  autorisatie-eisen in het commentaar.

```pascal
Sdk := TOrApiSdk.Create(Api);
try
  Json := Sdk.DebiteurenList;                       // zonder parameters
  Json := Sdk.DebiteurenList('{"q":"jansen"}');     // met JSON-body
finally
  Sdk.Free;
end;
```

Hergenereren na een API-wijziging:

```bash
python tools/build_openapi.py
python tools/build_delphi_sdk.py
```

De selftest bewaakt dat spec en SDK niet achterlopen op `api.php`.
**Niet met de hand bewerken** — de volgende generatie gooit je wijziging weg.

## Bouwen en uitgeven

**Alles in één commando** (Fase 6):

```bash
python build.py
```

Dat doet: `brcc32` (resource) → `dcc32` (GUI + smoketest, Win32) → `lazbuild`
(GUI, Win64) → `fpc` (smoketest) → OpenSSL-DLL's klaarzetten → zippen in
`dist\`. Met `--snel` slaat hij het verpakken over. Staat je Delphi of Lazarus
elders, zet dan `OR_STUDIO` / `OR_LAZARUS` / `OR_MINGW`.

Er komen twee pakketten uit:

| Pakket | Doel | Inhoud |
|---|---|---|
| `OpenRekeningClient-<versie>-win32.zip` | Delphi/VCL | alleen de exe — **geen** extra bestanden nodig (SChannel zit in Windows) |
| `openrekeningclient-<versie>-win64.zip` | Lazarus/LCL | exe **plus** `libssl-1_1-x64.dll` en `libcrypto-1_1-x64.dll` |

Beide bevatten `LEESMIJ.md`, een ingevuld `OpenRekeningClient.ini`, een
`versie.txt` en het installatiescript `install.ps1`.

> **Geverifieerd vanaf een schone map**, niet alleen "het compileert": beide
> pakketten uitgepakt in een lege map starten daar, en de Win64-smoketest
> haalde met uitsluitend die twee meegeleverde DLL's alle 34 controles binnen
> — inclusief HTTPS en de binaire Excel-download. Dat is het bewijs dat het
> pakket compleet is.

### Installeren (`install.ps1`)

Elke zip bevat een **per-gebruiker installatiescript** (geen beheerdersrechten,
geen extra tooling). Uitpakken en dan:

```powershell
powershell -ExecutionPolicy Bypass -File install.ps1
```

Wat het doet:
1. **Randvoorwaarden controleren** — Windows-versie, en voor de Lazarus-build de
   aanwezigheid van de OpenSSL-DLL's (weigert hard als die ontbreken). De
   Delphi-build is standalone (SChannel); er is **geen** .NET of VC++-runtime
   nodig. Welke build het is, leidt het af uit de échte exe-naam
   (`Get-ChildItem`, want Windows is hoofdletter-ongevoelig).
2. **Kopiëren** naar `%LOCALAPPDATA%\Programs\OpenRekeningClient`.
3. **Server-URL** in het `.ini` zetten (standaard productie; met `-ServerUrl`
   forceer je bv. staging of een eigen server — zonder BOM zodat `TIniFile` hem
   goed leest).
4. **Snelkoppelingen** in het Startmenu (en op het bureaublad, tenzij
   `-NoDesktopShortcut`).
5. **Uninstaller** (`uninstall.ps1`) schrijven en registreren bij *Apps en
   onderdelen* (HKCU) — deïnstalleren kan dus ook via Windows zelf.

Parameters: `-InstallDir`, `-ServerUrl`, `-NoDesktopShortcut`, `-Silent`.
De volledige install→verifieer→deïnstalleer-cyclus is getest voor beide builds
(incl. de harde weigering bij ontbrekende OpenSSL-DLL's).

### Echte installer (`setup.exe`, Inno Setup)

`build.py` compileert daarnaast een **`OpenRekeningClient-<versie>-setup.exe`**
(uit `installer.iss`) zodra Inno Setup 6 aanwezig is
(`winget install JRSoftware.InnoSetup`; `ISCC.exe` wordt automatisch gezocht,
ook de per-gebruiker winget-locatie). De setup verpakt de **standalone
Delphi/VCL-build** en is **per machine** (admin):

- **Randvoorwaarden** — de Delphi-build heeft er geen: SChannel + PowerShell 5.1
  zitten in Windows, geen .NET/VC++/OpenSSL nodig. **Een firewallpoort is niet
  nodig** (de client praat uitsluitend uitgaand HTTPS/443, standaard toegestaan);
  wél is er een **optioneel, standaard-uit** vinkje voor een uitgaande allow-regel
  voor firewalls die uitgaand blokkeren (via `netsh`, opgeruimd bij deïnstallatie).
- **Snelkoppelingen** — Startmenu (app + een link naar de webversie), en het
  bureaublad als je dat aanvinkt.
- **Config schrijfbaar** — omdat Program Files niet schrijfbaar is voor gewone
  gebruikers, bewaart de app zijn `.ini` bij een installatie in
  **`%APPDATA%\OpenRekening`** (zie `uConfig.IniPad`); de meegeleverde template
  met de server-URL wordt bij de eerste start overgenomen. Bij een portable zip
  (schrijfbare map) blijft het `.ini` gewoon naast de exe.
- **Uninstaller** — echte EXE-deïnstallatie + vermelding bij *Apps en
  onderdelen*.

Dezelfde `setup.exe` kan ook **per gebruiker** (zonder admin):
`OpenRekeningClient-<versie>-setup.exe /CURRENTUSER`
(`PrivilegesRequiredOverridesAllowed`). De install→deïnstalleer-cyclus is in die
modus end-to-end getest; `uConfig` (schrijfbaarheid/kopie/ini-pad) is los
geverifieerd onder dcc32 én fpc.

### Linux-build (`build-linux.sh`)

Dezelfde broncode bouwt ook voor **Linux** (Lazarus/LCL, GTK2) — geverifieerd
via WSL2-Ubuntu op deze machine: GUI start onder WSLg, en de console-smoketest
haalde tegen staging alle controles binnen (HTTPS via de systeem-OpenSSL, geen
DLL's nodig). Resultaat: `dist/openrekeningclient-<versie>-linux-x86_64.tar.gz`
(gestripte binary ~7 MB + LEESMIJ + ini-template).

```bash
# eenmalig (Ubuntu): apt-get install -y lazarus libssl-dev build-essential libgtk2.0-dev
wsl -d Ubuntu -u root -e bash /mnt/c/.../client-delphi/build-linux.sh
```

Aandachtspunten: bouw in `~/orbuild`, **niet** in `/mnt/c` (de Windows-
`__lazbuild` bevat win64-`.ppu`'s die botsen); `build-essential` is nodig voor
de `crt*`-linkobjecten en `libgtk2.0-dev` voor de widgetset. Het `.ini` staat
op Linux in `~/.config/openrekening` als de programmamap niet schrijfbaar is
(zie `uConfig`). **macOS** vergt een echte Mac (SDK/notarization) — de code is
er wel op voorbereid (LCL + IFDEF UNIX).

### Appicoon (Windows / Linux / macOS)

`openrekening.ico` (multi-size, gegenereerd uit het splash-logo; bron:
`openrekening-512.png`) is het venster-/taakbalkicoon. Windows/Delphi krijgt
hem **ingebed** via `MAINICON` in de `.rc` — let op: brcc32 (1999) kan geen
PNG-gecomprimeerde iconen aan ("Allocate failed"), dus `build.py` gebruikt de
**Windows-SDK `rc.exe`**. De Lazarus-builds (Win64 + Linux) laden het `.ico`
runtime naast de binary (best-effort, meegeleverd in zip/tarball); de
installer gebruikt hem als `SetupIconFile`. Voor **macOS** is
`openrekening-512.png` de bron: op de Mac met `iconutil` naar `.icns`
converteren (zie taak macOS-build).

### Digitaal ondertekenen (Authenticode / SmartScreen)

`build.py` ondertekent de exe's **en** de `setup.exe` automatisch met
Authenticode (SHA-256 + RFC3161-timestamp) **zodra een certificaat via
omgevingsvariabelen is ingesteld** — anders blijft de build bewust
onondertekend. `signtool.exe` wordt automatisch gezocht (nieuwste Windows-SDK).

| Variabele | Betekenis |
|---|---|
| `OR_SIGN_PFX` (+ `OR_SIGN_PASSWORD`) | onderteken met een `.pfx`-bestand |
| `OR_SIGN_THUMBPRINT` | onderteken met een cert uit de Windows-certificaatstore |
| `OR_SIGN_SUBJECT` | onderteken met een cert op naam (`/n`) |
| `OR_SIGN_TS` | timestamp-URL (standaard `http://timestamp.digicert.com`) |
| `OR_SIGNTOOL` | pad naar een specifieke `signtool.exe` |

> **SmartScreen ≠ zomaar weg met een handtekening.** SmartScreen kijkt naar
> *reputatie*. Een **OV**-certificaat ondertekent wel, maar een verse exe houdt
> de "onbekende app"-waarschuwing tot hij reputatie heeft opgebouwd. Alleen een
> **EV**-certificaat (hardware-token of cloud-HSM), of **Azure Trusted Signing**,
> geeft **direct** SmartScreen-vertrouwen. Een **zelf-ondertekend** cert helpt
> niet (publiek niet vertrouwd). De pijplijn is met het lokale dev-cert getest
> (handtekening + timestamp aantoonbaar gezet); de uitgeleverde `dist`-bestanden
> blijven onondertekend tot er een écht (EV) certificaat wordt meegegeven.

**Versienummer** staat op één plek: `CLIENT_VERSIE` in `uVersie.pas`. De
applicatie zet hem in de venstertitel en `build.py` stempelt er de
pakketnamen mee, dus exe en zip kunnen niet uit elkaar lopen.

### Met de hand bouwen

**Lazarus** (geen extra stappen):

```bash
C:\lazarus\lazbuild.exe openrekeningclient.lpi
```

**Delphi** — dcc32 heeft expliciet de lib-paden én de unit-scope-namen nodig
(die komen normaal uit de IDE-config):

```bash
brcc32 OpenRekeningClient.rc
dcc32 -B "-UC:\Program Files (x86)\Embarcadero\Studio\37.0\lib\win32\release" "-NSSystem;Vcl;Winapi;System.Win;Vcl.Imaging;Data" -N__dcc -E__dcc OpenRekeningClient.dpr
```

Smoketest draaien (zonder argument gaat hij naar staging):

```bash
__dcc\test_api.exe
```

De test print welke omgeving hij raakt. Expliciet meegeven kan ook:

```bash
__dcc\test_api.exe https://pre.openrekening.nl/_pre/backend/api.php
```

Mét inloggegevens draait hij ook het geslaagde pad (token + beveiligde aanroep):

```bash
__dcc\test_api.exe <url> <gebruiker> <wachtwoord>
```

Met een TOTP-code erbij draait hij óók de MFA-keten:

```bash
__dcc\test_api.exe <url> <gebruiker> <wachtwoord> <6-cijferige code>
```

> **Staging-testaccount:** `delphitest@openrekening.nl` (rol beheerder), aangemaakt
> op 22-07-2026 via de `rebirth`-runbookactie met `CRON_SECRET_PRE`. Bestaat
> **alleen op staging** (eigen database) — niet op productie. Het wachtwoord staat
> niet in deze repo; opnieuw zetten kan altijd met dezelfde rebirth-aanroep.
>
> Op dit account staat **MFA aan** (sinds 22-07-2026), zodat de MFA-keten echt
> getest kan worden. De test werkt zowel mét als zonder MFA: komt er `stap=mfa`
> terug en is er een code meegegeven, dan rondt hij de tweede stap gewoon af.

## Valkuilen die we hier al zijn tegengekomen

- **Staging zit in de submap `/_pre/`.** `pre.openrekening.nl` is een *alias*
  van dezelfde webroot als productie; alleen `index.php` doet host-routing.
  Het pad `/backend/api.php` op de pre-host komt dus uit in de **productie**-
  docroot. Staging is `…/_pre/backend/api.php`. Geverifieerd via het
  versielabel in `app.html`: `/_pre/app.html` → `v3.9-pre`, `/app.html` →
  `v3.9`. `ping` verraadt het verschil níet — beide antwoorden identiek.
- **`X-Auth-Token`, niet `Authorization`.** nginx op TransIP geeft de
  `Authorization`-header niet door aan PHP; de API accepteert daarom
  `X-Auth-Token`. De client stuurt die.
- **`{ }`-commentaar nest niet in Pascal.** Een `{$IFDEF ...}` of een
  JSON-voorbeeld met accolades *binnen* een `{ }`-commentaarblok sluit het
  commentaar voortijdig → cryptische "BEGIN expected" / "illegal character"
  fouten. Gebruik `//` of laat accolades uit commentaartekst.
- **`CreateNew` zet géén `Application.MainForm`** (dat doet alleen
  `Application.CreateForm`, en dát vereist een `.dfm`). Zonder MainForm keert
  `Application.Run` direct terug en sluit de app meteen. Daarom draaien we het
  hoofdvenster **modaal** — werkt identiek in VCL en LCL.
- **Windows is hoofdletter-ongevoelig**: `OpenRekeningClient.exe` en
  `openrekeningclient.exe` zijn hetzelfde bestand, dus de tweede build
  overschrijft de eerste. Daarom bouwen beide naar een **eigen map**
  (`__dcc\` en `__lazbuild\`).
- **FPC op Windows heeft OpenSSL-DLL's nodig** voor HTTPS (Delphi gebruikt het
  ingebouwde SChannel en heeft niets nodig). Zonder DLL's: *"Could not
  initialize OpenSSL library"*. FPC 3.2.2 zoekt naar `libssl-1_1-x64.dll` en
  `libcrypto-1_1-x64.dll` — het kent OpenSSL 3's namen (`libssl-3-x64.dll`)
  **niet**. Werkende oplossing: kopieer de OpenSSL-3-DLL's naast de exe ónder
  de 1_1-namen (getest en werkend); ze staan o.a. in
  `C:\Program Files\Git\mingw64\bin`.
- **Evaluatievolgorde van argumenten verschilt tussen Delphi en FPC.** Een
  `out`-parameter vullen én in dezelfde expressie gebruiken
  (`Meld(Api.Ping(Info), '...' + Info)`) geeft op FPC nog de oude waarde.
  Eerst aanroepen, dan de melding bouwen.
- **FPC transcodeert consoletekst niet** naar de console-codepage (Delphi wel);
  niet-ASCII in `Writeln` geeft dan mojibake. In de GUI (LCL) speelt dit niet.

- **Delphi 10.3+ inline-`var` werkt niet in FPC.** `var X: T := ...;` midden in
  een routine compileert wél onder Delphi en níet onder FPC (delphi-mode).
  Declareer in het `var`-blok. Geldt voor alle nieuwere Delphi-syntax: bouw
  altijd bij bèide compilers vóór je verder gaat.

## Volgende stappen

Het bouwplan is doorlopen (Fase 0 t/m 6). Wat er nog ligt:

- **Acties met bevestiging** — herinnering versturen, dossier pauzeren en
  incasso vanuit de client, met een dialoog die toont wát er verstuurd wordt
  en naar wie. Bewust nog niet gebouwd; zie de noot bij Fase 4/5.
- **Bank-tab nakijken** zodra er een bankbestand op staging staat.
- **Ondertekenen van de exe** als hij buiten de eigen machines wordt verspreid
  (zonder handtekening geeft SmartScreen een waarschuwing). Het patroon staat
  in de globale CLAUDE.md bij het ai-host4me-windows-project.

Oudere notitie: **Fase 1** (MFA/sessie compleet — het staging-testaccount
heeft geen MFA, dus dat pad is nog niet doorlopen), **Fase 4** (debiteuren en
facturen: `debiteur_*`, `factuur_*`, `dossier_*`), **Fase 5** (import, bank,
rapportage met Excel-export) en **Fase 6** (Lazarus-build voor andere
platforms).

Wat de beheertabs nog niet doen: klanten aanmaken of verwijderen, en
gebruikersbeheer (accounts, MFA-herstel) — dat loopt bewust nog via de webapp,
waar de bevestigingsstappen en waarschuwingen al zitten.
