Guide développeur
Cargamento est une application PHP 8.2 sans framework ni dépendance Composer, avec MariaDB, servie par Apache. L'interface est en JavaScript sans bibliothèque, les dessins en SVG générés par le code.
Architecture
Trois couches, avec le moteur de calcul isolé (fiche technique §7) :
| Couche | Rôle | Où |
|---|---|---|
| Écrans | Collectent les saisies, dessinent les résultats. Aucune règle métier. | views/app/index.php, public/assets/js/app.js |
| Vitrine et comptes | Site public, connexion, demande d'accès, compte. | src/Controller/Site.php, views/site/* |
| Moteur | Fonctions pures, sans DOM, sans base, sans HTTP. | src/Engine/*.php et public/assets/js/engine.js |
| API et back-office | Clés, validation, plans enregistrés, référentiels. | src/Api, src/Controller, src/Service |
Deux moteurs jumeaux
Le serveur n'a pas Node : le moteur existe en PHP (API, tests) et en JavaScript (navigateur, calcul instantané et hors connexion). Les deux sont écrits ligne à ligne en miroir et produisent la même structure JSON, aux mêmes clés.
Règle d'or : toute modification d'un moteur est reportée dans l'autre dans le même commit, puis vérifiée par php tests/parity.php, qui génère 500 cas aléatoires (palettisations et chargements), les calcule des deux côtés (le JS dans Chromium headless) et compare chaque champ.
Points d'attention pour garder la parité :
- Arrondis :
Num::round()reproduitMath.round(demi vers le haut), pas l'arrondi de PHP. - Format des nombres :
Num::fmt()etfmt()écrits à la main (espace fine insécable U+202F pour les milliers), sanstoLocaleStringninumber_format. - Tris :
usort(stable en PHP 8) etArray.sort(stable) avec les mêmes comparateurs<=>. - Clés de regroupement
arrêt|L|l: PHP et JS écrivent les flottants de la même façon (121.9,120). - Tolérance de
1e-9sur les divisions (120 / 40 doit donner 3).
Fonctions du moteur
| PHP | JS | Rôle |
|---|---|---|
Palletizer::palletize($in) | palletize(in) | Palettisation d'une référence (§4), découpage de commande, lignes prêtes pour le camion. |
Palletizer::bestLayer() | bestLayer() | Meilleur plan de couche : grille simple, blocs mixtes en longueur et en largeur. |
TruckPlanner::plan($in) | planTruck(in) | Dépliage, rejet, gerbage, tri, rangées, numérotation, indicateurs, jauges, alertes (§5, §6). |
TruckPlanner::evaluate($in, $layout) | evaluateLayout(in, layout) | Chargement manuel : contrôle une disposition posée à la main et la chiffre comme un plan automatique. |
TruckPlanner::dockLayout($plan) | dockLayout(plan) | Plan de quai : plancher pivoté, rangée 1 côté porte (§6.3). |
Num::num(), Num::fmt() | num(), fmt() | Lecture « 12,5 » / « 12.5 », affichage français. |
plan() et evaluate() partagent expand() (palettes unitaires, rejets), finish() (numérotation, indicateurs, jauges, alertes, quai) et stackOut().
Chargement manuel
- Disposition :
[{ units: ['L1#1', 'L1#2'], x, y, rotated }]. Chaque palette unitaire a une clé stable « id de ligne # rang dans la ligne » (stack.unitKeysdans les sorties).unitsva du bas vers le haut ;rotated= côté long en travers. - Contrôles (
TruckPlanner::ISSUES/E.ISSUES) : bloquantsoutside,overlap(recouvrement > 0,5 cm),tooHigh,formatMix,notStackable,tooManyLevels; avertissementsheavyOnLight,stopInStack,blockedStop(A livré avant B mais centre de A plus près de la cabine que celui de B). Chaque placement porte sesissues; une alerte par type liste les numéros. - Rangées déduites : tri par
xpuisy; une pile rejoint la rangée en cours si elle commence avant la moitié de la profondeur de la première pile de la rangée. Numérotation ensuite identique au mode automatique. - Palettes non posées :
overflowavecreason = 'unplaced', alerteunplaced(avertissement). - L'interface (
app.js) ne fait que proposer des positions : aimantation, première place libre, fantôme vert/rouge. Avant d'enregistrer un déplacement, elle appelleevaluateLayout()et refuse si la pile concernée a un contrôle bloquant (message tiré deE.ISSUES). Après chaque calcul, la disposition stockée est relue depuis la sortie du moteur (clés disparues retirées). - État navigateur :
mode(auto/manual) etlayoutdanscargamento-v1.
Les sorties détaillées sont décrites dans la documentation de l'API (/aide/api) et dans docs/openapi.json.
Routes
| Chemin | Accès | Rôle |
|---|---|---|
/ | public | Vitrine. |
/demande-acces | public | Formulaire de demande d'accès (table access_requests). |
/mentions-legales | public | Mentions légales, données personnelles, cookies (views/site/legal.php). À tenir à jour si un traitement ou un cookie change. |
/connexion, /deconnexion | public | Connexion unique outil + administration. ?retour= accepte seulement un chemin local. |
/aide/{slug} | public | Guides Markdown. |
/app | compte | L'outil (trois onglets). |
/p/{id} | compte | Plan enregistré par l'API, ouvert dans l'outil. |
/compte | compte | Changement de mot de passe. |
/admin/* | rôle admin | Back-office. |
/api/v1/* | clé d'API | API REST, indépendante des comptes. |
/_shot | jeton disque | Connexion temporaire pour les captures (voir plus bas), 404 sinon. |
Comptes et sessions
- Table
users:role=userouadmin,active.Auth::require()exige un compte,Auth::requireAdmin()le rôle admin (403 sinon). Un compte désactivé ou supprimé est déconnecté à la requête suivante. - Session PHP (
cargamento_sid, HttpOnly, Secure, SameSite=Lax), régénérée à la connexion. - « Rester connecté » : cookie
cargamento_rm=selector:validator, tableremember_tokens(empreinte SHA-256 du validator, 30 jours). Le jeton est renouvelé à chaque usage ; un validator faux sur un selector connu supprime le jeton (rejeu d'un cookie volé). Changer de mot de passe, en recevoir un nouveau ou être désactivé supprime tous les jetons de l'utilisateur. Ce mécanisme existe parce que le nettoyage des sessions de Debian (gc_maxlifetime) déconnecterait les tablettes du quai. - Anti force brute : table
login_attempts, 10 échecs par IP sur 15 minutes. Les tentatives d'une IP sont effacées après une connexion réussie. - CSRF : jeton de session dans chaque formulaire POST (
Auth::csrfField()/Auth::csrfCheck()).
Arborescence
bin/ install.php, create-admin.php, create-api-key.php, make-doc-shots.php, build-openapi.php
config/ config.php (non versionné), config.sample.php
database/ schema.sql, seed.sql (idempotents)
deploy/ vhosts Apache HTTP et HTTPS
docs/ guides Markdown rendus sur /aide, openapi.json
public/ index.php (contrôleur frontal), sw.js, manifest, assets/
src/Engine/ moteur PHP (pur)
src/Api/ Api.php (routes, auth, erreurs), Input.php (validation)
src/Controller/ Site.php (vitrine, connexion, demandes, compte), Web.php (outil, plans, aide), Admin.php
src/Core/ Config, Database, Router, View, Auth, Markdown
src/Service/ Referentials, ApiKeys, Plans
storage/logs/ php-error.log
tests/ engine.php, parity.php, api.php, web.php, manual-ui.php, cases.json
views/ gabarits PHP
Installation
git clone … /var/www/cargamento
cd /var/www/cargamento
cp config/config.sample.php config/config.php # renseigner la base et l'URL
mysql -e "CREATE DATABASE cargamento CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
php bin/install.php # tables + référentiels par défaut
php bin/create-admin.php email@societe.fr "Nom"
cp deploy/cargamento.conf deploy/cargamento-ssl.conf /etc/apache2/sites-available/
a2ensite cargamento cargamento-ssl && systemctl reload apache2
chown -R www-data:root . && chmod 640 config/config.php
Certificat : certbot certonly --webroot -w /var/www/cargamento/public -d <domaine>. Le vhost HTTP laisse passer /.well-known/acme-challenge/ et redirige le reste en HTTPS.
bin/install.php est idempotent : il crée les tables manquantes et les référentiels par défaut sans écraser les valeurs modifiées. À relancer après chaque mise à jour du schéma.
Configuration
| Clé | Rôle |
|---|---|
app.url | URL publique, utilisée pour les liens de plans partagés renvoyés par l'API. |
app.debug | Affiche les erreurs PHP. Jamais en production. |
app.version | Affichée en pied de page. |
db.* | Connexion MariaDB. |
security.cookie_secure | Cookie de session du back-office en HTTPS uniquement. |
api.rate_per_minute | Appels par clé et par minute (120). |
api.max_pallets | Palettes unitaires max par calcul (2 000). |
Tests
php tests/engine.php # cas T1 à T11 de la fiche §8.1 + compléments, performance (< 50 ms)
php tests/parity.php # parité PHP / JS : 500 cas aléatoires + 125 chargements manuels perturbés (Chromium requis)
php tests/api.php # API de bout en bout sur l'URL configurée (crée puis supprime une clé de test)
php tests/web.php # vitrine, connexion, rester connecté, droits, demande d'accès (comptes de test supprimés à la fin)
php tests/manual-ui.php # chargement manuel dans Chromium : bac, toucher, glisser, gerber, refus, rotation, clavier, Quai
Les trois sortent avec un code non nul en cas d'échec : à brancher tels quels en intégration continue.
Persistance
- Outil : saisies dans le
localStoragedu navigateur, clécargamento-v1, partagé par tous les comptes qui utilisent le même navigateur. Rien côté serveur (fiche §7.3, V1). - Comptes :
users,remember_tokens,login_attempts; demandes d'accès :access_requests(avec l'IP pour la limite de débit). - Plans d'API : table
plans, colonneinput= saisie normalisée (véhicule résolu en cotes, commandes déjà palettisées en lignes). Les résultats ne sont jamais stockés : ils sont recalculés à chaque lecture. - Clés : table
api_keys, empreinte SHA-256 seulement. - Journal : table
api_log, sans corps de requête. Purge à prévoir si le volume devient important.
Interface
- Champs numériques en
type="text" inputmode="decimal": un champtype="number"refuse la virgule selon la langue du navigateur. - L'onglet actif est mémorisé et reflété dans l'URL (
#palettisation,#camion,#quai). - Couleurs en variables CSS dans
:root, redéfinies pour le thème sombre (prefers-color-scheme) et forcées pardata-themesur<html>. Les 8 couleurs de lignes ont un contraste ≥ 4,5:1 avec le texte blanc dans les deux thèmes. - Service worker
public/sw.js: réseau d'abord pour les pages, cache d'abord pour/assets/(versionnés par?v=<mtime>). Seules/appet/p/…sont mises en cache, et seulement si la réponse n'est pas une redirection (sinon on servirait la page de connexion hors ligne). ChangerCACHEdanssw.jsforce un renouvellement complet. - Polices hébergées localement (
public/assets/fonts) : aucun appel à un service tiers.
Documentation
Les guides sont dans docs/*.md, rendus sur /aide/{slug} par src/Core/Markdown.php (sous-ensemble : titres, listes, tableaux, code, images, citations). https://cargamento.vozit.dev est remplacé par app.url. Les captures du guide utilisateur (public/assets/img/aide/*.png) et de la vitrine (public/assets/img/site/*.webp, dont des recadrages *-zoom mesurés dans la page et les captures du chargement manuel préparées par script dans un cadre) se régénèrent avec php bin/make-doc-shots.php. L'outil exigeant un compte, le script écrit un jeton dans storage/shot-token (valable 5 minutes, supprimé à la fin) que la route /_shot échange contre une session du premier administrateur actif.
À chaque livraison : mettre à jour les quatre guides, la vitrine (views/site/home.php) et ses captures, docs/openapi.json si l'API change (php bin/build-openapi.php docs/openapi.json), et ajouter une entrée datée dans docs/00-changelog.md.
Pièges connus
- Apache ne transmet pas l'en-tête
Authorizationà PHP sans la règle depublic/.htaccess: ne pas la retirer. Input::loadPlan()palettise les commandes avant le calcul : l'identifiant d'une palette incomplète est<id>-partial.- Le moteur accepte des saisies imparfaites (texte, vide = 0) pour l'outil ; c'est l'API qui refuse les saisies invalides avec un 422 détaillé.
- Les limites connues de l'heuristique (fiche §5.3) sont voulues : pas de mélange de formats dans une pile, pas de mélange d'arrêts dans une rangée, le poids n'est pas un critère de placement.
Évolutions prévues (V2, fiche §8.3)
Export PDF, comptes et sauvegarde des plans dans l'outil, charge par essieu, vue 3D, palettes mixtes, contraintes produits (ADR, alimentaire), chargement latéral, import WMS / TMS, plan de couche en 5 blocs.