Migration Angular 20 -> 21 : fin de la migration securite (0 vulnerabilite npm)

- ng update @angular/core@21 @angular/cli@21 (corrige GHSA-jrmj-c5cx-3cw6, XSS SVG)
- Migration automatique des templates vers le control flow natif (@if/@for, 138 fichiers)
- Correction des 5 `track` invalides generes par la migration (trackBy a 1 argument
  -> track $index) + suppression des fonctions trackBy mortes
- TypeScript 5.9, zone.js 0.15 ; npm audit : 0 vulnerabilite

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 14:55:36 +02:00
parent 6acad41672
commit 23878f1c63
142 changed files with 7055 additions and 6292 deletions

View File

@@ -1,206 +1,233 @@
<div class="campaign-detail" *ngIf="campaign">
<!-- ============ Header : mode lecture ============ -->
<div class="detail-header" *ngIf="!editing">
<div class="header-texts">
<h1>{{ campaign.name }}</h1>
<p class="description">{{ campaign.description }}</p>
<div class="meta">
<span class="badge">{{ campaign.playerCount || 0 }} joueurs</span>
<!-- Badge "Univers" : lien vers le Lore associé si présent -->
<a *ngIf="linkedLore"
class="badge badge-lore"
[routerLink]="['/lore', linkedLore.id]"
title="Ouvrir l'univers associé">
<lucide-icon [img]="Globe" [size]="12"></lucide-icon>
{{ linkedLore.name }}
</a>
<!-- Campagne liée à un Lore qui n'existe plus (supprimé ailleurs) -->
<span *ngIf="campaign.loreId && !linkedLore" class="badge badge-lore-missing" title="L'univers associé est introuvable">
<lucide-icon [img]="Globe" [size]="12"></lucide-icon>
Univers introuvable
</span>
@if (campaign) {
<div class="campaign-detail">
<!-- ============ Header : mode lecture ============ -->
@if (!editing) {
<div class="detail-header">
<div class="header-texts">
<h1>{{ campaign.name }}</h1>
<p class="description">{{ campaign.description }}</p>
<div class="meta">
<span class="badge">{{ campaign.playerCount || 0 }} joueurs</span>
<!-- Badge "Univers" : lien vers le Lore associé si présent -->
@if (linkedLore) {
<a
class="badge badge-lore"
[routerLink]="['/lore', linkedLore.id]"
title="Ouvrir l'univers associé">
<lucide-icon [img]="Globe" [size]="12"></lucide-icon>
{{ linkedLore.name }}
</a>
}
<!-- Campagne liée à un Lore qui n'existe plus (supprimé ailleurs) -->
@if (campaign.loreId && !linkedLore) {
<span class="badge badge-lore-missing" title="L'univers associé est introuvable">
<lucide-icon [img]="Globe" [size]="12"></lucide-icon>
Univers introuvable
</span>
}
</div>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="startEdit()">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="deleteCampaign()">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</div>
</div>
<div class="header-actions">
<button type="button" class="btn-secondary" (click)="startEdit()">
<lucide-icon [img]="Pencil" [size]="14"></lucide-icon>
Modifier
</button>
<button type="button" class="btn-danger" (click)="deleteCampaign()">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
Supprimer
</button>
</div>
</div>
<!-- ============ Header : mode édition inline ============ -->
<div class="detail-header edit-mode" *ngIf="editing">
<div class="field">
<label>Nom</label>
<input type="text" [(ngModel)]="editName" name="editName" required />
</div>
<div class="field">
<label>Description</label>
<textarea [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
</div>
<div class="field">
<label>Univers associé</label>
<select [(ngModel)]="editLoreId" name="editLoreId">
<option value="">— Aucun univers (campagne libre) —</option>
<option *ngFor="let lore of availableLores" [value]="lore.id">{{ lore.name }}</option>
</select>
</div>
<div class="field">
<label>Système de JDR</label>
<select *ngIf="!creatingGameSystem"
}
<!-- ============ Header : mode édition inline ============ -->
@if (editing) {
<div class="detail-header edit-mode">
<div class="field">
<label>Nom</label>
<input type="text" [(ngModel)]="editName" name="editName" required />
</div>
<div class="field">
<label>Description</label>
<textarea [(ngModel)]="editDescription" name="editDescription" rows="3"></textarea>
</div>
<div class="field">
<label>Univers associé</label>
<select [(ngModel)]="editLoreId" name="editLoreId">
<option value="">— Aucun univers (campagne libre) —</option>
@for (lore of availableLores; track lore) {
<option [value]="lore.id">{{ lore.name }}</option>
}
</select>
</div>
<div class="field">
<label>Système de JDR</label>
@if (!creatingGameSystem) {
<select
[(ngModel)]="editGameSystemId"
name="editGameSystemId"
(ngModelChange)="onEditGameSystemChange($event)">
<option value="">— Aucun (générique) —</option>
<option *ngFor="let gs of availableGameSystems" [value]="gs.id">{{ gs.name }}</option>
<option [value]="CREATE_GAMESYSTEM_SENTINEL">+ Créer un nouveau système…</option>
</select>
<div *ngIf="creatingGameSystem" class="inline-create">
<input
type="text"
[(ngModel)]="newGameSystemName"
name="newGameSystemName"
placeholder="Nom du nouveau système (ex: D&D 5e, Nimble, Maison)"
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
(keydown.escape)="cancelCreateGameSystem()"
autofocus
/>
<div class="inline-create-actions">
<button type="button" class="btn-inline-primary"
[disabled]="!newGameSystemName.trim() || creatingGameSystemInFlight"
(click)="submitCreateGameSystem()">
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
Créer
</button>
<button type="button" class="btn-inline-secondary" (click)="cancelCreateGameSystem()">
Annuler
</button>
</div>
</div>
</div>
<div class="header-actions">
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
Sauvegarder
</button>
<button type="button" class="btn-secondary" (click)="cancelEdit()">
Annuler
</button>
</div>
</div>
<section class="detail-section personas-section" *ngIf="!editing">
<div class="section-header">
<h2>Personnages</h2>
</div>
<!-- Les PJ ne sont plus rattachés à la campagne mais à une Partie (Playthrough) :
ils se gèrent depuis la page d'une Partie. Ici on ne liste que les PNJ
(donnée de scénario, partagée par toutes les Parties). -->
<!-- Sous-section : Personnages non-joueurs (PNJ) -->
<div class="persona-subsection">
<div class="subsection-header">
<h3>
<lucide-icon [img]="Drama" [size]="16"></lucide-icon>
Personnages non-joueurs
<span class="count-badge" *ngIf="npcs.length > 0">{{ npcs.length }}</span>
</h3>
<button class="btn-add" (click)="createNpc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouveau PNJ
</button>
</div>
<div class="characters-grid" *ngIf="npcs.length > 0">
<div class="character-card" *ngFor="let npc of npcs" (click)="viewNpc(npc)">
<lucide-icon [img]="Drama" [size]="20" class="character-icon character-icon--npc"></lucide-icon>
<div class="character-info">
<span class="character-name">{{ npc.name }}</span>
<span class="character-snippet">{{ personaSnippet(npc) }}</span>
<option value="">— Aucun (générique) —</option>
@for (gs of availableGameSystems; track gs) {
<option [value]="gs.id">{{ gs.name }}</option>
}
<option [value]="CREATE_GAMESYSTEM_SENTINEL">+ Créer un nouveau système…</option>
</select>
}
@if (creatingGameSystem) {
<div class="inline-create">
<input
type="text"
[(ngModel)]="newGameSystemName"
name="newGameSystemName"
placeholder="Nom du nouveau système (ex: D&D 5e, Nimble, Maison)"
(keydown.enter)="$event.preventDefault(); submitCreateGameSystem()"
(keydown.escape)="cancelCreateGameSystem()"
autofocus
/>
<div class="inline-create-actions">
<button type="button" class="btn-inline-primary"
[disabled]="!newGameSystemName.trim() || creatingGameSystemInFlight"
(click)="submitCreateGameSystem()">
<lucide-icon [img]="Check" [size]="14"></lucide-icon>
Créer
</button>
<button type="button" class="btn-inline-secondary" (click)="cancelCreateGameSystem()">
Annuler
</button>
</div>
</div>
}
</div>
<div class="header-actions">
<button type="button" class="btn-primary" (click)="saveEdit()" [disabled]="!editName.trim()">
Sauvegarder
</button>
<button type="button" class="btn-secondary" (click)="cancelEdit()">
Annuler
</button>
</div>
</div>
</div>
<div class="empty-state empty-state--compact" *ngIf="npcs.length === 0">
<p>Aucun PNJ pour le moment.</p>
<button class="btn-add-first" (click)="createNpc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Créer votre premier PNJ
</button>
</div>
}
@if (!editing) {
<section class="detail-section personas-section">
<div class="section-header">
<h2>Personnages</h2>
</div>
<!-- Les PJ ne sont plus rattachés à la campagne mais à une Partie (Playthrough) :
ils se gèrent depuis la page d'une Partie. Ici on ne liste que les PNJ
(donnée de scénario, partagée par toutes les Parties). -->
<!-- Sous-section : Personnages non-joueurs (PNJ) -->
<div class="persona-subsection">
<div class="subsection-header">
<h3>
<lucide-icon [img]="Drama" [size]="16"></lucide-icon>
Personnages non-joueurs
@if (npcs.length > 0) {
<span class="count-badge">{{ npcs.length }}</span>
}
</h3>
<button class="btn-add" (click)="createNpc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouveau PNJ
</button>
</div>
@if (npcs.length > 0) {
<div class="characters-grid">
@for (npc of npcs; track npc) {
<div class="character-card" (click)="viewNpc(npc)">
<lucide-icon [img]="Drama" [size]="20" class="character-icon character-icon--npc"></lucide-icon>
<div class="character-info">
<span class="character-name">{{ npc.name }}</span>
<span class="character-snippet">{{ personaSnippet(npc) }}</span>
</div>
</div>
}
</div>
}
@if (npcs.length === 0) {
<div class="empty-state empty-state--compact">
<p>Aucun PNJ pour le moment.</p>
<button class="btn-add-first" (click)="createNpc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Créer votre premier PNJ
</button>
</div>
}
</div>
</section>
}
@if (!editing) {
<section class="detail-section arcs-section">
<div class="section-header">
<h2>Arcs narratifs</h2>
<div class="section-header-actions">
<button class="btn-add" (click)="createArc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouvel arc
</button>
</div>
</div>
@if (arcs.length > 0) {
<div class="arcs-grid">
@for (arc of arcs; track arc) {
<div class="arc-card" (click)="openArc(arc)">
<lucide-icon [img]="Swords" [size]="24" class="arc-icon"></lucide-icon>
<span class="arc-name">{{ arc.name }}</span>
<span class="arc-meta">{{ chapterCountByArc[arc.id!] || 0 }} chapitres</span>
</div>
}
</div>
}
@if (arcs.length === 0) {
<div class="empty-state">
<lucide-icon [img]="Swords" [size]="40" class="empty-icon"></lucide-icon>
<p>Aucun arc narratif pour le moment.</p>
<button class="btn-add-first" (click)="createArc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Créer votre premier arc
</button>
</div>
}
</section>
}
<!-- ============ Parties (Playthroughs) ============ -->
@if (!editing) {
<section class="detail-section playthroughs-section">
<div class="section-header">
<h2>
<lucide-icon [img]="Dices" [size]="18"></lucide-icon>
Mes parties
</h2>
<button class="btn-add" [disabled]="newPlaythroughInFlight" (click)="createPlaythrough()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouvelle partie
</button>
</div>
@if (playthroughs.length > 0) {
<div class="playthroughs-grid">
@for (p of playthroughs; track p) {
<div class="playthrough-card"
(click)="openPlaythrough(p)">
<lucide-icon [img]="Dices" [size]="22" class="playthrough-icon"></lucide-icon>
<div class="playthrough-info">
<span class="playthrough-name">{{ p.name }}</span>
@if (p.description) {
<span class="playthrough-meta">{{ p.description }}</span>
}
</div>
</div>
}
</div>
}
@if (playthroughs.length === 0) {
<div class="empty-state empty-state--compact">
<p>Aucune partie. Créez-en une pour démarrer une session avec une table.</p>
</div>
}
</section>
}
<!-- Sessions retirées : elles vivent désormais dans une Partie.
Les faits narratifs sont définis dans les conditions de chaque quête
(chapter-edit > « Conditions de déblocage »). -->
</div>
</section>
<section class="detail-section arcs-section" *ngIf="!editing">
<div class="section-header">
<h2>Arcs narratifs</h2>
<div class="section-header-actions">
<button class="btn-add" (click)="createArc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouvel arc
</button>
</div>
</div>
<div class="arcs-grid" *ngIf="arcs.length > 0">
<div class="arc-card" *ngFor="let arc of arcs" (click)="openArc(arc)">
<lucide-icon [img]="Swords" [size]="24" class="arc-icon"></lucide-icon>
<span class="arc-name">{{ arc.name }}</span>
<span class="arc-meta">{{ chapterCountByArc[arc.id!] || 0 }} chapitres</span>
</div>
</div>
<div class="empty-state" *ngIf="arcs.length === 0">
<lucide-icon [img]="Swords" [size]="40" class="empty-icon"></lucide-icon>
<p>Aucun arc narratif pour le moment.</p>
<button class="btn-add-first" (click)="createArc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Créer votre premier arc
</button>
</div>
</section>
<!-- ============ Parties (Playthroughs) ============ -->
<section class="detail-section playthroughs-section" *ngIf="!editing">
<div class="section-header">
<h2>
<lucide-icon [img]="Dices" [size]="18"></lucide-icon>
Mes parties
</h2>
<button class="btn-add" [disabled]="newPlaythroughInFlight" (click)="createPlaythrough()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon>
Nouvelle partie
</button>
</div>
<div class="playthroughs-grid" *ngIf="playthroughs.length > 0">
<div class="playthrough-card"
*ngFor="let p of playthroughs"
(click)="openPlaythrough(p)">
<lucide-icon [img]="Dices" [size]="22" class="playthrough-icon"></lucide-icon>
<div class="playthrough-info">
<span class="playthrough-name">{{ p.name }}</span>
<span class="playthrough-meta" *ngIf="p.description">{{ p.description }}</span>
</div>
</div>
</div>
<div class="empty-state empty-state--compact" *ngIf="playthroughs.length === 0">
<p>Aucune partie. Créez-en une pour démarrer une session avec une table.</p>
</div>
</section>
<!-- Sessions retirées : elles vivent désormais dans une Partie.
Les faits narratifs sont définis dans les conditions de chaque quête
(chapter-edit > « Conditions de déblocage »). -->
</div>
}