# API Tracker MCC WMS

Documentation de l'API HTTP du dashboard de tracking MCC WMS — read-mostly,
exposée par le container `mcc-wms-dashboard` sur le VPS.

> Cette API expose la base SQLite `tracker.db` (events de tous les flux),
> les archives sur disque (`/opt/mcc-wms/data/sync/`,
> `/opt/mcc-wms/data/inbound/picking_response/`...) et un lookup live sur
> l'API Stealth. Elle est consommable par un humain (curl, Postman) ou par
> un agent IA (champ `summary` LLM-friendly sur certains endpoints).

---

## Base URL

```
https://wms-tracker.mccstudio.eu
```

Le container interne écoute sur `:8088`. L'exposition publique passe par
Traefik (Coolify) avec basic auth + TLS Let's Encrypt.

## Authentification

**Basic auth HTTP** (toutes les routes du domaine).

Deux comptes configurés dans le middleware Traefik
`mcc-wms-auth.basicauth.users` :

| User | Rôle | Droits |
|------|------|--------|
| `mcc` | Admin / IT | Accès complet, vue tech, replay, resend, downloads |
| `adv` | Administration des Ventes | Vue ADV forcée sur `/pl/`, lecture seule |

```bash
# Auth via curl
curl -u mcc:<password> https://wms-tracker.mccstudio.eu/api/stats
curl -u adv:<password> https://wms-tracker.mccstudio.eu/api/v1/pl/NS-190/timeline
```

> Le password ADV courant : `Oxouqx.4QAao.fAhmB`
> Pour ajouter un user : modifier le label Traefik
> `mcc-wms-auth.basicauth.users=mcc:$apr1$...,adv:$apr1$...,nouveau:$apr1$...`

## Format général des réponses

| Type | Content-Type | Format |
|------|--------------|--------|
| Endpoints `/api/v1/*` | `application/json; charset=utf-8` | JSON enrichi avec champ `summary` text LLM-friendly |
| Endpoints `/api/*` (legacy) | `application/json` | JSON brut |
| Routes HTML (`/`, `/pl/`, etc.) | `text/html; charset=utf-8` | Pages rendues |
| Downloads (`/api/file/*`, `/api/dequeue-zip/*`) | `application/xml`, `application/zip`, etc. | Binaire avec `Content-Disposition: attachment` |

Codes HTTP standard : `200` OK · `400` mauvaise requête · `401` non auth ·
`403` interdit · `404` introuvable · `413` payload trop gros · `500` erreur
serveur.

---

## Endpoints API JSON

### `GET /api/v1/pl/{ref}/timeline`

Cycle de vie complet d'une Picking List.

**Paramètres URL :**
- `ref` (path) — Référence PL dans l'un de ces 3 formats :
  - `FR1-2026-NS-190` (canonique)
  - `2026NS190` (forme filename)
  - `NS-190` (raccourci, suppose année courante + société FR1)

**Query params :**
- `view=adv|tech` (default `adv`) — Niveau de détail
  - `adv` : 5 étapes métier, sans jargon technique
  - `tech` : 8 étapes complètes avec event_id et filenames

**Exemple :**

```bash
curl -u mcc:<password> "https://wms-tracker.mccstudio.eu/api/v1/pl/NS-190/timeline?view=adv"
```

**Réponse type :**

