Commit 19b939a7 authored by Kourser's avatar Kourser
Browse files

Visio : dire qu'elle n'est pas installée, au lieu d'accuser le réseau

Signalé à l'usage : « Rejoindre l'appel » finissait sur un conseil de
diagnostic réseau et de partage de connexion mobile. Or il n'y avait aucune
panne — cette instance tourne sous le profil minimal, sans serveur média, et le
produit le savait depuis son démarrage.

  Envoyer quelqu'un chercher une panne de réseau qui n'existe pas est pire que
  ne rien dire. Sur un outil de crise, c'est même exactement le contraire de ce
  qu'on lui demande : lever le doute sur ce qui marche encore.

Le bouton refuse avant le clic
  Sans serveur média il porte « Visio non configurée », désactivé, avec le
  motif en infobulle. On ne propose pas une porte qui ne s'ouvre pas.

Le panneau distingue les deux causes
  Serveur média joignable mais connexion impossible : le diagnostic réseau
  garde tout son sens. Pas de serveur média du tout : aucun diagnostic n'y
  changera rien, et le message dit ce qu'il faut faire — profil « complet », ou
  LIVEKIT_URL, LIVEKIT_CLE, LIVEKIT_SECRET. Il rappelle aussi que la cellule
  tient par le salon et la main courante, et que le téléphone est le repli que
  le dossier de préparation prévoit.

Un endroit unique pour les briques absentes
  « useCapacites » lit la sonde d'état publique, déjà servie, et dit ce dont
  l'instance dispose : visio, stockage objet, analyse antivirale. Les autres
  fonctions optionnelles pourront s'y raccorder plutôt que de découvrir leur
  absence au moment de l'échec.

Le chat était introuvable
  Demandé au cahier des charges, écrit et fonctionnel, mais rangé dans le rail
  sous « Cellules » avec pour seuls intitulés les noms des cellules. Rien ne
  disait qu'on y discutait. Le groupe s'appelle désormais « Cockpit et salons ».
  Le modèle ne change pas — on rejoint le salon de sa cellule, pas un « chat »
  général — mais on le trouve.

Signed-off-by: default avatarKourser <contact@kourser.bzh>
Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent bf592864
Loading
Loading
Loading
Loading
+36 −0
Original line number Diff line number Diff line
import { useQuery } from "@tanstack/react-query";
import { api } from "./api.js";

/**
 * Ce dont l'instance dispose réellement.
 *
 * Une instance minimale n'a ni serveur média, ni analyse antivirale. L'ignorer
 * conduit à proposer un bouton qui échoue, puis à expliquer l'échec par le
 * réseau — alors que le produit sait, depuis son démarrage, qu'aucun serveur
 * média n'a été configuré. Mieux vaut le dire avant le clic.
 *
 * La sonde d'état est publique, sans session, et déjà servie : elle est la
 * source la moins coûteuse de cette information.
 */
export interface Capacites {
  visio: boolean;
  stockage: boolean;
  antivirus: boolean;
}

export function useCapacites(): Capacites {
  const { data } = useQuery({
    queryKey: ["capacites"],
    queryFn: () => api.lire<{
      composants: {
        visioconference: boolean; stockage_objet: boolean; analyse_antivirale: boolean;
      };
    }>("/api/etat"),
    staleTime: 600_000,
  });
  return {
    visio: data?.composants.visioconference ?? false,
    stockage: data?.composants.stockage_objet ?? false,
    antivirus: data?.composants.analyse_antivirale ?? false,
  };
}
+12 −3
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import { Preparation } from "../ecrans/Preparation.js";
import { Tableaux } from "../ecrans/Tableaux.js";
import { Mobilisation } from "../ecrans/Mobilisation.js";
import { Cloture } from "../ecrans/Cloture.js";
import { useCapacites } from "../capacites.js";

