Commit f7da4b07 authored by Kourser's avatar Kourser
Browse files

Identité d'organisation : réglages, cellule de crise, et instantané

Étape 3 du socle multi-organisations. Une organisation porte désormais son
secteur, son compte officiel sur les réseaux simulés, son habillage joueur et ses
variables de mise en situation par défaut — en plus du logo, qui existait déjà. Et
une cellule de crise pré-configurée : les membres récurrents, saisis une fois.

La décision qui compte ici : un exercice RECOPIE ces valeurs à sa création et ne
les relit jamais. En lien vivant — le comportement actuel du logo — renommer
l'organisation six mois plus tard réécrirait ce que montre le RETEX d'un exercice
clos. La restitution cesserait de correspondre à ce que les joueurs ont vu. C'est
donc un changement de comportement délibéré, à signaler aux exploitants.

Deux effets de bord heureux :

- Le contexte fusionne clef par clef, jamais en bloc. Un kit qui cite {{ville}}
  bénéficie de la valeur par défaut de l'organisation, sans écraser ce que
  l'animation a saisi. Un remplacement en bloc perdait les clefs non renseignées.
- Les fichiers de logo n'étant jamais supprimés du disque — limite que j'avais
  signalée comme un défaut — recopier le chemin ne peut pas pointer dans le vide.

La cellule de crise s'AJOUTE aux personnages du kit, elle ne s'y substitue pas.
J'ai vérifié les `ref` des kits fournis : ce sont des noms slugifiés
(`agence-regionale-de-sante-ars`), pas un vocabulaire de fonctions. Un mappage
automatique demanderait de réviser tous les kits. L'animation retire les doublons
quand le kit prévoit déjà les mêmes fonctions, et l'option se décoche pour un
exercice qui met en scène une entité fictive.

`exerciseSnapshot` est un module pur, couvert par six tests dont celui qui compte :
un champ laissé vide n'est pas un choix vide, c'est une absence de choix.

Non vérifié : aucune chaîne Node dans cet environnement, donc rien n'a été
compilé ni testé. Contrôles manuels : équilibre des délimiteurs, balises JSX
appariées (auto-fermantes comprises), classes CSS présentes, 587 clés i18n sans
doublon, `OrgMember` ajouté aux modèles cloisonnés.

Co-Authored-By: default avatarClaude (RCA) <noreply@anthropic.com>
parent 022ad1f4
Loading
Loading
Loading
Loading
+15 −0
Original line number Diff line number Diff line
@@ -90,6 +90,21 @@ de chat. Les garde-fous :
  d'un exercice doit rester **fictif** : une capture d'écran réelle peut contenir des données
  personnelles.

## Fidélité des exercices clos

Un exercice **recopie** l'identité de son organisation à sa création — nom du compte officiel,
logo, cellule de crise, variables de mise en situation — et ne la relit jamais ensuite
(`apps/api/src/exercises/organisation-snapshot.ts`, couvert par des tests).

Ce n'est pas un détail d'implémentation. En lien vivant, renommer l'organisation ou changer
son logo six mois plus tard réécrirait ce que montre le RETEX d'un exercice déjà clos : la
restitution cesserait de correspondre à ce que les joueurs ont réellement vu. L'organisation
fournit donc le **point de départ**, jamais la source permanente. Ce qui a été saisi
explicitement au parcours de création l'emporte toujours sur le défaut de l'organisation.

Corollaire : les fichiers de logo ne sont jamais supprimés du disque quand on en téléverse un
nouveau, ce qui garantit qu'un instantané ne pointe pas dans le vide.

## Administration de l'instance

Un compte `INSTANCE_ADMIN` administre le **service**, pas les exercices. Il crée, règle,
+34 −0
Original line number Diff line number Diff line
-- Identite d'organisation reprise par defaut dans les exercices, et cellule de
-- crise pre-configuree.
--
-- Ces valeurs sont RECOPIEES a la creation d'un exercice, jamais lues en continu :
-- un exercice clos doit rester fidele a ce que les joueurs ont vu.

-- AlterTable
ALTER TABLE "Tenant" ADD COLUMN "sector" TEXT;
ALTER TABLE "Tenant" ADD COLUMN "socialAccountName" TEXT;
ALTER TABLE "Tenant" ADD COLUMN "socialAccountHandle" TEXT;
ALTER TABLE "Tenant" ADD COLUMN "defaultPlayerSkin" "PlayerSkin";
ALTER TABLE "Tenant" ADD COLUMN "defaultContext" JSONB;