```json
{
  "summary": "FR1-2026-NS-190 - (B2B, 32 lignes) - creee 13/05/2026 09:49 - envoyee Reflex 13/05/2026 11:34 - => En attente bon de livraison ADV",
  "ref": "FR1-2026-NS-190",
  "parsed": {"company": "FR1", "year": 2026, "index": "NS", "number": 190, "canonical": "FR1-2026-NS-190"},
  "metadata": {
    "type": "B2B",
    "operator": "CYRIL",
    "doc_id": "693091028",
    "cli_cod": "003992",
    "lines_count": 32,
    "shopify_ref": "",
    "carrier_code": "100",
    "carrier_name": "100",
    "delivery": null
  },
  "stages": [
    {
      "stage": "stealth_created",
      "timestamp": "2026-05-13T09:49:39",
      "actor": "stealth",
      "display_name": "1. Commande saisie",
      "summary": "Creee dans Stealth par CYRIL (32 lignes)"
    },
    {
      "stage": "pl_generated",
      "timestamp": "2026-05-13T11:34:01",
      "actor": "middleware",
      "display_name": "2. Bon de preparation pret",
      "summary": "Bon de preparation genere (TXT Reflex)",
      "tracker_event_id": 108235,
      "filename": "COMMANDES_2026NS190_20260513113358.txt"
    },
    {
      "stage": "pl_uploaded",
      "timestamp": "2026-05-13T11:34:02",
      "actor": "middleware",
      "display_name": "3. Envoyee a l'entrepot",
      "summary": "Envoyee a l'entrepot Reflex (SFTP)"
    },
    {
      "stage": "boinpicked_sent",
      "timestamp": "2026-05-13T15:39:02",
      "actor": "middleware",
      "display_name": "4. Preparation terminee",
      "summary": "BOINPICKED envoye a Stealth (tracking colis : 871747188492)",
      "tracking": "871747188492",
      "tracking_present": true,
      "shopify_ref": "",
      "status": "ok",
      "error_message": "",
      "error_category": null,
      "error_label": "",
      "error_hint": ""
    }
  ],
  "tracking": {
    "present": true,
    "value": "871747188492",
    "note": null
  },
  "current_status": {
    "stealth_logisticStatus": "to_send",
    "stealth_allocStatus": "30",
    "stealth_found_in_allocation": true
  },
  "verdict": "ready_to_ship",
  "status_label": "En attente bon de livraison ADV",
  "view": "adv",
  "generated_at": "2026-05-18T15:51:33"
}
```

**Verdicts possibles :**

| `verdict` | `status_label` | Sens |
|-----------|----------------|------|
| `unknown` | Inconnue (aucune trace) | Aucun event dans le tracker |
| `queued` | En attente d'envoi vers l'entrepot | Créée dans Stealth, pas encore générée |
| `in_warehouse` | En preparation chez Reflex | Uploadée à Reflex, pas de réponse |
| `ready_to_ship` | En attente bon de livraison ADV | BOINPICKED OK, attend Delivery Note |
| `boinpicked_rejected` | Rejetee par Stealth - correction necessaire | Erreur BOINPICKED catégorisée |
| `transient_error` | Erreur technique transitoire - rejouer possible | HTTP 5xx ou timeout |
| `annulee` | PL introuvable / Annulee | PL absente de Stealth |
| `expediee` | Expediee | logisticStatus = sent |

**Catégories d'erreur (`error_category` sur le stage `boinpicked_sent`) :**

