Architecture¶
Vue d'ensemble¶
flowchart LR
ios[App iOS] -->|HTTPS + JWT| nginx
portal["Portail web<br/>portal/"] -->|HTTPS + JWT| nginx
agent[Agent IA] -->|MCP + jeton cbt_| nginx
cron[Planificateur<br/>SYNC_INTERVAL / cron] -->|X-Sync-Token| nginx
nginx[nginx<br/>fichiers statiques] -->|/api/ FastCGI| api
subgraph serveur[PHP-FPM]
api["src/api.php"] --> ws[Webservice]
ws --> resolver["ApiResolver<br/>attributs ApiRoute"]
resolver --> res["src/resources/*"]
res --> db[(MariaDB / MySQL)]
res --> woob[Woob<br/>commande shell]
res --> apns[Apns<br/>HTTP/2]
end
woob --> banque[(Banque)]
apns --> apple[Apple Push<br/>Notification service]
| Couche | Rôle |
|---|---|
| nginx | Sert le portail (/portal/) et Swagger UI (/swagger/) avec leurs en-têtes de sécurité, transmet /api/ à PHP-FPM (sans tampon, pour les flux SSE) ; tout autre chemin renvoie 404 |
src/api.php |
Point d'entrée : sans configuration, ne sert que l'assistant d'installation (Setup) ; sinon toute URL sans point est traitée comme un appel d'API ; convertit les erreurs en réponse JSON avec code HTTP |
Webservice |
Lit le corps JSON et la query string, positionne les en-têtes (sécurité, JSON ou SSE), contrôle l'accès (JWT), appelle la ressource |
ApiResolver |
Découvre les routes par réflexion sur l'attribut #[ApiRoute(path, method, public, stream, raw)], avec cache APCu invalidé quand un fichier de ressource change |
src/resources/ |
Logique métier : User, Bank, Transaction, Budget, Category, Insight, Device, ApiToken, Mcp, System |
Setup, Installer |
Assistant d'installation (/api/setup) : base, schéma, premier administrateur, data/conf/prod.ini |
Migrator |
Versionnement du schéma (table schema_migrations) et application des migrations |
Setting |
Réglages modifiés depuis le portail (table settings) |
Db |
Connexion mysqli unique, requêtes préparées |
Woob |
Exécution de woob (proc_open) avec envoi de heartbeats SSE pendant l'attente |
Apns |
Envoi des notifications (authentification par clé .p8 ou certificat) |
Logger |
Logs bufferisés dans data/logs/carbure_AAAAMMJJ.log, avec un identifiant (uid) par requête |
Traitement d'une requête¶
sequenceDiagram
participant C as Client (iOS / portail)
participant A as api.php
participant W as Webservice
participant R as ApiResolver
participant X as Ressource
C->>A: GET /api/transaction?month=9&year=2026
A->>W: exec()
W->>R: findRoute("/transaction", "GET")
R-->>W: Transaction::get (protégée)
W->>W: vérifie le JWT (Authorization: Bearer)
W->>X: Transaction::get(month: 9, year: 2026)
X-->>W: tableau de transactions
W-->>C: 200 JSON
Note over A,C: En cas d'erreur : code HTTP + {"error", "code", "uid"}
Les paramètres sont passés par nom : la clé month de la requête alimente le
paramètre $month de la méthode. Un paramètre inconnu provoque une erreur 400.
Synchronisation bancaire¶
sequenceDiagram
participant P as Cron / client
participant B as Bank::getSync
participant W as Woob
participant T as Transaction
participant D as Device / Apns
P->>B: GET /api/bank/sync (JWT ou sync_token)
B->>B: GET_LOCK('bank_sync') — une seule synchro à la fois
loop Pour chaque compte (numéro@banque)
B->>W: woob bank coming <compte> -f json
W-->>B: opérations à venir (type 12 conservé)
B->>T: save() : nettoyage du libellé, UUID, INSERT … ON DUPLICATE KEY
T-->>B: transactions nouvelles
B->>W: woob bank history <compte> -f json
W-->>B: historique
B->>T: save()
T-->>B: transactions nouvelles
B->>B: date et résultat dans bank_account.last_sync_*
B->>D: notification à chaque utilisateur du foyer<br/>(nombre de transactions non pointées)
B->>D: alerte « dépense importante » pour chaque nouvelle dépense<br/>au-delà du seuil alert_threshold d'un utilisateur
end
B->>T: updateMissingCategories() (mots-clés)
B-->>P: flux SSE « Synchronization complete »
?account=<numéro>@<banque>limite la synchronisation à un compte (bouton de synchronisation de l'onglet Comptes).- Les transactions importées sont rattachées au premier utilisateur ayant ajouté le compte
(
MIN(user_id)dansbank_account), sans restreindre leur visibilité. - Le libellé est mis en majuscules puis nettoyé par les expressions régulières
regex_labelde la configuration. - L'UUID d'une transaction est
md5(date réelle + libellé + montant)(libellé tronqué à 24 caractères pour les paiements par carte) : il permet de dédoublonner les transactions entre deux synchronisations et de transformer une opération « en cours » en opération débitée. - Les virements de type 1 (salaire) datés après le 25 sont rattachés au mois suivant.
- La catégorisation recherche les mots-clés de
bank_transaction_category_keyworddans le libellé des transactions sans catégorie du dernier mois (ou de tout l'historique avecPOST /category/keyword/apply). - Une transaction est nouvelle quand son UUID n'existait pas (l'
INSERTcrée une ligne). Seules les nouvelles dépenses déclenchent l'alerte de seuil : une resynchronisation ou le passage d'une opération « en cours » à « débitée » ne ré-alerte pas.
Portail web¶
Le portail (portal/) est une application Vue 3 sans étape de build : index.html
charge Vue, i18n.js et le module app.js. L'état et les méthodes sont répartis en
mixins (portal/modules/), les appels HTTP sont centralisés dans
portal/services/apiService.js. Voir Portail web.
Installation et mises à jour du schéma¶
sequenceDiagram
participant N as Navigateur
participant S as api.php / Setup
participant D as MariaDB
N->>S: GET /api/setup (pas de data/conf/prod.ini)
S-->>N: valeurs par défaut (environnement Docker), code requis ?
N->>S: POST /api/setup/database
S->>D: connexion, table users ? migrations en attente ?
S-->>N: state = none | current | outdated
N->>S: POST /api/setup/install
S->>D: sql/carbure.sql + baseline, ou migrations (sauvegarde confirmée)
S->>D: premier administrateur (s'il n'y en a pas)
S-->>N: data/conf/prod.ini écrit avec des secrets aléatoires
- Le schéma est versionné :
Migratorliste les fichierssql/migrations/*.sql, la tableschema_migrations(créée au besoin) enregistre ceux appliqués, et la version du schéma est le dernier. Une installation neuve crée le schéma depuissql/carbure.sqlet enregistre toutes les migrations comme appliquées (baseline). - Une migration peut déclarer
-- applied-if: <requête>: si la requête renvoie un nombre non nul (migration déjà appliquée à la main), elle est seulement enregistrée. - Les migrations en attente sont appliquées au démarrage du conteneur Docker
(
tools/migrate.php, le conteneur s'arrête si l'une échoue), dans l'assistant pour une base existante, ou par un administrateur depuis le bandeau du portail (POST /api/system/migrate).
Serveur MCP¶
POST /api/mcp (Mcp::handle) est une route publique au sens du routeur, déclarée
raw : la méthode renvoie elle-même le corps JSON-RPC. Elle vérifie que le serveur est
activé (Setting mcp_enabled), l'en-tête Origin, puis authentifie le jeton d'accès
(ApiToken::authenticate, empreinte SHA-256, expiration) ou un JWT, et exécute les outils
en lecture seule en réutilisant les ressources (Transaction, Budget, Category) avec
l'identité de l'utilisateur du jeton. Voir Agents IA (MCP).
Image Docker¶
| Élément | Rôle |
|---|---|
docker/Dockerfile |
php:8.3-fpm-alpine + nginx (image multi-étapes : woob est construit à part, sans compilateur dans l'image finale), extensions mysqli et APCu, woob et curl_cffi dans /opt/woob, HEALTHCHECK sur /api/health |
docker/nginx.conf |
Site nginx (portail, Swagger, API, en-têtes de sécurité) |
docker/php.ini, docker/php-fpm.conf |
Réglages PHP et PHP-FPM |
docker/entrypoint.sh |
Prépare /data, attend la base et applique les migrations si Carbure est installé, lance la synchronisation périodique (SYNC_INTERVAL), puis PHP-FPM et nginx |
Volume /data (le dossier data/ du projet) |
conf/prod.ini, conf/setup.code, conf/certs/, logs/, cache/, configuration woob (woob/) |