# TicketGate

**TicketGate** est un sas d'entrée, de batch et de filtre avant la création automatique de tickets dans **maj-wp**.

Il reçoit des demandes (Gmail, Monday API, extension Chrome Monday), les normalise, déduplique, puis attend une **validation humaine** avant de les exposer à maj-wp en mode **pull**.

```
SOURCE → INGESTION → NORMALISATION → DÉDUPLICATION → CANDIDAT
  → validation humaine → APPROVED → claim maj-wp → ticket créé → sync état
```

## Architecture

Monorepo npm workspaces :

| Composant | Stack | Rôle |
|-----------|-------|------|
| `server/` | Node 20+, Fastify, SQLite (better-sqlite3), Zod | API REST, ingestion, dédup, claim/lease, Gmail OAuth, Monday API |
| `web/` | React 19, Vite, React Router | Interface de validation (5 vues) |
| `extension/` | Chrome MV3, vanilla JS | Extraction multi-stratégie depuis une page Monday |

**Base de données** : SQLite locale (`data/ticket-gate.db`). Pas de dépendance externe pour la V1.

**Sécurité** :
- Interface web : session cookie (login admin)
- maj-wp : Bearer token (`MAJ_WP_API_KEY`)
- Extension Chrome : appairage par code temporaire (10 min), token stocké dans `chrome.storage.local` — **aucun secret serveur embarqué**
- Secrets dans `.env` (hors Git)

## Modèle de données — `IntakeItem`

| Champ | Description |
|-------|-------------|
| `id` | ID TicketGate (nanoid) |
| `source_type` | `gmail` \| `monday_api` \| `monday_chrome` \| `demo` |
| `source_account` | Compte Gmail, board Monday, etc. |
| `source_external_id` | ID message Gmail, item Monday… |
| `source_url` | Lien source |
| `sender_id` / `sender_name` / `sender_email` | Auteur |
| `client_project` | Client/projet si identifiable |
| `title` / `body` | Contenu normalisé |
| `metadata` | JSON libre (thread_id, board_id…) |
| `attachments` | Métadonnées PJ |
| `parse_warnings` | Avertissements d'extraction |
| `raw_payload` | Payload source brut (audit) |
| `received_at` / `ingested_at` | Dates |
| `decision` | `pending` \| `approved` \| `ignored` \| `auto_ignored` |
| `status` | Sync maj-wp : `none` \| `approved` \| `claimed` \| `created` \| `in_progress` \| `completed` \| `failed` |
| `maj_wp_ticket_id` | ID ticket maj-wp |
| `remote_status` | État brut maj-wp (non normalisé) |
| `remote_message` / `error` | Messages |
| `last_sync_at` | Dernière sync |
| `claim_token` / `claim_expires_at` / `claimed_by` | Lease de claim |
| `idempotency_key` | Clé SHA256 pour dédup |

**`IgnoredSender`** : blacklist email ou identité Monday (`sender_type`, `sender_key`, `active`).

## Machine d'états

### Décision humaine (`decision`)
```
pending ──approve──► approved
pending ──ignore──► ignored
pending ──blacklist──► auto_ignored (nouvelles entrées du même expéditeur)
ignored ──restore──► pending
auto_ignored ──restore──► pending
```

### Sync maj-wp (`status`)
```
approved (decision=approved, status=none|approved)
  ──claim──► claimed (+ claim_token, lease 300s)
  ──created──► created (+ maj_wp_ticket_id)
  ──status──► in_progress | completed | failed
```

Le `remote_status` conserve toujours la valeur brute renvoyée par maj-wp.

## Endpoints API

### Interface admin (session cookie)

