---
title: "Jobs & files"
description: "Récupère les fichiers media publiés dont la row DB a disparu (média hard-deleté hors-bande, upload avorté ayant écrit le WebP avant la row, purge SQL manue…"
---

## `POST /admin/jobs/cleanup-orphan-files`

Récupère les fichiers media publiés dont la row DB a disparu (média hard-deleté hors-bande, upload avorté ayant écrit le WebP avant la row, purge SQL manuelle…). Scanne le root publié (`MEDIA_STORAGE_PATH`), ne retient que les WebP **primaires** `<hex>.webp`, batch les ids et interroge la DB : un fichier sans row est un orphelin. Le supprimer retire aussi son compagnon `-blurhash.webp`.

**Sécurité — dry-run par défaut.** Rien n'est supprimé sans `?delete=1`. Un dry-run rapporte exactement ce qu'une vraie passe supprimerait.

Le scan est **plafonné** (`?limit`, défaut 1000, max 50000 fichiers examinés par appel) pour rester borné sur un arbre à plusieurs millions de fichiers. Quand `capped = true`, la borne a été atteinte — relancer pour continuer (en mode `delete`, l'ensemble des orphelins rétrécit au fil des suppressions).

Les **originaux** (root séparé, extension source) ne sont pas touchés : leur extension n'est pas déductible de l'id seul, et ce sont des archives froides — hors périmètre du nettoyage du root publié.

**Query params**
- `limit` (int, optionnel) : 1..50000 fichiers à examiner. Défaut `1000`.
- `delete` (bool, optionnel) : `1` / `true` pour réellement supprimer. Défaut off (dry-run).

**Réponse (200)**

```json
{
  "status":       "ok",
  "dryRun":       true,
  "scanned":      1000,
  "orphansFound": 7,
  "deleted":      0,
  "capped":       true,
  "sample":       ["01a3471992e44c60a8f08321f713635a", "..."],
  "durationMs":   142
}
```

| Champ | Sens |
|---|---|
| `dryRun` | `true` tant que `?delete=1` n'est pas passé |
| `scanned` | nombre de WebP primaires examinés |
| `orphansFound` | fichiers sans row DB |
| `deleted` | fichiers réellement supprimés (`0` en dry-run) |
| `capped` | `true` si la borne `limit` a été atteinte (il peut rester des orphelins) |
| `sample` | aperçu des ids orphelins (max 100) |

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `400` | `{ "error": "Invalid limit." }` | `limit` non entier ou hors 1..50000 |
| `403` | `{ "error": "..." }` | auth KO |

**Exemple curl**

```bash
# Dry-run (audit)
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/jobs/cleanup-orphan-files?limit=5000"

# Suppression effective
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/jobs/cleanup-orphan-files?limit=5000&delete=1"
```

---


## `POST /admin/jobs/flush/{job}`

Force un **drain immédiat** d'un tampon habituellement vidé par cron, sans accès shell (Postman / Talend). L'endpoint n'est qu'un déclencheur HTTP : il appelle exactement le même `FlushService::flush()` que les entry-points `bin/*-flush.php` (aucune logique dupliquée).

**`{job}`** (la route restreint déjà aux valeurs valides) :

| `job` | Service(s) | Équivaut au cron |
|---|---|---|
| `counters` | media **+** user (les deux en un appel) | `bin/media-counters-flush.php` + `bin/user-counters-flush.php` |
| `notifications` | digest OneSignal | `bin/notifications-flush.php` |
| `tracking` | tampon de clics d'affiliation | `bin/tracking-flush.php` |

Idempotent par nature : un flush sur un tampon vide renvoie des compteurs à zéro. Un échec de transaction laisse remonter l'exception → `500` JSON plat (l'opérateur relance), comme un cron qui sortirait en code 2.

**Réponse (200)**

```json
{
  "job": "counters",
  "summary": {
    "media": { "drained": 0, "bumped": 0, "deletedEvents": 0, "gcRows": 0 },
    "user":  { "drained": 0, "bumped": 0, "deletedEvents": 0, "gcRows": 0 }
  }
}
```

```json
{ "job": "notifications", "summary": { "recipients": 3, "pushed": 3, "skipped": 0, "failed": 0 } }
```

```json
{ "job": "tracking", "summary": { "drained": 42, "bumped": 12, "deletedEvents": 42 } }
```

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `400` | `{ "error": "Unknown job '…'. Expected: counters, notifications, tracking." }` | garde défensive (la regex de route 404 avant, en principe) |
| `403` | `{ "error": "..." }` | auth KO |
| `500` | `{ "error": "..." }` | échec de flush (transaction) — relancer |

**Exemples curl**

```bash
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/jobs/flush/counters"

curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/jobs/flush/tracking"
```

---


## `GET /admin/jobs/describe-queue`

**Observabilité de la file IA** `work.media_to_describe` (FIFO des médias en attente de description, consommée par un worker hors-bande). Jusqu'ici l'admin était aveugle sur ce backlog. Lecture seule, un seul round-trip vers la base `work`.

**Réponse (200)**

```json
{
  "size":             42,
  "oldestEnqueuedAt": "2026-06-19T08:12:00+00:00",
  "oldestAgeSeconds": 1834,
  "enqueuedLastHour": 7,
  "enqueuedLast24h":  120
}
```

| Champ | Sens |
|---|---|
| `size` | profondeur de la file (médias en attente) |
| `oldestEnqueuedAt` | timestamp de la tête de file (`null` si vide) |
| `oldestAgeSeconds` | âge de la tête en secondes — grandit ⇒ le worker décroche |
| `enqueuedLastHour` / `enqueuedLast24h` | taux d'arrivée récent, à comparer au débit du worker |

```bash
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  "http://hydrogen.dev.com/admin/jobs/describe-queue"
```

---


## `POST /admin/jobs/describe-queue/requeue`

**Réinjecte un média bloqué** dans la file IA (worker mort en plein `processing`, ligne de queue perdue, retry après `failed`). `INSERT IGNORE` idempotent + remise de `media.status` à `pending` (sauf s'il l'est déjà) pour que le cycle de vie reste cohérent et qu'un futur `claim` soit légal.

Refuse un média en état **terminal** (`published` / `rejected`) : il a déjà un verdict, le redécrire serait une régression (`409`).

**Body**

| Champ | Type | Requis | Sens |
|---|---|---|---|
| `mediaId` | string (32 hex) | oui | id du média à réinjecter |

**Réponse (200)**

```json
{
  "status":         "ok",
  "mediaId":        "d26d1600cde54bd095e09f8b68ace05f",
  "previousStatus": "failed",
  "mediaStatus":    "pending",
  "alreadyQueued":  false
}
```

| Champ | Sens |
|---|---|
| `previousStatus` | état du média avant requeue |
| `mediaStatus` | toujours `pending` après requeue |
| `alreadyQueued` | `true` si la ligne était déjà dans la file (réinjection no-op) |

**Erreurs**

| Status | Body | Sens |
|---|---|---|
| `400` | `{ "error": "Body must be JSON object with 'mediaId' 32-hex string." }` | corps absent / `mediaId` manquant ou mal formé |
| `404` | `{ "error": "Media not found." }` | id inconnu |
| `409` | `{ "error": "Cannot requeue a media in terminal state '<state>'." }` | média `published` / `rejected` |
| `403` | `{ "error": "..." }` | auth KO |

```bash
curl -s -X POST -H "Authorization: Bearer $ADMIN_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "mediaId": "d26d1600cde54bd095e09f8b68ace05f" }' \
  "http://hydrogen.dev.com/admin/jobs/describe-queue/requeue"
```

---