| Catégorie | Sens |
|-----------|------|
| `couleur_inexistante` | Variante article (couleur) inconnue du catalogue maître |
| `modele_inexistant` | Modèle article inconnu du catalogue maître |
| `deja_traitee` | BOINPICKED déjà traité avec succès (faux positif d'erreur) |
| `pl_non_trouvee` | PL absente de Stealth (probablement annulée) |
| `quantite_invalide` | Quantité incorrecte dans le BOINPICKED |
| `http_5xx_stealth_down` | Serveur Stealth indisponible |
| `http_4xx` | Requête refusée (auth/format) |
| `timeout` | Timeout sur l'appel API |
| `autre` | Erreur non catégorisée |

---

### `POST /api/replay/{event_id}`

Rejoue un event en erreur : copie le fichier source vers `incoming/` du flux,
**marque automatiquement les events `error` précédents en `error_superseded`**
pour contourner la protection anti-boucle.

**Paramètres :** `event_id` (path) — ID numérique de l'event en erreur.

**Pré-conditions :**
- L'event doit avoir `status='error'`
- Le fichier source doit exister sur disque (chemin retourné par `/api/details/{id}`)
- Le chemin source doit être confiné à `/opt/mcc-wms/`

**Exemple :**

```bash
curl -u mcc:<password> -X POST "https://wms-tracker.mccstudio.eu/api/replay/135506"
```

**Réponse type :**

```json
{
  "status": "ok",
  "message": "Fichier INT5001329.txt copie dans incoming/ — sera retraite au prochain cycle (2 event(s) error precedent(s) marque(s) error_superseded)",
  "target": "/opt/mcc-wms/data/inbound/picking_response/incoming/INT5001329.txt",
  "superseded": 2
}
```

**Erreurs :**
- `{"status":"error","message":"Evenement introuvable"}` → 404 logique
- `{"status":"error","message":"Seuls les evenements en erreur peuvent etre rejoues"}`
- `{"status":"error","message":"Fichier source introuvable"}`
- `{"status":"error","message":"Chemin non autorise"}` → tentative path traversal

---

### `POST /api/resend_boinpicked/{event_id}`

Renvoie à l'API Stealth un BOINPICKED XML déjà généré (utile si le premier
envoi avait échoué pour cause de Stealth indispo, référentiel corrigé, etc.).

**Implémentation** : `urllib.request` stdlib pur (le container n'a pas le venv
host monté ni `requests` installé). POST login → PUT XML sur
`/v1/core/system/adapter/queue/enqueue/REFLEX_WEB?boName=PICKINGLIST` avec
headers `X-Operation: Picked`, `X-ProcessingMode: sync`.

**Pré-conditions :**
- `flow='picking_response'`
- Le `source_path` doit pointer vers un `.xml` dans `output/` ou `sent_api/`

**Exemple :**

```bash
curl -u mcc:<password> -X POST "https://wms-tracker.mccstudio.eu/api/resend_boinpicked/135506"
```

**Réponse type :**

```json
{
  "status": "ok",
  "message": "Envoi BOINPICKED OK (RETURN_CODE=0)",
  "return_code": "0",
  "http_code": 200,
  "errors": []
}
```

Le nouvel event tracker `api_send` contient automatiquement la ref PL dans
ses `details` (extrait depuis le XML BOINPICKED) pour que la recherche
intelligente le retrouve.

---

### `POST /api/resend-batch`

Rejoue plusieurs events en une seule requête (max 50/appel).

**Deux modes :**

```bash
# Mode 1 : liste explicite d'event_ids
curl -u mcc:<password> -X POST "https://wms-tracker.mccstudio.eu/api/resend-batch?ids=123,456,789"

# Mode 2 : auto-pick tous les api_send/error actuels (max 50)
curl -u mcc:<password> -X POST "https://wms-tracker.mccstudio.eu/api/resend-batch?mode=all_errors"
```

**Réponse type :**

```json
{
  "summary": "23 events rejoues : 12 OK, 11 erreurs",
  "total": 23,
  "ok": 12,
  "error": 11,
  "results": [
    {"event_id": 136307, "status": "ok", "return_code": "0", "message": "Envoi BOINPICKED OK"},
    {"event_id": 136294, "status": "error", "return_code": "-1", "message": "Echec : HTTP 200, RETURN_CODE=-1 | ..."}
  ],
  "generated_at": "2026-05-19T14:39:08"
}
```

---

### `GET /api/details/{event_id}`

Détail complet d'un event tracker, enrichi avec :
- `source_path` (chemin du fichier source si trouvé)
- `error_log_path` + `error_log` (log .err inline)
- `has_source` / `can_replay` / `can_resend_api` (capacités UI)
- `timestamp` formaté en `DD/MM/YYYY HH:MM`

**Exemple :**

```bash
curl -u mcc:<password> "https://wms-tracker.mccstudio.eu/api/details/135506"
```