| Méthode | Route | Action |
|---------|-------|--------|
| POST | `/api/auth/login` | Connexion |
| GET | `/api/intake/pending` | À valider |
| GET | `/api/intake/tickets` | Tickets validés |
| GET | `/api/intake/ignored` | Ignorés |
| POST | `/api/intake/:id/approve` | Valider |
| POST | `/api/intake/:id/ignore` | Ignorer |
| POST | `/api/intake/:id/restore` | Restaurer |
| POST | `/api/intake/:id/blacklist-sender` | Blacklist expéditeur |
| GET/POST | `/api/blacklist` | Gestion blacklist |
| GET | `/api/sources/status` | État des sources |
| POST | `/api/demo/seed` | Données démo (dev) |

### Extension Chrome (Bearer extension token)

| Méthode | Route | Action |
|---------|-------|--------|
| POST | `/api/extension/pair` | Échanger code d'appairage |
| POST | `/api/extension/ingest` | Envoyer item Monday |

### maj-wp v1 (Bearer `MAJ_WP_API_KEY`)

| Méthode | Route | Action |
|---------|-------|--------|
| GET | `/api/v1/tickets/ready` | Lister items prêts |
| POST | `/api/v1/intake/claim-next` | Claim atomique du prochain |
| POST | `/api/v1/intake/:id/claim` | Claim d'un item |
| POST | `/api/v1/intake/:id/created` | Accusé création (idempotent) |
| POST | `/api/v1/intake/:id/status` | Mise à jour état / erreur |
| GET | `/api/v1/intake/:id` | Détail |

Spécification OpenAPI : [`openapi.yaml`](openapi.yaml)

---

## Contrat d'intégration maj-wp

> Document à transmettre à l'agent maj-wp.

### Authentification

```
Authorization: Bearer <MAJ_WP_API_KEY>
```

La clé est configurée dans `.env` côté TicketGate. maj-wp doit la stocker de façon sécurisée.

### Workflow recommandé

1. **Poll ou claim** : `POST /api/v1/intake/claim-next` avec `{ "consumer_id": "maj-wp" }`
2. Si 404 → rien à traiter
3. Si 200 → recevoir `item` + `claim_token` (lease 300 s)
4. Créer le ticket dans maj-wp
5. **Accuser** : `POST /api/v1/intake/{id}/created`
   ```json
   {
     "claim_token": "<token>",
     "maj_wp_ticket_id": "12345",
     "remote_status": "open",
     "remote_message": "Ticket créé avec succès"
   }
   ```
6. **Sync état** (optionnel, périodique) : `POST /api/v1/intake/{id}/status`
   ```json
   {
     "status": "in_progress",
     "remote_status": "en_cours",
     "remote_message": "Pris en charge par Jean"
   }
   ```
7. **Erreur** :
   ```json
   {
     "status": "failed",
     "remote_status": "error",
     "error": "Description de l'erreur"
   }
   ```

### Idempotence

- Un second `POST .../created` avec le même `maj_wp_ticket_id` → 200, pas d'incohérence
- Un second `claim` sur un item déjà claimé (lease active) → 409 `already_claimed`
- Lease expirée → re-claim possible

### Codes HTTP

| Code | Signification |
|------|---------------|
| 200 | Succès |
| 201 | Création (extension) |
| 400 | Payload invalide |
| 401 | Auth manquante/invalide |
| 404 | Item introuvable / rien à claim |
| 409 | Conflit (claim, approve…) |

### Champs item exposés à maj-wp

`id`, `title`, `body`, `source_type`, `source_url`, `sender_name`, `sender_email`, `client_project`, `metadata`, `attachments`, `received_at`

---

## Gmail

- OAuth 2.0 (googleapis) — **jamais de mot de passe**
- Configurer `GMAIL_CLIENT_ID`, `GMAIL_CLIENT_SECRET` dans `.env`
- Connecter via **Sources → Connecter Gmail**
- Tokens stockés en SQLite (`gmail_tokens`)
- Poll automatique (5 min par défaut) avec requête configurable (`GMAIL_SEARCH_QUERY`)
- Dédup via `source_external_id` = Gmail message ID

## Monday API

