MAIDEO SERVICES · POC v1.2 · Septembre 2026 · Spec de référence : /opt/poc/poc-casse/SPEC.md
Navigateur (SPA Vue 3)
│ HTTPS (TLS Let's Encrypt)
▼
Caddy (droplet maideo-lab) poc-casse.lab.ikki.io
├── /api/* → reverse_proxy 127.0.0.1:4701
└── /* → fichiers statiques /opt/poc/poc-casse/web/
▼
Fastify (conteneur poc-casse-api, port 4701)
│ driver mongodb
▼
MongoDB 7 (conteneur poc-casse-mongo, NON exposé)
Sync entrante (sens unique, 1×/heure) :
Mongo PROD Maideo (readonly) ──► script sync-poc-casse-missions.mjs
(droplet maideo-intelligence) ──► POST /api/maideo/index (Bearer)
Deux principes structurants : le POC ne se connecte jamais à la production (la donnée prod est poussée vers lui, en lecture seule côté prod) et chaque conteneur Mongo est dédié au POC (aucune BDD partagée).
| Composant | Détail |
|---|---|
| Hôte | Droplet maideo-lab — jamais de POC sur un droplet de prod |
| Racine | /opt/poc/poc-casse/ (api/, web/, docker-compose.yml, .env, SPEC.md) |
| Conteneurs | poc-casse-api (Node 22 alpine, Fastify 5, mongodb 6) · poc-casse-mongo (mongo:7, volume mongo_data) |
| Réseau | API publiée sur 127.0.0.1:4701 uniquement ; Mongo sans clef ports (jamais exposée) |
| Reverse proxy | /etc/caddy/poc/poc-casse.caddy — TLS automatique Let's Encrypt sur poc-casse.lab.ikki.io |
| Config | .env : API_TOKEN (Bearer) · MONGO_URL et PORT injectés par compose |
Commandes usuelles :
ssh maideo-lab
cd /opt/poc/poc-casse
docker compose up -d --build # (re)déployer après modif du code api/
docker compose logs -f api # logs applicatifs
docker compose restart api # redémarrage simple
curl https://poc-casse.lab.ikki.io/api/health # sonde publique
/api/*, vérifié par un hook Fastify onRequest. Seule exception : GET /api/health (sonde publique sans donnée)..env côté serveur et dans localStorage côté navigateur (poc_casse_token).maideoMissionId, maideoClientId).q.replace(/[.*+?^${}()|[\]\\]/g…)) contre l'injection RegExp.role === 'direction' exigé, HTTP 403 sinon) — pas seulement masquée côté UI. NB : au stade POC le rôle est déclaratif (choisi dans le sélecteur d'identité), sans authentification individuelle.poc_casse)dossiers{
ref: "INC-2026-0001", // compteur annuel (collection counters)
status: "declare|instruction|decision|execution|cloture",
level: "N1|N2|N3", // calculé à la déclaration (refAmount)
clientName: "…",
missionRef: "2026-…", // n° dossier Maideo (numeroDossier)
maideoMissionId: "…", maideoClientId: "…", // ids prod (lecture seule)
workerName: "…", channel: "telephone|email|client_app|worker",
description: "…", refAmount: 250 | null,
owner: "…", // jamais vide si ouvert (invariant I1)
insurance: { suggested, status, claimRef, declaredAt,
ganContract: "314879172001", ganAgency: "Dreux" },
decision: null | { type, amount, proposedBy, proposedAt,
directionRequired, status: "en_attente_direction|validee",
validatedBy, validatedAt },
nextAction: { label, dueDate } | null, // null seulement si clôturé (I2)
messages: [{ at, actor, body }],
history: [{ at, actor, action, note }],
createdAt, updatedAt, closedAt, outcome
}
| Collection | Rôle | Index |
|---|---|---|
missions_index | Miroir des missions prod (numeroDossier, clientName, workerNames, status, prestation, isAbonnement, syncedAt) alimenté par la sync horaire | numeroDossier unique, clientName |
notifications | Journal de TOUTE notification émise (dossierId, type, recipient, channel, body, sentAt). Canal externe email/SMS hors scope : channel: "log" | dossierId + sentAt |
counters | Séquence annuelle des références INC-{année}-{nnnn} (findOneAndUpdate atomique) | — |
api/index.js)| Constante | Valeur | Champ visé | Effet |
|---|---|---|---|
REF_THRESHOLD_N2 | 150 € | ref_amount (réclamé) | < 150 → N1, sinon N2 |
REF_THRESHOLD_N3 | 300 € | ref_amount | > 300 → N3 + assurance suggérée |
DIRECTION_THRESHOLD | 300 € | decision_amount (décision) | > 300 → validation Direction requise, quel que soit le niveau |
Montant réclamé inconnu (null) → N2 par prudence.
declare → instruction → decision → execution → cloture
TRANSITIONS) : aucun saut ni retour arrière (HTTP 422).decision.status === 'validee' obligatoire pour entrer en execution.owner non vide (à la prise en charge, l'acteur devient owner si « A assigner »).nextAction { label, dueDate } non vide ; null uniquement à la clôture.Tant que decision.status !== 'validee', tout message client matchant /rembours|indemnis|dédommag|dedommage|compensation financière/i est rejeté (HTTP 422 formulation_bloquee). Protection contre la reconnaissance de responsabilité prématurée.
Base : https://poc-casse.lab.ikki.io — header Authorization: Bearer <API_TOKEN> partout sauf /api/health.
| Méthode · path | Rôle | Codes d'erreur notables |
|---|---|---|
GET /api/health | Sonde publique { ok, service, time } | — |
GET /api/meta | Seuils, infos Gan, liste des statuts (consommés par le front) | 401 |
GET /api/dossiers?status=&level= | Liste (max 500, tri récent d'abord) | 401 |
POST /api/dossiers | Création. Body : numeroDossier? (préremplit depuis l'index), clientName, description, refAmount?, channel?, workerName?, owner?, actor? | 400 champs requis · 422 numeroDossier inconnu / invariant |
GET /api/dossiers/:id | Détail complet | 400 id · 404 |
POST /api/dossiers/:id/transition | Changement d'étape. Body : to, actor, note?, nextAction? | 422 transition interdite / décision non validée / invariant |
POST /api/dossiers/:id/decision | Proposer une décision. Body : type (remboursement | geste_commercial | refus | renvoi_assurance), amount, actor. > 300 € → en_attente_direction + notif Direction | 400 type/montant · 422 mauvaise étape |
POST /api/dossiers/:id/validate | Validation Direction. Body : actor, role: "direction" | 403 rôle · 422 rien en attente |
POST /api/dossiers/:id/insurance | Statut Gan. Body : status (a_declarer | declaree | en_cours | acceptee | refusee), claimRef?, actor? | 400 statut |
POST /api/dossiers/:id/messages | Message client consigné. Body : body, actor? | 400 vide · 422 formulation_bloquee |
POST /api/dossiers/:id/update | Réassignation owner / nextAction manuelle (invariants appliqués) | 422 invariant |
GET /api/dossiers/:id/notifications | Journal des notifications du dossier (100 dernières) | 400 id |
GET /api/stats | Compteurs par statut/niveau, actions en retard, créations du mois | 401 |
POST /api/maideo/index | Réception d'un lot de missions (upsert par numeroDossier) — appelé par la sync uniquement | 400 items[] |
GET /api/maideo/missions/search?q= | Autocomplete déclaration (n° dossier ou nom client, 10 résultats, q ≥ 2 car.) | 401 |
sync-poc-casse-missions.mjs sur le droplet maideo-intelligence, déclenché toutes les heures (cron). Il lit la prod avec l'utilisateur readonly (missions actives + terminées depuis < 90 j) et pousse par lots vers POST /api/maideo/index.numeroDossier : la sync est idempotente, rejouable sans doublon.numeroDossier est fourni et connu de l'index, l'API impose clientName, joint workerNames et stocke maideoMissionId/maideoClientId. N° inconnu → HTTP 422 (« sync en cours ? »).web/index.html : Vue 3 (CDN vue.global.prod.js) + Tailwind v4 (CDN browser). Pas d'étape de build — choix assumé pour un dimensionnement de 4–5 dossiers/mois.try_files {path} /index.html).localStorage (poc_casse_token) ; tout appel API envoie le Bearer.actorRole) : pilote l'affichage du bouton « Valider (Direction) » — le contrôle réel reste côté serveur./manuel.html (utilisateur) et /manuel-technique.html (ce document), imprimables en PDF via la feuille de style @media print.| Besoin | Commande / emplacement |
|---|---|
| Santé de l'app | curl https://poc-casse.lab.ikki.io/api/health |
| Logs API | docker compose logs -f api (dans /opt/poc/poc-casse) |
| Logs Caddy | journalctl -u caddy -f |
| Données | Volume Docker poc-casse_mongo_data — backup : docker exec poc-casse-mongo mongodump --out /tmp/dump |
| Rotation token | Modifier API_TOKEN dans .env puis docker compose up -d ; redistribuer le token aux utilisateurs |
| Modifier la config Caddy | Éditer /etc/caddy/poc/poc-casse.caddy → caddy validate → systemctl reload caddy |
| Forcer une sync missions | Relancer manuellement sync-poc-casse-missions.mjs sur maideo-intelligence |
channel: "log" — aucun email/SMS réel. En prod : brancher Resend / Twilio sur la collection notifications.