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

@@ -13,164 +13,199 @@
</div>
<!-- Étape 1 : upload (masqué une fois en revue) -->
<section class="upload-area" *ngIf="!reviewing">
<input #pdfInput type="file" accept="application/pdf,.pdf" hidden (change)="onPdfSelected($event)" />
<button type="button" class="btn-primary big" [disabled]="importing" (click)="pdfInput.click()">
<lucide-icon [img]="Upload" [size]="16"></lucide-icon>
{{ importing ? 'Import en cours…' : 'Choisir un PDF de campagne' }}
</button>
<!-- Progression live -->
<div class="import-progress" *ngIf="importing">
<p class="import-phase">{{ importPhase }}</p>
<div class="progress-bar" *ngIf="importProgress">
<div class="progress-fill"
[style.width.%]="importProgress.total ? (importProgress.current / importProgress.total) * 100 : 0">
@if (!reviewing) {
<section class="upload-area">
<input #pdfInput type="file" accept="application/pdf,.pdf" hidden (change)="onPdfSelected($event)" />
<button type="button" class="btn-primary big" [disabled]="importing" (click)="pdfInput.click()">
<lucide-icon [img]="Upload" [size]="16"></lucide-icon>
{{ importing ? 'Import en cours…' : 'Choisir un PDF de campagne' }}
</button>
<!-- Progression live -->
@if (importing) {
<div class="import-progress">
<p class="import-phase">{{ importPhase }}</p>
@if (importProgress) {
<div class="progress-bar">
<div class="progress-fill"
[style.width.%]="importProgress.total ? (importProgress.current / importProgress.total) * 100 : 0">
</div>
</div>
}
@if (importCounts) {
<p class="import-counts">
Trouvé jusqu'ici : {{ importCounts.arcs }} arc(s) · {{ importCounts.chapters }} chapitre(s) · {{ importCounts.scenes }} scène(s)
</p>
}
</div>
</div>
<p class="import-counts" *ngIf="importCounts">
Trouvé jusqu'ici : {{ importCounts.arcs }} arc(s) · {{ importCounts.chapters }} chapitre(s) · {{ importCounts.scenes }} scène(s)
</p>
</div>
<p class="import-error" *ngIf="importError">{{ importError }}</p>
</section>
}
@if (importError) {
<p class="import-error">{{ importError }}</p>
}
</section>
}
<!-- Étape 2 : revue de l'arbre éditable -->
<section class="review-area" *ngIf="reviewing">
<div class="review-header">
<p class="review-summary">
À créer : <strong>{{ arcCount }}</strong> nouvel(s) arc(s),
<strong>{{ chapterCount }}</strong> chapitre(s),
<strong>{{ sceneCount }}</strong> scène(s).
Les éléments <em>« déjà présent »</em> (grisés) sont affichés pour situer les ajouts ;
ils ne seront pas recréés. Modifiez les nouveaux, puis créez.
</p>
</div>
<div class="tree">
<!-- Arc -->
<div class="arc-card" *ngFor="let arc of tree; let ai = index">
<div class="node-head arc-head" [class.existing-node]="arc.existing">
<button type="button" class="btn-collapse" (click)="toggleArc(arc)">
<lucide-icon [img]="arc.collapsed ? ChevronRight : ChevronDown" [size]="16"></lucide-icon>
</button>
<lucide-icon [img]="Swords" [size]="16" class="node-icon"></lucide-icon>
<input type="text" class="node-name" [(ngModel)]="arc.name" [name]="'arc-' + ai"
[readonly]="arc.existing" placeholder="Nom de l'arc" />
<span class="badge-existing" *ngIf="arc.existing">déjà présent</span>
<button type="button" class="btn-remove" *ngIf="!arc.existing" (click)="removeArc(ai)" title="Supprimer l'arc">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
</button>
</div>
<div class="node-body" *ngIf="!arc.collapsed">
<div class="arc-type-toggle" *ngIf="!arc.existing">
<span class="toggle-label">Type :</span>
<button type="button" class="type-btn" [class.active]="arc.type === 'LINEAR'"
(click)="setArcType(arc, 'LINEAR')">Linéaire</button>
<button type="button" class="type-btn" [class.active]="arc.type === 'HUB'"
(click)="setArcType(arc, 'HUB')">Hub (quêtes)</button>
</div>
<textarea class="node-desc" *ngIf="!arc.existing" [(ngModel)]="arc.description" [name]="'arc-desc-' + ai"
rows="2" placeholder="Synopsis de l'arc (optionnel)"></textarea>
<!-- Chapitres -->
<div class="chapter-card" *ngFor="let chapter of arc.chapters; let ci = index">
<div class="node-head chapter-head" [class.existing-node]="chapter.existing">
<button type="button" class="btn-collapse" (click)="toggleChapter(chapter)">
<lucide-icon [img]="chapter.collapsed ? ChevronRight : ChevronDown" [size]="14"></lucide-icon>
</button>
<lucide-icon [img]="BookOpen" [size]="14" class="node-icon"></lucide-icon>
<input type="text" class="node-name" [(ngModel)]="chapter.name" [name]="'chap-' + ai + '-' + ci"
[readonly]="chapter.existing" placeholder="Nom du chapitre" />
<span class="badge-existing" *ngIf="chapter.existing">déjà présent</span>
<button type="button" class="btn-remove" *ngIf="!chapter.existing" (click)="removeChapter(arc, ci)" title="Supprimer le chapitre">
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
@if (reviewing) {
<section class="review-area">
<div class="review-header">
<p class="review-summary">
À créer : <strong>{{ arcCount }}</strong> nouvel(s) arc(s),
<strong>{{ chapterCount }}</strong> chapitre(s),
<strong>{{ sceneCount }}</strong> scène(s).
Les éléments <em>« déjà présent »</em> (grisés) sont affichés pour situer les ajouts ;
ils ne seront pas recréés. Modifiez les nouveaux, puis créez.
</p>
</div>
<div class="tree">
<!-- Arc -->
@for (arc of tree; track arc; let ai = $index) {
<div class="arc-card">
<div class="node-head arc-head" [class.existing-node]="arc.existing">
<button type="button" class="btn-collapse" (click)="toggleArc(arc)">
<lucide-icon [img]="arc.collapsed ? ChevronRight : ChevronDown" [size]="16"></lucide-icon>
</button>
<lucide-icon [img]="Swords" [size]="16" class="node-icon"></lucide-icon>
<input type="text" class="node-name" [(ngModel)]="arc.name" [name]="'arc-' + ai"
[readonly]="arc.existing" placeholder="Nom de l'arc" />
@if (arc.existing) {
<span class="badge-existing">déjà présent</span>
}
@if (!arc.existing) {
<button type="button" class="btn-remove" (click)="removeArc(ai)" title="Supprimer l'arc">
<lucide-icon [img]="Trash2" [size]="14"></lucide-icon>
</button>
}
</div>
<div class="node-body" *ngIf="!chapter.collapsed">
<textarea class="node-desc" *ngIf="!chapter.existing" [(ngModel)]="chapter.description" [name]="'chap-desc-' + ai + '-' + ci"
rows="2" placeholder="Synopsis du chapitre (optionnel)"></textarea>
<!-- Scènes -->
<div class="scene-card" *ngFor="let scene of chapter.scenes; let si = index"
[class.existing-node]="scene.existing">
<div class="scene-row">
<lucide-icon [img]="MapPin" [size]="13" class="node-icon scene-icon"></lucide-icon>
<div class="scene-fields">
<input type="text" class="node-name" [(ngModel)]="scene.name"
[name]="'scene-' + ai + '-' + ci + '-' + si" [readonly]="scene.existing"
placeholder="Nom de la scène" />
<input type="text" class="node-desc-inline" *ngIf="!scene.existing" [(ngModel)]="scene.description"
[name]="'scene-desc-' + ai + '-' + ci + '-' + si" placeholder="Synopsis (optionnel)" />
@if (!arc.collapsed) {
<div class="node-body">
@if (!arc.existing) {
<div class="arc-type-toggle">
<span class="toggle-label">Type :</span>
<button type="button" class="type-btn" [class.active]="arc.type === 'LINEAR'"
(click)="setArcType(arc, 'LINEAR')">Linéaire</button>
<button type="button" class="type-btn" [class.active]="arc.type === 'HUB'"
(click)="setArcType(arc, 'HUB')">Hub (quêtes)</button>
</div>
<span class="badge-existing" *ngIf="scene.existing">déjà présent</span>
<button type="button" class="btn-details" *ngIf="!scene.existing" (click)="toggleDetails(scene)"
title="Narration joueurs, notes MJ, pièces">
<lucide-icon [img]="scene.detailsOpen ? ChevronDown : ChevronRight" [size]="12"></lucide-icon>
Détails<span *ngIf="scene.rooms.length"> · {{ scene.rooms.length }} pièce(s)</span>
</button>
<button type="button" class="btn-remove" *ngIf="!scene.existing" (click)="removeScene(chapter, si)" title="Supprimer la scène">
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
</button>
</div>
<div class="scene-details" *ngIf="scene.detailsOpen && !scene.existing">
<label class="field-label">À lire aux joueurs</label>
<textarea class="node-desc" [(ngModel)]="scene.playerNarration"
[name]="'scene-pn-' + ai + '-' + ci + '-' + si" rows="3"
placeholder="Texte d'encadré lu aux joueurs (optionnel)"></textarea>
<label class="field-label">Notes MJ</label>
<textarea class="node-desc" [(ngModel)]="scene.gmNotes"
[name]="'scene-gm-' + ai + '-' + ci + '-' + si" rows="3"
placeholder="Secrets, développement, conséquences (optionnel)"></textarea>
<!-- Pièces (donjon) : vide = scène narrative classique -->
<span class="field-label">Pièces (lieu explorable)</span>
<div class="rooms">
<div class="room-row" *ngFor="let room of scene.rooms; let ri = index">
<input type="text" class="room-name" [(ngModel)]="room.name"
[name]="'room-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Pièce" />
<input type="text" class="room-field" [(ngModel)]="room.description"
[name]="'room-desc-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Description" />
<input type="text" class="room-field" [(ngModel)]="room.enemies"
[name]="'room-en-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Ennemis" />
<input type="text" class="room-field" [(ngModel)]="room.loot"
[name]="'room-loot-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Trésor" />
<button type="button" class="btn-remove" (click)="removeRoom(scene, ri)" title="Supprimer la pièce">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
}
@if (!arc.existing) {
<textarea class="node-desc" [(ngModel)]="arc.description" [name]="'arc-desc-' + ai"
rows="2" placeholder="Synopsis de l'arc (optionnel)"></textarea>
}
<!-- Chapitres -->
@for (chapter of arc.chapters; track chapter; let ci = $index) {
<div class="chapter-card">
<div class="node-head chapter-head" [class.existing-node]="chapter.existing">
<button type="button" class="btn-collapse" (click)="toggleChapter(chapter)">
<lucide-icon [img]="chapter.collapsed ? ChevronRight : ChevronDown" [size]="14"></lucide-icon>
</button>
<lucide-icon [img]="BookOpen" [size]="14" class="node-icon"></lucide-icon>
<input type="text" class="node-name" [(ngModel)]="chapter.name" [name]="'chap-' + ai + '-' + ci"
[readonly]="chapter.existing" placeholder="Nom du chapitre" />
@if (chapter.existing) {
<span class="badge-existing">déjà présent</span>
}
@if (!chapter.existing) {
<button type="button" class="btn-remove" (click)="removeChapter(arc, ci)" title="Supprimer le chapitre">
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
</button>
}
</div>
@if (!chapter.collapsed) {
<div class="node-body">
@if (!chapter.existing) {
<textarea class="node-desc" [(ngModel)]="chapter.description" [name]="'chap-desc-' + ai + '-' + ci"
rows="2" placeholder="Synopsis du chapitre (optionnel)"></textarea>
}
<!-- Scènes -->
@for (scene of chapter.scenes; track scene; let si = $index) {
<div class="scene-card"
[class.existing-node]="scene.existing">
<div class="scene-row">
<lucide-icon [img]="MapPin" [size]="13" class="node-icon scene-icon"></lucide-icon>
<div class="scene-fields">
<input type="text" class="node-name" [(ngModel)]="scene.name"
[name]="'scene-' + ai + '-' + ci + '-' + si" [readonly]="scene.existing"
placeholder="Nom de la scène" />
@if (!scene.existing) {
<input type="text" class="node-desc-inline" [(ngModel)]="scene.description"
[name]="'scene-desc-' + ai + '-' + ci + '-' + si" placeholder="Synopsis (optionnel)" />
}
</div>
@if (scene.existing) {
<span class="badge-existing">déjà présent</span>
}
@if (!scene.existing) {
<button type="button" class="btn-details" (click)="toggleDetails(scene)"
title="Narration joueurs, notes MJ, pièces">
<lucide-icon [img]="scene.detailsOpen ? ChevronDown : ChevronRight" [size]="12"></lucide-icon>
Détails@if (scene.rooms.length) {
<span> · {{ scene.rooms.length }} pièce(s)</span>
}
</button>
}
@if (!scene.existing) {
<button type="button" class="btn-remove" (click)="removeScene(chapter, si)" title="Supprimer la scène">
<lucide-icon [img]="Trash2" [size]="13"></lucide-icon>
</button>
}
</div>
@if (scene.detailsOpen && !scene.existing) {
<div class="scene-details">
<label class="field-label">À lire aux joueurs</label>
<textarea class="node-desc" [(ngModel)]="scene.playerNarration"
[name]="'scene-pn-' + ai + '-' + ci + '-' + si" rows="3"
placeholder="Texte d'encadré lu aux joueurs (optionnel)"></textarea>
<label class="field-label">Notes MJ</label>
<textarea class="node-desc" [(ngModel)]="scene.gmNotes"
[name]="'scene-gm-' + ai + '-' + ci + '-' + si" rows="3"
placeholder="Secrets, développement, conséquences (optionnel)"></textarea>
<!-- Pièces (donjon) : vide = scène narrative classique -->
<span class="field-label">Pièces (lieu explorable)</span>
<div class="rooms">
@for (room of scene.rooms; track room; let ri = $index) {
<div class="room-row">
<input type="text" class="room-name" [(ngModel)]="room.name"
[name]="'room-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Pièce" />
<input type="text" class="room-field" [(ngModel)]="room.description"
[name]="'room-desc-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Description" />
<input type="text" class="room-field" [(ngModel)]="room.enemies"
[name]="'room-en-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Ennemis" />
<input type="text" class="room-field" [(ngModel)]="room.loot"
[name]="'room-loot-' + ai + '-' + ci + '-' + si + '-' + ri" placeholder="Trésor" />
<button type="button" class="btn-remove" (click)="removeRoom(scene, ri)" title="Supprimer la pièce">
<lucide-icon [img]="Trash2" [size]="12"></lucide-icon>
</button>
</div>
}
<button type="button" class="btn-add-inline" (click)="addRoom(scene)">
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Pièce
</button>
</div>
</div>
}
</div>
}
<button type="button" class="btn-add-inline" (click)="addScene(chapter)">
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Scène
</button>
</div>
<button type="button" class="btn-add-inline" (click)="addRoom(scene)">
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Pièce
</button>
</div>
}
</div>
</div>
<button type="button" class="btn-add-inline" (click)="addScene(chapter)">
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Scène
}
<button type="button" class="btn-add-inline" (click)="addChapter(arc)">
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Chapitre
</button>
</div>
</div>
<button type="button" class="btn-add-inline" (click)="addChapter(arc)">
<lucide-icon [img]="Plus" [size]="12"></lucide-icon> Chapitre
</button>
}
</div>
</div>
}
<button type="button" class="btn-add" (click)="addArc()">
<lucide-icon [img]="Plus" [size]="14"></lucide-icon> Ajouter un arc
</button>
</div>
<p class="apply-error" *ngIf="applyError">{{ applyError }}</p>
@if (applyError) {
<p class="apply-error">{{ applyError }}</p>
}
<div class="review-actions">
<button type="button" class="btn-primary" [disabled]="applying || !hasNewContent" (click)="apply()">
<lucide-icon [img]="Check" [size]="16"></lucide-icon>
@@ -179,5 +214,6 @@
<button type="button" class="btn-secondary" (click)="cancel()">Annuler</button>
</div>
</section>
}
</div>

View File

@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import {
@@ -47,7 +47,7 @@ interface ArcNode {
*/
@Component({
selector: 'app-campaign-import',
imports: [CommonModule, FormsModule, LucideAngularModule],
imports: [FormsModule, LucideAngularModule],
templateUrl: './campaign-import.component.html',
styleUrls: ['./campaign-import.component.scss']
})