- Optionnel : `MONDAY_API_TOKEN` + `MONDAY_BOARD_IDS`
- Poll GraphQL périodique
- Dédup partagée avec l'extension via `monday:{source_external_id}`

## Extension Chrome

1. **Sources** → Générer un code d'appairage
2. Charger l'extension en mode développeur (voir ci-dessous)
3. Entrer l'URL API + code → Appairer
4. Sur une page Monday item → **Envoyer vers TicketGate**

Extraction multi-stratégie :
- URL (`/boards/{id}/pulses/{id}`)
- DOM (sélecteurs multiples pour titre, description, auteur)
- Meta OG / title
- JSON embarqué dans la page si présent

Réponses : `created`, `duplicate`, ou erreur explicite.

---

## Lancement

### Prérequis

- Node.js ≥ 20
- npm

### Installation

```bash
cd /var/www/html/ticket-gate
cp .env.example .env   # si besoin
npm install
npm run build          # build web + server
```

### Développement

```bash
# Terminal 1 — API + UI buildée
npm run dev

# Terminal 2 — UI hot reload (optionnel)
cd web && npm run dev
```

Accès : **http://localhost:3847** — login `admin` / `admin` (voir `.env`)

### Production

```bash
npm run build
npm start
```

### Données de démo

```bash
npm run demo:seed
# ou via l'interface : Sources → Charger les données de démo
```

---

## Extension Chrome — mode développeur

1. Ouvrir `chrome://extensions/`
2. Activer **Mode développeur**
3. **Charger l'extension non empaquetée**
4. Sélectionner le dossier `/var/www/html/ticket-gate/extension`
5. Appairer depuis le popup (code depuis TicketGate → Sources)

---

## Tests manuels

1. **Démo** : Sources → Charger démo → voir 3 items dans « À valider »
2. **Valider** : cliquer « Valider pour création de ticket » → item dans « Tickets »
3. **Ignorer** : ignorer un item → visible dans « Ignorés » → restaurer
4. **Blacklist** : depuis Ignorés → « Ignorer cet expéditeur » → nouvel email auto-ignoré
5. **maj-wp simulé** :
   ```bash
   # Lister prêts
   curl -H "Authorization: Bearer dev-maj-wp-api-key-change-in-prod" \
     http://localhost:3847/api/v1/tickets/ready

   # Claim
   curl -X POST -H "Authorization: Bearer dev-maj-wp-api-key-change-in-prod" \
     -H "Content-Type: application/json" \
     -d '{"consumer_id":"test"}' \
     http://localhost:3847/api/v1/intake/claim-next
   ```
6. **Extension** : appairer + envoyer depuis une page Monday

---

## Tests automatisés

```bash
npm test
```

Couvre : ingestion, dédup, validation, ignore, blacklist, restore, claim concurrent, acknowledgment, idempotence, extension auth, validation payload, flow HTTP.

---

## Déploiement production OVH

### Hébergement détecté (audit 2026-08-28)

| Élément | Constat |
|---------|---------|
| **Type** | **C — VPS / serveur dédié** (Ubuntu, Apache, systemd) |
| **Node** | v22.23.2 (`/home/nicolas/.local/bin/node`) |
| **SSH** | Oui (accès root/nicolas) |
| **Supervision** | systemd (`ticket-gate.service`) |
| **Reverse proxy** | Apache → `127.0.0.1:3847` |
| **Document root projet** | `/var/www/html/ticket-gate` |
| **IP publique VPS** | `128.78.93.76` |
| **DNS actuel** | `ticket-gate.sitecreateur.fr` → `46.105.57.169` (OVH LB, **IP différente**) |
| **État domaine public** | HTTP 403 Apache OVH — **NOT VERIFIED LIVE** sur ce VPS |

> **Action DNS requise** : pointer `ticket-gate.sitecreateur.fr` vers `128.78.93.76` (ou configurer le proxy OVH vers ce serveur).

