Développeurs et intégrateurs
Parqueo — documentation technique
Architecture, modèle de données, API et sécurité. Public visé : l'équipe technique qui intègre, exploite ou audite Parqueo.
- 1. Vue d'ensemble
- 2. Pile technique
- 3. Modèle de données
- 4. Authentification et SSO
- 5. API HTTP
- 6. Workflows
- 7. Automatismes
- 8. Paramètres
- 9. Sécurité
- 10. Dimensionnement et limites
1. Vue d'ensemble#
Parqueo est un logiciel ITSM auto-hébergé : ticketing, inventaire de parc, catalogue de demandes, base de connaissances et automatisation par workflows visuels. Il se déploie en deux processus servis sur une même origine :
Navigateur
│ HTTP
▼
Reverse proxy ──┬── / → client/dist (SPA React statique)
└── /api → API Express (Node, port 4000)
│
┌────────────┼──────────────┐
▼ ▼ ▼
PostgreSQL server/uploads/ SMTP / IMAP
(Prisma) (pièces jointes)
L'API est purement JSON (une seule page HTML : l'enquête de satisfaction). Le
client appelle /api en relatif, ce qui impose de servir front et API sur la
même origine (voir la documentation d'installation).
2. Pile technique#
| Couche | Choix | Version |
|---|---|---|
| Runtime | Node.js (ESM) | 22.x |
| API | Express | ^4.21 |
| ORM | Prisma Client | ^5.20 |
| Base | PostgreSQL | 14+ |
| Auth | jsonwebtoken (HS256) + bcrypt | ^9.0 / ^6.0 |
| Sécurité HTTP | helmet, cors, express-rate-limit | — |
| nodemailer (sortant), imapflow + mailparser (entrant) | — | |
| Inventaire SNMP | net-snmp | ^3.26 |
| Front | React + React Router, build Vite, styles Tailwind | ^19 / ^7 |
Le SSO Microsoft Entra ID est implémenté nativement (fetch + crypto), sans
client OIDC tiers.
2.1 Tests#
Aucun framework : le lanceur intégré node --test suffit.
| Commande | Portée | Base requise |
|---|---|---|
npm test (dans server/) |
logique pure (src/**/*.test.js) : visibilité, workflows, inventaire, SNMP, emails entrants |
non |
npm run test:api (dans server/) |
API de bout en bout (test/**/*.test.js) : requêtes HTTP réelles sur un serveur éphémère |
oui |
npm run test:all (dans server/) |
les deux | oui |
npm test (dans client/) |
client d'API : jeton, interception du 401, erreurs réseau | non |
Les tests d'API utilisent une base séparée, par défaut parqueo_test sur le même
serveur que DATABASE_URL (surchargeable par TEST_DATABASE_URL). Ils la
vident entre les fichiers, d'où le garde-fou de test/setup.js qui refuse toute
base dont le nom ne contient pas « test ». Préparation :
createdb parqueo_test
DATABASE_URL="postgresql://…/parqueo_test" npx prisma migrate deploy
npm run test:all
3. Modèle de données#
PostgreSQL, nommage snake_case en base et camelCase via Prisma.
3.1 Énumérations#
| Enum | Valeurs |
|---|---|
Role |
admin, technician, user |
TicketStatus |
new, in_progress, waiting, resolved, closed |
TicketPriority |
low, medium, high |
CommentType |
comment (message humain), event (journal) |
AssetType |
pc, printer, server, software |
AssetStatus |
in_service, in_repair, retired |
FieldType |
text, textarea, select, date, checkbox |
Le tri par statut ou priorité suit l'ordre de l'enum (new→closed,
low→high), pas l'ordre alphabétique.
3.2 Entités#
User—emailunique,passwordHashnullable (les comptes SSO n'en ont pas),role, équipe optionnelle.Ticket— cœur du modèle : titre, description, statut, priorité, et les liens catégorie (requis), auteur (requis), assigné, équipe, actif, formulaire. Porte l'enquête de satisfaction (1/0/null).TicketComment— messages et journal d'événements, discriminés partype. Cascade avec le ticket.Attachment— métadonnées ; le fichier vit sur le disque.Asset— inventaire : nom, type, emplacement, date d'achat, état, utilisateur assigné. Champs d'inventaire automatique :uuid/serial(uniques, clés de déduplication),source(manual|agent|intune|scan),lastSeenAt, caractéristiques matérielles (manufacturer,model,os,cpu,ramMb,diskGb) etraw(rapport brut).Software/SoftwareInstall— catalogue de logiciels partagé (nom + éditeur, unique) et installations par actif (avec version).Form/FormField— catalogue de demandes.KbArticle— base de connaissances.Setting— table clé/valeur JSON (paramètres globaux, layouts de tableau de bord).Workflow/WorkflowStep/WorkflowRun— voir §6.
3.3 Diagramme relationnel#
Team ──< User ──< Ticket >── Category
│ │ │
│ │ ├──< Form ──< FormField
│ │ └──< KbArticle
│ ├──< TicketComment
│ ├──< Attachment
│ └──< WorkflowRun >── Workflow ──< WorkflowStep
└── Asset ──< SoftwareInstall >── Software
4. Authentification et SSO#
4.1 Jeton#
POST /api/auth/login vérifie l'email/mot de passe (bcrypt) et émet un JWT
HS256 signé avec JWT_SECRET, portant sub, role et teamId. Durée de
vie 7 jours, sans révocation. Le client le stocke et l'envoie en
Authorization: Bearer ….
Conséquence : un changement de rôle ou d'équipe, ou une suppression de compte, ne prend effet qu'à l'expiration du jeton (voir §9).
4.2 Visibilité des tickets#
Appliquée dans la requête SQL, à toute lecture :
| Rôle | Tickets visibles |
|---|---|
admin |
tous |
technician |
ceux dont il est l'auteur, ou qui lui sont assignés, ou non assignés, ou portés par son équipe |
user |
ceux dont il est l'auteur |
Pour l'inventaire, un user ne voit que ses actifs assignés, et l'accès peut
lui être coupé (paramètre assetsVisibleToUsers).
4.3 SSO Microsoft Entra ID#
OIDC authorization code flow, client confidentiel. Activé dès que
SSO_TENANT_ID, SSO_CLIENT_ID et SSO_CLIENT_SECRET sont renseignés
(configuration : documentation d'installation §4).
Navigateur API Parqueo Entra ID
│ GET /api/auth/sso/login │
│─────────────────►│ state = JWT{nonce} (10 min) │
│◄─── 302 ─────────│ │
│──────────────── authorize?…&state&nonce ──────────►│
│◄──────────────── 302 code ─────────────────────────│
│ GET /api/auth/sso/callback?code&state │
│─────────────────►│──── POST /token (secret) ──────►│
│ │◄─── id_token ───────────────────│
│ │ vérifie RS256/JWKS, audience, │
│ │ issuer, nonce ; provisioning JIT │
│◄── 302 ?sso_token=… ────────────────────────────────
- Sans session serveur : le
stateest un JWT signé qui porte le nonce anti-rejeu. - Le jeton d'identité est vérifié en RS256 (clé publique via JWKS), avec contrôle de l'audience, de l'issuer et du nonce.
- Provisioning JIT : un compte inexistant est créé au premier login en rôle
user; toute élévation reste un geste d'administration. SSO_ALLOWED_DOMAINSrestreint les domaines email acceptés.- Les comptes locaux continuent de fonctionner en parallèle (accès de secours).
4.4 Limitation de débit#
Seul /api/auth/login est limité (20 tentatives / 15 min, les connexions
réussies ne comptent pas, pour ne pas bloquer une équipe derrière un même NAT).
5. API HTTP#
Préfixe commun /api. Toutes les routes exigent un jeton sauf
GET /api/health, GET /api/auth/config, POST /api/auth/login, les routes SSO
et GET /api/tickets/satisfaction.
Erreurs : { "error": "message" } avec un statut cohérent (400 validation, 401
auth, 403 rôle, 404 introuvable ou invisible, 409 conflit, 413 corps trop
volumineux, 500 bug). Le 404 sert aussi l'invisible : on ne distingue pas un
ticket inexistant d'un ticket non autorisé.
Toutes les erreurs passent par un middleware d'erreur unique
(src/app.js), alimenté par le routeur de src/lib/router.js qui enveloppe
chaque handler async. Sans lui, Express 4 laisserait fuir les rejets de
promesse : sous Node 20+ un rejet non géré tue le processus, et une simple
requête malformée suffisait à couper l'API. Le middleware traduit aussi les
erreurs Prisma en statuts métier (P2002 → 409, P2003 → 400, P2025 → 404)
et les 5xx sont journalisés avec leur pile. Un identifiant d'URL non numérique
est rejeté en 404 avant d'atteindre la base.
5.1 Authentification#
| Méthode | Chemin | Accès |
|---|---|---|
| GET | /api/health |
public — sonde de vie, ne divulgue rien d'autre |
| GET | /api/setup/status |
public — { needsSetup }, vrai tant qu'aucun compte n'existe |
| POST | /api/setup |
public — crée le premier administrateur ; 409 si un compte existe déjà |
| GET | /api/about |
authentifié — { version, license } |
| GET | /api/avatars/:fichier |
public par capacité — photo de profil, voir ci-dessous |
| POST | /api/auth/avatar |
authentifié — envoie sa propre photo (multipart, 2 Mo, PNG/JPEG/WebP/GIF) |
| DELETE | /api/auth/avatar |
authentifié — retire sa propre photo |
| GET | /api/auth/config |
public — { sso, intune, snmp } pour l'affichage conditionnel de l'UI |
| POST | /api/auth/login |
public — { email, password } → { token, user } |
| GET | /api/auth/sso/login, /api/auth/sso/callback |
public — flux SSO |
| GET | /api/auth/me |
authentifié — profil courant |
| PATCH | /api/auth/password |
authentifié — { currentPassword, newPassword }, change son propre mot de passe |
GET /api/about est authentifiée à dessein : publier le numéro de version d'un
logiciel auto-hébergé indique à un visiteur anonyme quelles failles connues
essayer. /api/health reste publique et ne renvoie que { ok: true }.
GET /api/avatars/:fichier ne demande pas de jeton, et c'est également un choix.
Une balise <img> ne sait pas envoyer d'en-tête Authorization : servir les
photos derrière authentification imposerait un fetch par pastille converti en
object URL, soit jusqu'à cent requêtes non mises en cache sur une liste de
tickets. L'accès est donc protégé par le nom du fichier — 32 caractères
hexadécimaux tirés au hasard, renouvelés à chaque envoi, stockés dans
users.avatar. Connaître un identifiant de compte ne permet pas de deviner
l'adresse de sa photo. La contrepartie assumée : qui obtient l'adresse complète
voit l'image sans être connecté. Les pièces jointes de tickets, elles, restent
authentifiées.
POST /api/auth/login normalise l'email (minuscules, espaces retirés) : les
comptes sont stockés en minuscules à la création, à l'import et via le SSO.
PATCH /api/auth/password exige l'ancien mot de passe, impose 8 caractères
minimum et refuse les comptes SSO (pas de mot de passe local). Les autres
sessions du même compte restent valables jusqu'à leur expiration : le jeton
est sans état, voir §9.
5.2 Tickets#
| Méthode | Chemin | Rôle |
|---|---|---|
| GET | /api/tickets |
tous — filtres + pagination |
| GET | /api/tickets/:id |
tous — détail + commentaires + pièces jointes + état des workflows |
| POST | /api/tickets |
tous — crée, déclenche workflows et notification |
| PATCH | /api/tickets/:id |
admin, technicien |
| POST | /api/tickets/:id/comments |
tous (si visible) |
| POST/GET | /api/tickets/:id/attachments[/:aId] |
tous (si visible) — upload / téléchargement |
| GET | /api/tickets/satisfaction |
public (lien signé) — ?token=&value=up|down |
Filtres : status, priority, assigneeId (id ou none), categoryId,
teamId, q (titre/description), sort. Pagination : avec page, la
réponse est { items, total, counts } (counts = nombre de tickets par statut,
calculé hors filtre de statut) ; pageSize accepte un entier (≤ 500) ou all.
| GET | /api/tickets/stats | tous (filtré) — agrégats du tableau de bord |
| DELETE | /api/tickets/:id | admin — suppression définitive (cascade + fichiers) |
| DELETE | /api/tickets/:id/attachments/:pjId | support, ou l'auteur de l'envoi |
GET /api/tickets/stats renvoie en quelques kilo-octets tout ce qu'affiche le
tableau de bord : répartitions par statut, priorité, catégorie, assigné et
équipe, ventilation par âge, flux hebdomadaire sur douze semaines, compteurs, et
les listes courtes des widgets. Tout est calculé en SQL (GROUP BY) dans les
limites de visibilité du demandeur.
Le filtre ?status= accepte les cinq statuts et le regroupement open
(nouveau + en cours + en attente). counts renvoie un total open en plus du
détail par statut. ?authorId= filtre sur le demandeur ; comme tous les
filtres, il est cumulé en AND avec la visibilité et ne peut donc que la
restreindre.
Longueurs maximales (src/lib/input.js) : titre 200, description et
commentaire 20 000, noms de référentiels 120, article 100 000 caractères. Un
dépassement donne un 400 explicite.
5.3 Inventaire#
| Méthode | Chemin | Rôle |
|---|---|---|
| GET | /api/assets |
tous (filtré) — ?type=&status=&q= |
| GET | /api/assets/:id |
tous (filtré) — tickets + logiciels installés |
| POST / PATCH | /api/assets[/:id] |
admin, technicien |
| DELETE | /api/assets/:id |
admin — 409 si des tickets le référencent |
| GET | /api/software, /api/software/:id |
admin, technicien — catalogue + postes |
Ingestion automatique (hors JWT, appelée par des agents/scripts, protégée par
le token INVENTORY_TOKEN — 404 tant qu'il n'est pas défini) :
| Méthode | Chemin | Auth |
|---|---|---|
| POST | /api/inventory |
token — rapport au format normalisé |
| POST | /api/inventory/glpi |
token — rapport natif de l'agent GLPI (zlib/gzip acceptés) |
| POST | /api/inventory/intune/sync |
JWT admin — synchro Microsoft Intune |
| POST | /api/inventory/snmp/scan |
JWT admin — scan réseau SNMP |
Taille des rapports. Un inventaire réel (matériel, volumes, plusieurs
centaines de logiciels) dépasse couramment les 100 ko. Les routes d'ingestion
sont donc montées avant le parseur JSON global de l'application, avec leurs
propres limites : 5 Mo pour /api/inventory, 15 Mo pour
/api/inventory/glpi. Au-delà, la réponse est un 413. Les autres routes de
l'API restent à la limite par défaut d'Express (100 ko), largement suffisante.
Ne pas remonter express.json() au-dessus de ce montage : le corps serait parsé
en amont et toutes les ingestions replafonneraient à 100 ko, avec des agents
recevant un 413 sans explication.
Mise en œuvre côté exploitant : documentation d'installation §6 ; côté usage : documentation fonctionnelle §9.
5.4 Utilisateurs#
| Méthode | Chemin | Rôle |
|---|---|---|
| GET | /api/users/assignable |
admin, technicien |
| GET / POST / PATCH / DELETE | /api/users[/:id] |
admin |
| POST | /api/users/import |
admin — import en masse (≤ 1000 lignes) |
L'import est ligne à ligne (compte rendu created/skipped/error par
ligne) ; un mot de passe aléatoire est généré et renvoyé en clair quand la ligne
n'en fournit pas. Garde-fous : on ne peut ni retirer son propre rôle admin, ni se
supprimer, ni supprimer un compte porteur de tickets (409).
5.5 Workflows, formulaires, base de connaissances, référentiels#
| Méthode | Chemin | Rôle |
|---|---|---|
| GET/POST/PUT/DELETE | /api/workflows[/:id] |
admin |
| GET | /api/forms[/:id] |
tous (actifs ; ?all=1 admin) |
| POST | /api/forms/:id/submit |
tous — crée le ticket |
| POST/PATCH/DELETE | /api/forms[/:id] |
admin |
| GET | /api/kb[/:id] |
tous (les user ne voient que le publié) |
| POST/PATCH/DELETE | /api/kb[/:id] |
admin, techniciens si autorisé |
| GET | /api/categories, /api/teams |
tous |
| POST | /api/categories, /api/teams |
admin |
Catégories et équipes se suppriment (DELETE /api/categories/:id,
/api/teams/:id, admin) tant qu'elles ne sont référencées par rien : sinon 409
avec le détail des usages. Leurs noms sont uniques.
5.6 Paramètres#
| Méthode | Chemin | Rôle |
|---|---|---|
| GET | /api/settings/app |
tous (le client adapte l'UI) |
| PATCH | /api/settings/app |
admin |
| GET/PUT/DELETE | /api/settings/dashboard[…] |
selon rôle et permission |
6. Workflows#
Un workflow est un graphe orienté qui automatise le traitement des tickets.
- Déclencheur :
ticket_createdoustatus_changed. - Conditions d'entrée (toutes vraies) : catégorie, formulaire, priorité, statut cible.
- Blocs reliés par des fils, avec deux sorties oui/non pour les conditions :
| Catégorie | Bloc | Effet |
|---|---|---|
| Action | assign_team, assign_user |
affecte l'équipe / assigne |
| Action | set_priority, set_status |
change la priorité / le statut |
| Action | add_note, send_email |
journalise / notifie (gabarits {{clé}}) |
| Action | webhook |
POST JSON vers un outil tiers |
| Branche | condition |
teste priorité, statut, catégorie, formulaire ou assignation → oui/non |
| Attente | wait_assigned, wait_status |
parque le ticket jusqu'à la prise en charge / un statut |
Parking et reprise. Un ticket qui atteint un bloc d'attente y reste « parqué » ; il repart de ce bloc dès qu'un changement de statut ou d'assignation satisfait la condition. Un même ticket n'entre qu'une fois dans un workflow donné, et une modification faite par un workflow ne peut pas en redéclencher un autre (anti-boucle par construction).
Gabarits. Les notes et emails acceptent des variables {{ticket.title}},
{{author.name}}, {{assignee.email}}, {{category.name}}, etc.
Webhook. POST JSON { event, workflow, ticket }, en-tête optionnel
X-Parqueo-Token, timeout 5 s ; un échec ne bloque pas le parcours.
7. Automatismes#
Quatre traitements tournent dans le processus de l'API, chacun activé seulement si sa configuration est présente.
Notifications email (mailer.js), chacune débrayable dans les paramètres :
| Notification | Destinataires |
|---|---|
| Création de ticket | demandeur + assigné |
| Changement de statut | demandeur + assigné (+ liens de satisfaction à la résolution) |
| Assignation | le technicien qui reçoit (pas soi-même) |
| Nouveau message | l'autre partie, jamais l'auteur |
Sans SMTP_HOST, rien n'est envoyé (les messages sont journalisés). Un échec
d'envoi ne fait jamais échouer la requête en cours.
Collecteur email. Si IMAP_HOST est configuré : un email d'un utilisateur
connu crée un ticket, une réponse dont le sujet contient Ticket #n devient un
commentaire. Les expéditeurs inconnus et les adresses système sont ignorés.
Prise en charge. Assigner un ticket au statut new le passe
automatiquement en in_progress (src/lib/statut.js), depuis l'API comme
depuis le bloc « assigner » d'un workflow — le statut ne doit pas dépendre de
qui a assigné. La transition est tracée et suit le pipeline normal :
notification et workflows status_changed.
Clôture automatique. Chaque heure, les tickets resolved sans activité depuis
autoCloseDays jours passent en closed (0 = désactivé). La clôture emprunte le
même chemin qu'une clôture manuelle : événement tracé, notification de
changement de statut, et déclenchement des workflows status_changed. Un
automatisme bâti sur le statut « Fermé » se comporte donc pareil, que la
fermeture vienne d'un technicien ou de la tâche de fond. Un ticket en échec est
journalisé et n'interrompt pas la passe.
Inventaire automatique. Toutes les sources (script/agent, Intune, SNMP)
convergent vers un traitement unique : déduplication par uuid puis numéro de
série, création ou rafraîchissement de l'actif. Les champs remontés (matériel,
logiciels, dernière remontée) sont rafraîchis à chaque rapport ; les champs
gérés par l'humain (nom, type, emplacement, état, utilisateur assigné) ne
sont jamais écrasés. Le signalement des actifs « périmés » est purement visuel :
aucun statut n'est modifié automatiquement.
Les logiciels d'un rapport sont écrits en lot (quelques requêtes, quel que
soit le nombre d'applications) : le catalogue manquant est créé d'un bloc, puis
les installations de l'actif sont remplacées dans une transaction. Mesure sur
1 000 logiciels : 92 ms, contre 1 916 ms avec une boucle d'upsert — l'écart se
voit surtout sur une synchronisation Intune, qui enchaîne les machines.
Identité des équipements SNMP. Un switch ou une imprimante n'a pas d'UUID
matériel : le scan fabrique une identité de substitution, du plus solide au plus
fragile — numéro de série (snmp:sn:…, insensible au renommage et au changement
d'IP), sinon sysObjectID + nom système (snmp:oid:…, deux matériels de
familles différentes ne fusionnent pas), sinon adresse IP (snmp:ip:…, fragile
en DHCP). Limite assumée : sans numéro de série, deux équipements laissés au nom
d'usine par défaut restent indistinguables — leur donner un nom système unique
est la parade.
8. Paramètres#
Configuration globale (table settings, clé app.config), éditée dans la page
Paramètres (admin) et lue par tous les comptes pour adapter l'interface.
| Clé | Défaut | Effet |
|---|---|---|
ticketDefaultPriority |
medium |
priorité par défaut |
userCanSetPriority |
true |
un utilisateur peut choisir la priorité |
autoCloseDays |
7 |
délai de clôture automatique (0 = désactivé) |
satisfactionSurvey |
true |
enquête un clic à la résolution |
notifyOnCreate/Status/Assign/Comment |
true |
notifications email |
kbSuggest |
true |
suggestions d'articles à la création |
kbTechniciansWrite |
true |
rédaction de la base de connaissances par les techniciens |
assetsVisibleToUsers |
true |
inventaire visible des utilisateurs |
assetStaleDays |
30 |
délai au-delà duquel un actif automatique est signalé périmé (0 = jamais) |
dashboardPersonalTechnician/User |
false |
personnalisation du tableau de bord |
Écriture réservée aux admins, avec validation stricte : une clé inconnue ou une valeur invalide est refusée (400).
9. Sécurité#
En place :
helmet(en-têtes HTTP) etcorsrestreint àCLIENT_ORIGIN;- mots de passe bcrypt (coût 10), 8 caractères minimum ;
- limitation de débit sur le login ;
- autorisation à deux niveaux : rôle, puis visibilité appliquée dans la requête SQL ;
- pas de cookie de session → pas de surface CSRF ;
- SSO vérifiant signature, audience, issuer et nonce ;
- pièces jointes jamais servies en statique (téléchargement authentifié, avec revérification de la visibilité du ticket) ;
- robustesse : middleware d'erreur unique, handlers
asyncenveloppés, identifiants d'URL validés et champs texte lus défensivement — une requête malformée obtient un 4xx, elle ne peut plus faire tomber le processus ; - mot de passe changeable par son titulaire (ancien mot de passe exigé).
À connaître avant mise en production :
- Jeton en
localStorage: une faille XSS dans le front exposerait le jeton. Le choix évite CSRF et simplifie le déploiement. - Jeton non révocable (7 j) : un changement de rôle, une suppression de
compte ou un changement de mot de passe ne coupent pas les sessions en cours ;
pour une révocation immédiate, il faut faire tourner
JWT_SECRET(déconnecte tout le monde). sso_tokendans l'URL de retour : retiré immédiatement de la barre d'adresse, mais susceptible d'apparaître dans les journaux du reverse proxy — filtrez les query strings.- Collecteur email : l'expéditeur est vérifié par simple correspondance d'adresse (ni SPF/DKIM/DMARC) ; une adresse usurpée peut créer un ticket au nom de la personne usurpée. À filtrer en amont sur le serveur de messagerie. En revanche, une réponse par email applique la visibilité de l'expéditeur : répondre en citant « Ticket #n » n'écrit dans ce ticket que s'il serait visible dans l'interface ; sinon le message devient une demande à part.
- Pièces jointes : liste blanche d'extensions, sans analyse du contenu.
- Débit : seul le login est limité ; ajoutez un quota au reverse proxy si l'API est exposée publiquement.
- Webhook sortant : un administrateur peut cibler une IP interne (SSRF) — le rôle admin est de toute façon de confiance.
- Premier compte : aucun identifiant n'est livré avec l'application. Le
compte administrateur se crée au premier accès via
POST /api/setup, route publique qui ne répond que tant que la base ne contient aucun utilisateur. Le critère porte sur l'existence d'un utilisateur, non d'un administrateur : sinon, une base dont les administrateurs auraient été supprimés permettrait à n'importe qui de s'y déclarer administrateur. Deux appels concurrents sont sérialisés par un verrou consultatif PostgreSQL.
10. Dimensionnement et limites#
- L'API est un processus Node mono-thread léger : quelques dizaines d'utilisateurs simultanés tiennent sur 1 vCPU / 1 Go, PostgreSQL compris.
- Pagination serveur sur les tickets ; l'inventaire, les utilisateurs et la base de connaissances sont paginés côté client — acceptable à leur volume.
- Le tableau de bord interroge
GET /api/tickets/stats, agrégé en SQL : mesuré à 10 000 tickets, 11 ko en 67 ms, contre 5,2 Mo en 320 ms lorsqu'il téléchargeait tous les tickets pour les compter dans le navigateur. - Pièces jointes : stockées sur disque. Elles sont retirées du disque à la
suppression d'un ticket ou de la pièce jointe elle-même ; rien ne purge en
revanche les fichiers d'avant cette version. Surveiller
server/uploads/. - Journal d'audit : la traçabilité repose sur les événements de ticket ; il n'existe pas de journal distinct des créations de comptes, changements de rôle ou de paramètres.