interface SalleListee { id: string; libelle: string; cellule_id: string | null; genre: string }
interface SalonListe {
@@ -38,6 +39,7 @@ export function Coque({ criseId }: { criseId: string }) {
  const { segments, aller } = useRoute();
  const { rembobinage, rembobiner } = useEtat();
  const client = useQueryClient();
  const capacites = useCapacites();
  const [consigne, definirConsigne] = useState<Prefill | null>(null);
  const [recherche, definirRecherche] = useState(false);
  const [appel, definirAppel] = useState<Appel | null>(null);
@@ -157,10 +159,17 @@ export function Coque({ criseId }: { criseId: string }) {
            ↺ Rembobiner
          </button>
          {rembobinage === null && appel === null && salleDeLaCellule && (
            <button onClick={() => definirAppel({
            // Sans serveur média, le bouton refuse d’abord et explique. Le
            // laisser cliquable pour finir sur un diagnostic réseau ferait
            // chercher une panne là où il n’y a qu’une brique absente.
            <button
              disabled={!capacites.visio}
              title={capacites.visio ? undefined
                : "Aucun serveur média n’est configuré sur cette instance."}
              onClick={() => definirAppel({
                salleId: salleDeLaCellule.id, libelle: salleDeLaCellule.libelle,
              })}>
              Rejoindre l’appel
              {capacites.visio ? "Rejoindre l’appel" : "Visio non configurée"}
            </button>
          )}
          {rembobinage === null ? (
@@ -181,7 +190,7 @@ export function Coque({ criseId }: { criseId: string }) {
        {/* ── Rail : crise → cellule → module, jamais quatre niveaux ─────── */}
        <nav className={`rail${railOuvert ? " ouvert" : ""}`}>
          <div className="groupe">
            <span className="etiq">Cellules</span>
            <span className="etiq">Cockpit et salons</span>
            <button className={`item${module === "cockpit" ? " actif" : ""}`}
              onClick={() => aller(`/crises/${criseId}`)}>
              Cockpit
+22 −2
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import {
} from "livekit-client";
import { api, ErreurApi } from "../api.js";
import { initiales } from "../etat.js";
import { useCapacites } from "../capacites.js";

/**
 * ╔══════════════════════════════════════════════════════════════════════════╗
@@ -34,6 +35,8 @@ interface Tuile {
function PanneauVisio({
  criseId, appel, raccrocher,
}: { criseId: string; appel: Appel; raccrocher: () => void }) {
  const capacites = useCapacites();

  const [salle] = useState(() => new Room({ adaptiveStream: true, dynacast: true }));
  const [etat, definirEtat] = useState<"connexion" | "connecte" | "echec">("connexion");
  const [erreur, definirErreur] = useState<string | null>(null);
@@ -84,8 +87,11 @@ function PanneauVisio({
      } catch (e) {
        if (!vivant) return;
        definirEtat("echec");
        definirErreur(e instanceof ErreurApi ? e.message
          : "Le serveur média est injoignable depuis ce réseau.");
        definirErreur(
          e instanceof ErreurApi ? e.message
            : capacites.visio ? "Le serveur média est injoignable depuis ce réseau."
              : "Aucun serveur média n’est configuré sur cette instance.",
        );
      }
    })();

@@ -134,10 +140,24 @@ function PanneauVisio({
      {etat === "echec" ? (
        <div style={{ padding: 14 }}>
          <div className="erreur" style={{ marginBottom: 8 }}>{erreur}</div>
          {/* Deux causes très différentes, qui appelaient le même conseil :
              une brique absente ne se répare pas en changeant de réseau. */}
          {capacites.visio ? (
            <p className="aide" style={{ margin: 0 }}>
              Le diagnostic réseau dit ce qui est bloqué et à qui le demander. Si même
              le relais sur 443 échoue, basculez sur un partage de connexion mobile.
            </p>
          ) : (
            <p className="aide" style={{ margin: 0 }}>
              Cette instance n’a pas de serveur média : la visioconférence n’y est pas
              installée. Ce n’est pas une panne de votre réseau, et aucun diagnostic
              n’y changera rien. Démarrez la composition sous le profil «&nbsp;complet&nbsp;»,
              ou renseignez LIVEKIT_URL, LIVEKIT_CLE et LIVEKIT_SECRET.
              <br /><br />
              En attendant, la cellule tient par le salon et la main courante — et le
              téléphone reste le repli que le dossier de préparation prévoit.
            </p>
          )}
        </div>
      ) : (
        <>