> **INSTANCE_COUNT = 1** obligatoire (SQLite + pollers Gmail/Monday).

### Architecture production

```
Internet → HTTPS (Apache/OVH) → 127.0.0.1:3847 (Fastify)
                                    ├── /api/*  API
                                    └── /*      web/dist (React SPA)
```

### Build

```bash
cd /var/www/html/ticket-gate
npm install
npm run build
```

### Secrets production

```bash
# Générer hash admin
npm run hash-password -- "votre-mot-de-passe-fort"

# Copier et éditer
cp deploy/ticket-gate.env.example deploy/ticket-gate.env
chmod 600 deploy/ticket-gate.env
# Remplir SESSION_SECRET, MAJ_WP_API_KEY, EXTENSION_SECRET, ADMIN_PASSWORD_HASH
```

### systemd (supervision permanente)

```bash
sudo cp deploy/ticket-gate.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable ticket-gate
sudo systemctl start ticket-gate
sudo systemctl status ticket-gate
# Logs: journalctl -u ticket-gate -f
```

Le service redémarre automatiquement (`Restart=always`). Pas de `npm start` manuel.

### Apache reverse proxy

```bash
sudo cp deploy/ticket-gate.apache.conf /etc/apache2/sites-available/ticket-gate.conf
sudo a2enmod proxy proxy_http headers rewrite ssl
sudo a2ensite ticket-gate.conf
sudo systemctl reload apache2
```

SSL : certificat Let's Encrypt (`certbot --apache -d ticket-gate.sitecreateur.fr`) ou terminaison TLS OVH en amont.

### SQLite persistant

- Chemin : `/var/www/html/ticket-gate/data/ticket-gate.sqlite`
- Variable : `DATA_DIR=/var/www/html/ticket-gate/data`
- Hors `web/dist`, survit aux redeploys

### Gmail OAuth callback (Google Cloud Console)

Enregistrer exactement :

```
https://ticket-gate.sitecreateur.fr/api/gmail/callback
```

### Extension Chrome production

URL par défaut : `https://ticket-gate.sitecreateur.fr`  
Appairage via code temporaire (Sources → Générer un code).

### Healthcheck

```bash
curl -s https://ticket-gate.sitecreateur.fr/api/health
# {"status":"ok","database":true,...}
```

### Rollback

```bash
sudo systemctl stop ticket-gate
# Restaurer version précédente du code
npm run build
sudo systemctl start ticket-gate
```

### Fichiers à déployer

- `server/dist/`, `web/dist/`, `package.json`, `package-lock.json`
- `node_modules/` (ou `npm ci --omit=dev` sur le serveur)
- `deploy/ticket-gate.env` (hors Git)
- `data/` (base SQLite — ne pas écraser)

Ne pas déployer : `.env` dev, tests, caches, données demo.

---

## Limites V1

- Un seul compte admin (pas de multi-utilisateur)
- SQLite (pas de cluster horizontal)
- Gmail : pas de label automatique « Processed » (V1 simplifiée)
- Monday API : poll basique sans webhook
- Extension : sélecteurs DOM susceptibles d'évoluer avec l'UI Monday
- Pas de pièces jointes binaires (métadonnées uniquement)
- Pas de HTTPS imposé en dev

---

## Fichiers principaux

```
ticket-gate/
├── package.json
├── .env.example
├── openapi.yaml
├── README.md
├── server/src/
│   ├── index.ts, app.ts, config.ts, types.ts, schemas.ts
│   ├── db/index.ts
│   ├── services/intake.service.ts, gmail.service.ts, monday.service.ts
│   ├── routes/admin.ts, maj-wp.ts, extension.ts, auth.ts, sources.ts
│   └── middleware/auth.ts
├── server/tests/
├── web/src/
│   ├── App.tsx, api.ts
│   └── pages/ (Pending, Tickets, Ignored, Blacklist, Sources)
└── extension/
    ├── manifest.json, content.js, popup.js
```