-- CreateTable
CREATE TABLE "OrgMember" (
    "id" TEXT NOT NULL,
    "tenantId" TEXT NOT NULL,
    "name" TEXT NOT NULL,
    "title" TEXT,
    "orgUnit" TEXT,
    "kind" "CharacterKind" NOT NULL DEFAULT 'PLAYER',
    "simEmail" TEXT,
    "avatarColor" TEXT,
    "canPostAsOrg" BOOLEAN NOT NULL DEFAULT false,
    "position" INTEGER NOT NULL DEFAULT 0,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "OrgMember_pkey" PRIMARY KEY ("id")
);

CREATE INDEX "OrgMember_tenantId_idx" ON "OrgMember"("tenantId");

ALTER TABLE "OrgMember" ADD CONSTRAINT "OrgMember_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+44 −0
Original line number Diff line number Diff line
@@ -45,7 +45,25 @@ model Tenant {
  /// Logo de l'organisation (cle de fichier televerse). Avatar par defaut du
  /// compte officiel sur les reseaux simules ; un exercice peut le remplacer.
  logoPath     String?

  // --- Identite reprise par defaut dans les exercices ---
  // Ces valeurs sont RECOPIEES a la creation d'un exercice, pas lues en continu :
  // un exercice clos doit rester fidele a ce que les joueurs ont vu, meme si
  // l'organisation change de nom ou de logo ensuite (cf. SECURITY.md).
  /// Secteur d'activite, propose par defaut aux nouveaux exercices.
  sector       String?
  /// Compte officiel sur les reseaux simules. Null = derive du nom.
  socialAccountName   String?
  socialAccountHandle String?
  /// Habillage de l'espace joueur propose par defaut.
  defaultPlayerSkin   PlayerSkin?
  /// Variables de mise en situation par defaut ({{ville}}, {{prefecture}}...).
  /// Meme forme que `Exercise.context`, dont elles constituent le point de depart.
  defaultContext      Json?

  memberships  Membership[]
  /// Cellule de crise pre-configuree, reprise a la creation d'un exercice.
  orgMembers   OrgMember[]
  exercises    Exercise[]
  characters   Character[]
  participants Participant[]
@@ -249,6 +267,32 @@ model Character {
  @@index([exerciseId])
}

/// Membre pre-configure de la cellule de crise d'une ORGANISATION.
///
/// Meme forme qu'un `Character`, sans `exerciseId` : c'est le modele reutilisable
/// dont chaque exercice recoit une COPIE a sa creation. Modifier la cellule de
/// l'organisation ne touche donc pas les exercices deja crees.
model OrgMember {
  id          String        @id @default(cuid())
  tenantId    String
  tenant      Tenant        @relation(fields: [tenantId], references: [id], onDelete: Cascade)
  name        String
  title       String?
  orgUnit     String?
  kind        CharacterKind @default(PLAYER)
  simEmail    String?
  avatarColor String?
  /// Autorise a publier sous le compte officiel de l'organisation.
  canPostAsOrg Boolean      @default(false)
  /// Ordre d'affichage, pour presenter la cellule dans l'ordre voulu.
  position    Int           @default(0)

  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([tenantId])
}

/// Un joueur reel accedant a l'exercice via un lien/jeton signe (pas de compte).
model Participant {
  id              String    @id @default(cuid())
+2 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ import { PrismaModule } from './prisma/prisma.module';
import { AuditModule } from './common/audit.module';
import { TenantStatusModule } from './common/tenant-status.module';
import { InstanceModule } from './instance/instance.module';
import { OrganisationModule } from './organisation/organisation.module';
import { HealthModule } from './health/health.module';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
@@ -42,6 +43,7 @@ function staticWebModules(): DynamicModule[] {
    AuthModule,
    UsersModule,
    InstanceModule,
    OrganisationModule,
    ExercisesModule,
    EngineModule,
    MaintenanceModule,
+9 −1
Original line number Diff line number Diff line
import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
import { IsBoolean, IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
import { PlayerSkin } from '@prisma/client';

/** Champs de personnalisation communs à la création d'exercice et à l'installation d'un kit. */
@@ -7,6 +7,14 @@ export class PersonalizationDto {
  @IsEnum(PlayerSkin)
  playerSkin?: PlayerSkin;

  /**
   * Reprendre la cellule de crise pré-configurée de l'organisation.
   * Défaut : oui. `false` pour un exercice qui met en scène une entité fictive.
   */
  @IsOptional()
  @IsBoolean()
  useOrgMembers?: boolean;

  @IsOptional()
  @IsString()
  @MaxLength(160)
Loading