mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r2.4): portail public QR — signalement sans compte, suivi par jeton
- 3 routes publiques /portal (les seules @Public métier), throttlées
(@nestjs/throttler : 10 signalements/min, 60 lectures/min) : résolution
du QR, signalement (retourne un jeton de suivi opaque), suivi par
référence + jeton — pas de jeton, pas de lecture, jamais de liste
- migration r2_portail : Request.publicToken (unique)
- page /q/{réf} fidèle à l'écran 6 validé R0 (mobile d'abord) : équipement
prérempli « détecté par le QR », interrupteur personne bloquée,
suivi sans jargon Reçu → Intervention → Résolu (dérivé de l'OT lié),
rejet affiché « Sans suite : {motif} », photo annoncée (upload R3) ;
signalements gardés sur le téléphone (localStorage, 10 max)
- e2e : LA boucle produit — gardien sans compte → traitement → ✓ Résolu ;
11 tests Playwright verts, 50 tests API, 52 opérations au contrat
- générateur OpenAPI : paramètres non-« id » plus typés uuid
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,24 @@ Trace chronologique des sessions (la plus récente en premier). Le **playbook**
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-16 — Pr. Daaif (+ Claude) — R2.4 : portail public QR — l'étiquette prend vie
|
||||
|
||||
**Actions**
|
||||
|
||||
- **Trois routes publiques `/portal`** (les seules routes @Public métier de l'API), **throttlées** (`@nestjs/throttler` : 10 signalements/min, 60 lectures/min — première surface sans compte) : résolution du QR (`référence → appareil`), signalement (retourne un **jeton de suivi opaque**), suivi par `référence + jeton` (pas de jeton, pas de lecture — aucune énumération possible).
|
||||
- **Suivi sans compte** : `Request.publicToken` (migration `r2_portail`) ; le téléphone du gardien garde `[référence, jeton]` en localStorage (10 max) ; étapes sans jargon **Reçu → Intervention → Résolu** dérivées de l'OT lié ; un rejet s'affiche « Sans suite : {motif} ».
|
||||
- **Page `/q/{réf}`** fidèle à l'écran 6 validé R0 (mobile d'abord) : équipement prérempli « détecté par le QR », gros interrupteur personne bloquée, description, photo annoncée (upload → R3), messages d'erreur en français métier (QR inconnu, throttling).
|
||||
- **e2e Playwright** : LA boucle produit — le gardien signale sans compte sur `/q/A1`, l'équipe traite (approbation → démarrage → bilan → clôture via API), le gardien recharge et voit ✓ Résolu. 11 tests Playwright verts, 50 tests API, 52 opérations au contrat. Générateur OpenAPI : les paramètres non-`id` ne sont plus typés uuid.
|
||||
|
||||
**Décisions**
|
||||
|
||||
- Le portail n'expose JAMAIS de liste : lecture uniquement par jeton individuel.
|
||||
- Throttling au niveau du contrôleur portail seulement (le reste de l'API est derrière JWT).
|
||||
|
||||
**R2 est fonctionnellement complète** (OT, bilan codé, demandes, portail QR, préventif, compteurs). Prochaine étape : recette R2 avec le référent (revue pixel ↔ maquettes R0+R2), déploiement, tag `release/r2`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-16 — Pr. Daaif (+ Claude) — R2.3 : écrans web de l'exploitation
|
||||
|
||||
**Actions**
|
||||
|
||||
@@ -956,6 +956,119 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/portal/assets/{reference}": {
|
||||
"get": {
|
||||
"operationId": "getPortalAsset",
|
||||
"summary": "Résoudre le QR scanné (référence → appareil, préremplissage)",
|
||||
"tags": [
|
||||
"portal"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Appareil",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PortalAsset"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Référence inconnue"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/portal/requests": {
|
||||
"post": {
|
||||
"operationId": "createPortalRequest",
|
||||
"summary": "Signaler sans compte — renvoie le jeton de suivi (à garder sur le téléphone)",
|
||||
"tags": [
|
||||
"portal"
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PortalRequestCreate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "Signalement enregistré",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PortalRequestCreated"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Référence inconnue"
|
||||
},
|
||||
"429": {
|
||||
"description": "Trop de signalements — réessayez dans une minute"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/portal/requests/{reference}/{token}": {
|
||||
"get": {
|
||||
"operationId": "getPortalRequestStatus",
|
||||
"summary": "Suivi sans jargon (Reçu → Intervention → Résolu) — jeton requis",
|
||||
"tags": [
|
||||
"portal"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "reference",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "token",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Avancement",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PortalRequestStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Signalement inconnu ou jeton invalide"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/preventive/templates": {
|
||||
"get": {
|
||||
"operationId": "listTaskTemplates",
|
||||
@@ -3634,6 +3747,171 @@
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PortalAsset": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reference": {
|
||||
"type": "string"
|
||||
},
|
||||
"siteName": {
|
||||
"type": "string"
|
||||
},
|
||||
"locationName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reference",
|
||||
"siteName",
|
||||
"locationName"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PortalRequestCreated": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reference": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isPersonTrapped": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"RECEIVED",
|
||||
"APPROVED",
|
||||
"REJECTED"
|
||||
]
|
||||
},
|
||||
"step": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"RECEIVED",
|
||||
"IN_PROGRESS",
|
||||
"RESOLVED"
|
||||
]
|
||||
},
|
||||
"rejectionReason": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
},
|
||||
"publicToken": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reference",
|
||||
"description",
|
||||
"isPersonTrapped",
|
||||
"status",
|
||||
"step",
|
||||
"rejectionReason",
|
||||
"createdAt",
|
||||
"publicToken"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PortalRequestCreate": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetReference": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 30
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 1000
|
||||
},
|
||||
"isPersonTrapped": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"requesterName": {
|
||||
"type": "string",
|
||||
"maxLength": 120
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetReference",
|
||||
"description"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"PortalRequestStatus": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reference": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isPersonTrapped": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"RECEIVED",
|
||||
"APPROVED",
|
||||
"REJECTED"
|
||||
]
|
||||
},
|
||||
"step": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"RECEIVED",
|
||||
"IN_PROGRESS",
|
||||
"RESOLVED"
|
||||
]
|
||||
},
|
||||
"rejectionReason": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"reference",
|
||||
"description",
|
||||
"isPersonTrapped",
|
||||
"status",
|
||||
"step",
|
||||
"rejectionReason",
|
||||
"createdAt"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"TaskTemplatesResponse": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user