Le tableau de bord /admin/monitoring fonctionnait en polling côté client (15 s) déclenchant une sonde active côté serveur à chaque appel — deux onglets admin ouverts = deux séries de sondes redondantes. Inverse le mécanisme : le serveur sonde une fois par minute et pousse le résultat aux clients connectés via WebSocket (socket.io, room "monitoring"). Décisions actées avec l'utilisateur : l'endpoint REST disparaît entièrement (plus de repli, 100% WebSocket) ; la sonde tourne en continu dès le démarrage, sans lien avec la présence d'un admin connecté (le plus simple des deux, coût des sondes négligeable). Backend : - ExpressServer (packages/express-tools) expose désormais son http.Server sous-jacent (créé explicitement, plus dans .listen()) — socket.io s'attache au serveur HTTP lui-même, pas à Express/un port. - MonitoringBroadcastService (nouveau) : sonde immédiate au démarrage puis toutes les 60 s (réutilise getMonitoring() tel quel, aucune duplication), diffuse à la room "monitoring", rejoue le dernier résultat en cache à tout nouvel admin qui rejoint (pas d'attente jusqu'à une minute pour un onglet fraîchement ouvert). - requireAdminSocket (nouveau) : middleware de handshake socket.io, équivalent WebSocket de requireAdmin — même vérification (cookie + JWT + tokenVersion), factorisée dans resolveAdminFromToken (require-admin.ts) puisque les deux ne diffèrent que par où ils lisent le cookie brut (un handshake socket.io ne passe pas par cookie-parser). - GET /admin/api/monitoring supprimée (route + service de test associés). - server.ts (pas app.ts) instancie socket.io — les tests supertest n'ont pas besoin d'un serveur WebSocket. Frontend : - MonitoringSocketClient (nouveau, api/monitoring-socket.ts) : wrapper socket.io-client, interface MonitoringSocketClientLike volontairement petite et injectable dans MonitoringPage (prop optionnelle) pour rester testable sans serveur réel. - MonitoringPage : data et connectionState séparés — une coupure de connexion après un premier chargement affiche un badge "reconnexion en cours" sans jamais effacer le dernier board connu (repose sur la reconnexion automatique de socket.io-client). Seul le tout premier chargement affiche l'état plein-écran chargement/erreur. - adminApiClient.getMonitoring() supprimée (plus utilisée). Tests : - Nouveau apps/api/test/admin-monitoring-socket.test.ts : vraie connexion socket.io-client contre un serveur réel démarré dans le test (port 0), authentification avec un admin seedé + cookie signé, vérifie la réception de monitoring:update et le rejet sans cookie admin valide. Les tests heartbeat de l'ancien admin-monitoring.test.ts (route /internal/tech-steps/heartbeat, sans rapport avec ce changement) sont déplacés vers tech-step-worker.routes.test.ts, leur router d'origine. - Nouveau cypress/component/MonitoringPage.cy.tsx : monte la page avec un client factice, vérifie rendu des cartes, badge de reconnexion sans perte du board, états chargement/erreur — pas de test e2e Gherkin contre un vrai WebSocket (aucun mécanisme pour ça dans ce repo, fragile ; le test Mocha ci-dessus est la preuve bout-en-bout serveur). - apps/web/cypress/e2e/admin-monitoring.cy.ts (REST) supprimé, remplacé par le test composant ci-dessus. Vérification : pnpm lint clean, build complet du monorepo clean, suite Cypress e2e complète 27/27 fichiers (114/114 tests), suite composant complète 5/5 fichiers (54/54 tests, dont les 4 nouveaux). Suite Mocha apps/api non exécutable cette session (Postgres/Docker injoignable, comme pour #33) — le nouveau test socket.io n'a donc pas pu être run-vérifié, seulement relu attentivement et vérifié par le lint. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
122 lines
2.8 KiB
SCSS
122 lines
2.8 KiB
SCSS
// =============================================================================
|
|
// MonitoringPage — a grid of service health cards, one per probed target.
|
|
// Status drives a coloured left border + dot.
|
|
// =============================================================================
|
|
|
|
.monitoring-checked {
|
|
margin: 0 0 var(--space-md);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
// Shown above the board only while the WebSocket connection isn't
|
|
// currently up — the board itself always stays on screen underneath
|
|
// (see MonitoringPage.tsx's own comment on why `data` and
|
|
// `connectionState` are kept separate), this is just a heads-up that
|
|
// what's showing may be stale.
|
|
.monitoring-connection-badge {
|
|
display: inline-block;
|
|
margin: 0 0 var(--space-md);
|
|
padding: var(--space-xs) var(--space-sm);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 700;
|
|
|
|
&--reconnecting {
|
|
color: var(--color-warning);
|
|
background: color-mix(in srgb, var(--color-warning) 12%, transparent);
|
|
}
|
|
|
|
&--error {
|
|
color: var(--color-error);
|
|
background: color-mix(in srgb, var(--color-error) 12%, transparent);
|
|
}
|
|
}
|
|
|
|
.monitoring-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
|
gap: var(--space-md);
|
|
}
|
|
|
|
.monitoring-card {
|
|
padding: var(--space-md);
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-border);
|
|
border-left: 4px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
|
|
--status-color: var(--color-text-muted);
|
|
&--up {
|
|
--status-color: var(--color-success);
|
|
}
|
|
&--degraded {
|
|
--status-color: var(--color-warning);
|
|
}
|
|
&--down {
|
|
--status-color: var(--color-error);
|
|
}
|
|
&--unknown {
|
|
--status-color: var(--color-text-muted);
|
|
}
|
|
|
|
border-left-color: var(--status-color);
|
|
|
|
&__head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-sm);
|
|
margin-bottom: var(--space-sm);
|
|
|
|
h2 {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font-size: var(--font-size-md);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
&__dot {
|
|
flex-shrink: 0;
|
|
width: 0.6rem;
|
|
height: 0.6rem;
|
|
border-radius: 50%;
|
|
background: var(--status-color);
|
|
}
|
|
|
|
&__status {
|
|
flex-shrink: 0;
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--status-color);
|
|
}
|
|
|
|
&__meta {
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-xs);
|
|
|
|
div {
|
|
display: flex;
|
|
gap: var(--space-sm);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
dt {
|
|
flex-shrink: 0;
|
|
color: var(--color-text-muted);
|
|
min-width: 5rem;
|
|
}
|
|
|
|
dd {
|
|
margin: 0;
|
|
color: var(--color-text);
|
|
word-break: break-word;
|
|
}
|
|
}
|
|
}
|