**Réponse type :**

```json
{
  "id": 135506,
  "timestamp": "18/05/2026 12:16",
  "env": "prod",
  "direction": "inbound",
  "flow": "picking_response",
  "filename": "INT5001329.txt",
  "action": "error",
  "status": "error",
  "details": "Date: 2026-05-18 12:16:26\nFichier: INT5001329.txt\n...",
  "source_path": "/opt/mcc-wms/data/inbound/picking_response/error/20260518/INT5001329.txt",
  "has_source": true,
  "error_log_path": "/opt/mcc-wms/data/inbound/picking_response/error/20260518/INT5001329.error.log",
  "error_log": "...",
  "can_replay": true,
  "can_resend_api": false
}
```

---

### `GET /api/stats`

Stats globales du tracker (compteurs sur les dernières 24h).

```bash
curl -u mcc:<password> "https://wms-tracker.mccstudio.eu/api/stats"
```

---

### `GET /api/file/{event_id}`

Download du fichier source d'un event (XML, TXT, etc.).

```bash
curl -u mcc:<password> -O -J "https://wms-tracker.mccstudio.eu/api/file/108234"
```

Renvoie `Content-Disposition: attachment; filename="..."`.

---

### `GET /api/errlog/{event_id}`

Download du log d'erreur `.err` ou `.error.log` associé à un event.

```bash
curl -u mcc:<password> -O -J "https://wms-tracker.mccstudio.eu/api/errlog/135506"
```

---

### `GET /api/dequeue-file/{TYPE}/{filename}`

Download d'un XML de la dequeue store.

**Paramètres :**
- `TYPE` (path) — un de : `BARCODE`, `CUSTOMER`, `CUSTOMERINVOICE`,
  `STYLEITEM`, `SUPPLIER`, `WHMOVEMENT_03`
- `filename` (path) — nom complet du fichier (validé contre path traversal)
- `source=store|duplicates` (query, default `store`)

**Exemple :**

```bash
curl -u mcc:<password> -O \
  "https://wms-tracker.mccstudio.eu/api/dequeue-file/CUSTOMERINVOICE/CUSTOMERINVOICE_692871576_20260408T102556292_1.xml"

# Variante depuis le dossier duplicates
curl -u mcc:<password> -O \
  "https://wms-tracker.mccstudio.eu/api/dequeue-file/CUSTOMER/CUSTOMER_xxx.xml?source=duplicates"
```

**Sécurité :** path traversal bloqué, fichier doit être confiné à
`/opt/mcc-wms/data/sync/`.

---

### `GET /api/dequeue-zip/{TYPE}`

Téléchargement en zip de tous les fichiers d'un type (avec ou sans filtre).

**Paramètres :**
- `TYPE` (path) — un des 6 types valides
- `source=store|duplicates` (query, default `store`)
- `q=<motif>` (query, optionnel) — filtre substring sur le nom de fichier

**Limites :**
- Taille input max : **5 GB** → HTTP 413 sinon
- Streaming serveur : TTFB < 1s même sur 17K fichiers (testé STYLEITEM 2.2 GB en 19s)

**Exemple :**

```bash
# Tout le store CUSTOMERINVOICE
curl -u mcc:<password> -o invoices.zip \
  "https://wms-tracker.mccstudio.eu/api/dequeue-zip/CUSTOMERINVOICE?source=store"

# Filtré par date
curl -u mcc:<password> -o invoices_20260415.zip \
  "https://wms-tracker.mccstudio.eu/api/dequeue-zip/CUSTOMERINVOICE?source=store&q=20260415"
```

**Headers réponse :**
- `Content-Disposition: attachment; filename="dequeue-{TYPE}-{source}[-q-{filtre}]-{timestamp}.zip"`
- `X-Files-Total: N` (nb de fichiers dans le zip)

---

## Routes HTML (annexes)

