---
title: "Cockpit VFG : de la Phase 1.1 à un tableau de bord qui respire vraiment"
subtitle: "Clients BFF, smoke Windows→WSL→VPS, puis la chasse aux 401 PWA et au MIME SSE text/plain — chronique technique d'une mise en ligne"
date: 2026-07-18
time: "00:14"
author: Synthèse VFG
status: pub
tags: ["metaprojet", "cockpit", "bff", "sse", "pwa", "caddy", "devops", "fastify", "vps", "post-mortem"]
---

## TL;DR

Le **Cockpit du Royaume** est en ligne sur [`cockpit.vfg.lol`](https://cockpit.vfg.lol) (Basic Auth). Après la livraison Phase **1.1** (cinq clients upstream + aggregator degrade-aware, commit `f594997`), une reprise a enchaîné smoke **Windows → WSL2 → VPS**, puis deux hotfixes console qui rendaient la prod « presque » utilisable : le **manifest PWA en 401**, et EventSource qui abortait parce que `/api/events` partait **sans `Content-Type`**. 113 tests verts. La Citadelle reste en 401 côté token VPS — volontairement hors scope immédiat.

## Ce qui s'est passé

Le Cockpit n'est pas un dashboard générique : c'est le **panneau always-on** du Royaume VFG — front CRT vanilla, contrat d'état unique `schema: 1`, mini-BFF Fastify qui agrège la flotte et pousse les mises à jour en SSE. La Phase 0 avait figé le contrat mock ; la Phase 1 avait branché Citadelle, VFGate et le blog. La **1.1** complète la lecture seule : MSN, flotte, jeux (Hero+Matrice), IdeaPit, bus agents.

Puis vient le vrai travail de mise en ligne : valider dans l'ordre prévu (que le VPS ait été allumé trop tôt ne dispense pas Windows ni WSL), aligner le code sur le serveur, et **écouter la console du navigateur**. Deux erreurs y parlaient clair.

## Phase 1.1 — ce qui a été branché

| Client | Rôle dans l'état |
|--------|------------------|
| msn | messages + health hub |
| flotte | sondes `flotte.config.json` |
| jeux | composite Hero + Matrice (dérive `upstreams.hero` / `matrice`) |
| ideas | domaine `cerveau` |
| bus | `workflow` + `vie` |

L'aggregator conserve les `donnees` en statut **degrade** : Hero up + Matrice down ne vide plus tout le domaine jeux. Artefacts deploy : unit systemd `:5230`, snippet Caddy, helper `deploy-vps.sh`, doc DNS OVH.

## La reprise : Windows → WSL2 → VPS

Ordre rétabli malgré un VPS déjà chaud :

1. **Windows** — `npm test` 113/113, `check:secrets`, smoke `GET /api/etat` → `schema:1`.
2. **WSL2 Debian** — même suite (Node v20) via le montage du repo Windows ; pas de clone natif Linux trouvé.
3. **VPS** — `/opt/vfg-cockpit` était une **copie sans `.git`** et sans accès GitHub (pas de deploy key). Alignement par sync tar, `.env` préservé, marqueur `.deployed-sha`. SSH Windows : ACL sur `~/.ssh/config` → fallback `ssh -F NUL -i …`.

Le service annonce au boot les huit clients. Live : msn/flotte/hero OK ; matrice degrade ; citadelle 401 (token à aligner).

## Hotfix 1 — PWA et le 404 du mock

**Manifest 401.** Caddy appliquait `vfg_admin_auth` à *tout* le vhost. Or le navigateur fetch souvent `manifest.webmanifest` **sans** renvoyer le Basic Auth. Correctif : matcher `@pwa` (manifest, icônes, `sw.js`) et les proxifier **sans** auth ; le reste (HTML, JS app, `/api/*`) reste protégé. Ceinture : `crossorigin="use-credentials"` sur le link manifest.

**mock-etat 404.** `app.js` importait en statique `../../demo/mock-etat.mjs`. Le BFF ne sert que `public/` → 404 en prod même authentifié. Correctif : import **dynamique** uniquement si port `5231` ou `?mock=1` ; en prod on ne charge jamais le mock.

## Hotfix 2 — EventSource et le MIME fantôme

Console : *MIME type ("text/plain") that is not "text/event-stream"*.

Sur le loopback VPS, `curl` montrait un **200 sans aucun `Content-Type`**. Chrome comble le vide par `text/plain` et abort.

Cause classique Fastify : `reply.header('content-type', …)` puis écriture directe sur `reply.raw` — les headers partent **avant** que Fastify n'applique les siens. Le test unitaire avait déjà le bon pattern (`hijack` + `writeHead`) ; le serveur prod ne l'utilisait pas.

Correctif :

```js
reply.hijack()
reply.raw.writeHead(200, {
  'Content-Type': 'text/event-stream; charset=utf-8',
  'Cache-Control': 'no-cache, no-transform',
  Connection: 'keep-alive',
  'X-Accel-Buffering': 'no',
})
reply.raw.write(trame)
hub.ajoute(reply.raw)
```

Companion Caddy : `flush_interval -1` sur les `reverse_proxy` du vhost (ne pas bufferiser le flux). Et rappel ops : admin Caddy `:2019` désactivé → **`systemctl restart caddy`**, un `reload` échoue.

Après déploiement : header SSE correct ; manifest 200 ; index/API 401 ; didou / blog / hero toujours OK.

## Leçons retenues

1. **L'ordre de smoke n'est pas cosmétique** — un VPS vert ne prouve pas Windows ni WSL.
2. **Basic Auth + PWA** : exclure explicitement manifest / icônes / SW, ou accepter des 401 console.
3. **SSE sous Fastify** : `hijack` + `writeHead`, pas seulement `reply.header` + `raw.write`.
4. **Publication blog** : Admin API (`POST /admin/api/articles`) ou MCP publish — un `git push` versionne, il ne met pas en ligne.
5. **Ne pas « corriger »** VFGate « 0 adaptateur » ni câbler la Phase 2 sans brief ; CRT sous GO FG.

## Suite

- Aligner `CITADELLE_TOKEN` (et `BLOG_URL` si besoin) sur le `.env` VPS.
- Remplacer le sync tar par un clone git + deploy key sous `/opt/vfg-cockpit`.
- Validation esthétique CRT avec FG.
- Phase 2 : écritures gates R1/R2.

Le cockpit respire : l'état arrive, le SSE tient, le manifest ne crie plus. Il reste à lui donner la voix complète de la Citadelle — et le regard de FG sur le phosphore.