Pour info — pas faites pour une consommation programmatique, mais utiles pour
debug interactif :

| URL | Rôle |
|-----|------|
| `/` | Index tracker avec filtres |
| `/pl/` | Page de recherche PL |
| `/pl/{ref}` | Vue timeline HTML (ADV par défaut, toggle tech si user `mcc`) |
| `/doc` | Documentation projet HTML |
| `/dequeue` | Index explorateur dequeue |
| `/dequeue/{TYPE}` | Liste paginée d'un type |
| `/missing-pl` + `.csv` | Commandes web payées sans PL |
| `/pl-no-response` + `.csv` | PL uploadées sans réponse Reflex |
| `/pr-rejected` | Picking Responses rejetées |
| `/retours` | Suivi retours web |

---

## Recherche intelligente de PL

Le filtre **filename** du tracker (`?filename=...`) génère automatiquement les
variantes d'une ref PL pour matcher TOUS les events liés (outbound + inbound) :

| Tu tapes | Cherche aussi |
|----------|---------------|
| `FR1-2026-NS-214` | `2026NS214` |
| `2026NS214` | `FR1-2026-NS-214` |
| `NS-214` ou `NS214` | `2026NS214`, `FR1-2026-NS-214` (année courante) |

Du coup une requête `?filename=NS214` retourne maintenant :
- Les events outbound (filename `COMMANDES_2026NS214_*.txt`)
- ET les events inbound (filename `INT5001367.xml`, avec ref dans `details`)

Pour rappel, la même logique s'applique à `/api/v1/pl/{ref}/timeline` qui
accepte les 3 formats.

## Modèle de données : tracker.db

Table `file_events` (SQLite, sur le VPS `/opt/mcc-wms/data/tracker.db`).

| Colonne | Type | Description |
|---------|------|-------------|
| `id` | INTEGER PK | ID auto-incrément |
| `timestamp` | TEXT | ISO 8601 local (Europe/Paris) `2026-05-18T15:30:42` |
| `env` | TEXT | `prod` ou `test` |
| `direction` | TEXT | `outbound` / `inbound` / `sync` / `retour` |
| `flow` | TEXT | `picking_list`, `picking_response`, `customer`, `supplier`, `dequeue`, `retour_web` |
| `filename` | TEXT | Nom du fichier (sans path) |
| `action` | TEXT | `archive`, `output`, `ftp_upload`, `ftp_download`, `api_send`, `replay`, `error`, `stealth_synced`, `stealth_cancelled`, etc. |
| `status` | TEXT | `ok`, `error`, `error_superseded`, `pending`, `empty`, `skipped` |
| `details` | TEXT | Message libre / contexte |

**Index :** `idx_timestamp` sur `timestamp`.

**Politique de rétention :** events > 30 jours purgés via le workflow n8n
Maintenance (cf doc projet).

---

## Cas d'usage typiques

### Diagnostiquer une PL bloquée

```bash
# 1. Récupérer le statut
curl -u mcc:<password> "https://wms-tracker.mccstudio.eu/api/v1/pl/NS-190/timeline?view=tech" \
  | jq '.verdict, .status_label, .stages[-1]'

# 2. Si rejet BOINPICKED -> voir la catégorie d'erreur
curl -u mcc:<password> "https://wms-tracker.mccstudio.eu/api/v1/pl/NS-190/timeline?view=tech" \
  | jq '.stages[] | select(.stage=="boinpicked_sent") | {error_category, error_label, error_hint, error_message}'

# 3. Récupérer le détail de l'event en erreur
EVENT_ID=$(curl -s -u mcc:<password> "https://wms-tracker.mccstudio.eu/api/v1/pl/NS-190/timeline?view=tech" \
  | jq -r '.stages[] | select(.stage=="boinpicked_sent") | .tracker_event_id')
curl -u mcc:<password> "https://wms-tracker.mccstudio.eu/api/details/$EVENT_ID" | jq

# 4. Rejouer si l'erreur a été corrigée côté Stealth
curl -u mcc:<password> -X POST "https://wms-tracker.mccstudio.eu/api/replay/$EVENT_ID"
```

### Récupérer toutes les factures du jour pour Sage

```bash
TODAY=$(date +%Y%m%d)
curl -u mcc:<password> -o "invoices_$TODAY.zip" \
  "https://wms-tracker.mccstudio.eu/api/dequeue-zip/CUSTOMERINVOICE?source=store&q=$TODAY"
```

### Rejouer un batch de PL après correction de référentiel

```bash
# Pour chaque event_id en erreur que tu veux rejouer
for ID in 133568 133571 133574; do
  curl -u mcc:<password> -X POST "https://wms-tracker.mccstudio.eu/api/replay/$ID"
  echo
done
```

---

## Notes d'implémentation récentes

- **TZ container forcée Paris** (19/05/2026) : `dashboard.py` met
  `os.environ["TZ"]="Europe/Paris"; time.tzset()` au top du module. Sans ça,
  les events tracés par le container sont en UTC (décalage 2h en été).
- **`resend_boinpicked` en stdlib pur** (19/05/2026) : plus de subprocess vers
  `/opt/mcc-wms/venv/bin/python` (chemin inexistant dans le container). Tout
  passe par `urllib.request`. Avantage bonus : ~3× plus rapide.
- **Recherche tracker intelligente** (19/05/2026) : expansion auto des refs
  PL via `_expand_pl_ref_variants()` dans `tracker.py`.
- **Verdict timeline basé sur le meilleur historique** : si une PL a eu un
  `api_send/ok`, le verdict reste `ready_to_ship` même si des retries
  ultérieurs échouent.

## Roadmap (endpoints V2)

Pas encore implémentés, prévus pour la phase agent IA :

| Endpoint | Rôle | Statut |
|----------|------|--------|
| `GET /api/v1/health` | Résumé global (nb events 24h, flux silencieux, etc.) | À faire |
| `GET /api/v1/anomalies?period=24h` | Cas qui sortent du nominal (règles d'alerte) | À faire |
| `GET /api/v1/errors?since=ISO` | Erreurs récentes avec context et log | À faire |
| `GET /api/v1/events/{id}` | Détail event v1 standardisé | À faire |
| `GET /api/v1/flows/{flow}/stats?period=24h` | Volume / p50 / p95 / taux erreur | À faire |
| `GET /api/v1/stealth/pl?refs=A,B,C` | Statuts Stealth en batch (proxy) | À faire |

Format de réponse cible identique au timeline : enveloppe avec `summary`,
`data`, `meta` + champ `summary` par item.

---

## Conventions futures

- **Versioning** : tout nouvel endpoint sous `/api/v1/`. Pas d'évolution
  breaking sur les endpoints stables.
- **Read-only strict** : aucun `POST` qui modifie l'état métier hors des 2
  actions documentées (`replay`, `resend_boinpicked`). Pour l'agent IA, on
  reste sur du `GET` pur.
- **Token bearer dédié** (futur) : pour les appels agent IA, prévoir un
  `Authorization: Bearer <token>` séparé de la basic auth (révocable
  individuellement). Pas implémenté tant qu'on reste en consommation interne.

---

## Références

- Code serveur : `/opt/mcc-wms/scripts/dashboard.py` (snapshot local :
  `03_DEPLOY/deploy_vps_snapshot/dashboard.py`)
- Tracker DB : `/opt/mcc-wms/data/tracker.db` (SQLite)
- Logs container : `docker logs mcc-wms-dashboard`
- Doc projet métier : `https://wms-tracker.mccstudio.eu/doc`
- Mémoires liées : `pl_timeline_adv_auth.md`,
  `stealth_boinpicked_error_categories.md`, `dashboard_runs_in_docker.md`
