From 2776b50c0627de743482262559798747d362c5a9 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Fri, 29 May 2026 18:47:55 +0000 Subject: [PATCH 01/27] =?UTF-8?q?Edit=20des=20fichiers=20mod=C3=A8les?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/tg/cyberlabmanager/model/AuditEntry.java | 5 +++++ src/main/java/tg/cyberlabmanager/model/Lab.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java index 4c6731f..fa7fa65 100644 --- a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java @@ -8,6 +8,7 @@ public class AuditEntry { private Integer vmId; private String action; private String details; + private String labName; public int getId() { return id; @@ -48,4 +49,8 @@ public String getDetails() { public void setDetails(String details) { this.details = details; } + + public String getLog() { + return ""; + } } diff --git a/src/main/java/tg/cyberlabmanager/model/Lab.java b/src/main/java/tg/cyberlabmanager/model/Lab.java index 9e02ad8..9b0522d 100644 --- a/src/main/java/tg/cyberlabmanager/model/Lab.java +++ b/src/main/java/tg/cyberlabmanager/model/Lab.java @@ -10,6 +10,7 @@ public class Lab { private String description; private String category; private final List virtualMachines = new ArrayList<>(); + private String notes; public Lab() { } @@ -53,6 +54,10 @@ public void setCategory(String category) { this.category = category; } + public String getNotes() { return notes; } + + public void setNotes(String notes) { this.notes = notes; } + public List getVirtualMachines() { return Collections.unmodifiableList(virtualMachines); } From c204060251366340a6385c78d12d0eebb1c1d6e9 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Fri, 29 May 2026 19:51:31 +0000 Subject: [PATCH 02/27] Add lab name to audit schema --- src/main/resources/tg/cyberlabmanager/data/schema.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/tg/cyberlabmanager/data/schema.sql b/src/main/resources/tg/cyberlabmanager/data/schema.sql index fde532e..96160eb 100644 --- a/src/main/resources/tg/cyberlabmanager/data/schema.sql +++ b/src/main/resources/tg/cyberlabmanager/data/schema.sql @@ -35,6 +35,7 @@ CREATE TABLE IF NOT EXISTS audit_entry ( id_audit INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT NOT NULL, id_vm INTEGER, + lab_name TEXT, action TEXT NOT NULL, details TEXT, FOREIGN KEY (id_vm) REFERENCES virtual_machine(id_vm) ON DELETE SET NULL From e6b6e6d15f35cc0ec37125f38dd3ad019c9281d2 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Fri, 29 May 2026 19:51:55 +0000 Subject: [PATCH 03/27] Align model entities with domain logic --- .../tg/cyberlabmanager/model/AuditEntry.java | 27 ++++++++++++++++++- .../java/tg/cyberlabmanager/model/Lab.java | 5 ---- .../tg/cyberlabmanager/model/Snapshot.java | 18 +++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java index fa7fa65..b4fee56 100644 --- a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java @@ -1,8 +1,11 @@ package tg.cyberlabmanager.model; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; public class AuditEntry { + private static final DateTimeFormatter LOG_TIMESTAMP_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + private int id; private LocalDateTime timestamp; private Integer vmId; @@ -50,7 +53,29 @@ public void setDetails(String details) { this.details = details; } + public String getLabName() { + return labName; + } + + public void setLabName(String labName) { + this.labName = labName; + } + public String getLog() { - return ""; + StringBuilder log = new StringBuilder(); + log.append(timestamp == null ? "unknown-time" : timestamp.format(LOG_TIMESTAMP_FORMATTER)); + log.append(" | action=").append(action == null ? "UNKNOWN" : action); + + if (vmId != null) { + log.append(" | vmId=").append(vmId); + } + if (labName != null && !labName.isBlank()) { + log.append(" | lab=").append(labName); + } + if (details != null && !details.isBlank()) { + log.append(" | details=").append(details); + } + + return log.toString(); } } diff --git a/src/main/java/tg/cyberlabmanager/model/Lab.java b/src/main/java/tg/cyberlabmanager/model/Lab.java index 9b0522d..9e02ad8 100644 --- a/src/main/java/tg/cyberlabmanager/model/Lab.java +++ b/src/main/java/tg/cyberlabmanager/model/Lab.java @@ -10,7 +10,6 @@ public class Lab { private String description; private String category; private final List virtualMachines = new ArrayList<>(); - private String notes; public Lab() { } @@ -54,10 +53,6 @@ public void setCategory(String category) { this.category = category; } - public String getNotes() { return notes; } - - public void setNotes(String notes) { this.notes = notes; } - public List getVirtualMachines() { return Collections.unmodifiableList(virtualMachines); } diff --git a/src/main/java/tg/cyberlabmanager/model/Snapshot.java b/src/main/java/tg/cyberlabmanager/model/Snapshot.java index bd5e879..03eed48 100644 --- a/src/main/java/tg/cyberlabmanager/model/Snapshot.java +++ b/src/main/java/tg/cyberlabmanager/model/Snapshot.java @@ -4,9 +4,11 @@ public class Snapshot { private int id; + private String uuid; private String name; private LocalDateTime createdAt; private String description; + private boolean online; public int getId() { return id; @@ -16,6 +18,14 @@ public void setId(int id) { this.id = id; } + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + public String getName() { return name; } @@ -39,4 +49,12 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public boolean isOnline() { + return online; + } + + public void setOnline(boolean online) { + this.online = online; + } } From f284e9555088bef15d236bbba73450dd8e9d0386 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Fri, 29 May 2026 20:11:26 +0000 Subject: [PATCH 04/27] Document public Java APIs with Javadoc --- src/main/java/module-info.java | 5 +- .../tg/cyberlabmanager/app/CyberLabApp.java | 15 +++ .../java/tg/cyberlabmanager/app/Launcher.java | 14 +++ .../controller/LabController.java | 79 ++++++++++++++ .../cyberlabmanager/data/DatabaseManager.java | 100 ++++++++++++++++++ .../hypervisor/HypervisorException.java | 14 +++ .../hypervisor/IHypervisor.java | 49 +++++++++ .../hypervisor/VBoxException.java | 22 ++++ .../hypervisor/VBoxOrchestrator.java | 16 +++ .../tg/cyberlabmanager/model/AppConfig.java | 32 ++++++ .../tg/cyberlabmanager/model/AuditEntry.java | 77 ++++++++++++++ .../cyberlabmanager/model/JournalEntry.java | 42 ++++++++ .../java/tg/cyberlabmanager/model/Lab.java | 73 +++++++++++++ .../tg/cyberlabmanager/model/Snapshot.java | 73 +++++++++++++ .../tg/cyberlabmanager/model/VMStatus.java | 8 ++ .../cyberlabmanager/model/VirtualMachine.java | 85 ++++++++++++++- .../tg/cyberlabmanager/pdf/PdfExporter.java | 19 ++++ .../tg/cyberlabmanager/ui/LabListView.java | 8 ++ .../java/tg/cyberlabmanager/ui/MainView.java | 8 ++ .../tg/cyberlabmanager/ui/VmControlPanel.java | 8 ++ 20 files changed, 745 insertions(+), 2 deletions(-) diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6bcfcc9..930b3ae 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,3 +1,6 @@ +/** + * Module principal de l'application Cyber Lab Manager. + */ module tg.cyberlabmanager { // ── JavaFX ────────────────────────────────────────────────────────── @@ -31,4 +34,4 @@ exports tg.cyberlabmanager.hypervisor; exports tg.cyberlabmanager.pdf; exports tg.cyberlabmanager.ui; -} \ No newline at end of file +} diff --git a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java index 70b6866..d8770e1 100644 --- a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java +++ b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java @@ -7,7 +7,22 @@ import java.io.IOException; +/** + * Application JavaFX principale de Cyber Lab Manager. + */ public class CyberLabApp extends Application { + /** + * Cree l'application JavaFX. + */ + public CyberLabApp() { + } + + /** + * Initialise et affiche la fenetre principale JavaFX. + * + * @param stage scene principale fournie par JavaFX + * @throws IOException si le chargement du fichier FXML echoue + */ @Override public void start(Stage stage) throws IOException { FXMLLoader loader = new FXMLLoader(CyberLabApp.class.getResource("/tg/cyberlabmanager/ui/main-view.fxml")); diff --git a/src/main/java/tg/cyberlabmanager/app/Launcher.java b/src/main/java/tg/cyberlabmanager/app/Launcher.java index 7e25325..113aaca 100644 --- a/src/main/java/tg/cyberlabmanager/app/Launcher.java +++ b/src/main/java/tg/cyberlabmanager/app/Launcher.java @@ -2,7 +2,21 @@ import javafx.application.Application; +/** + * Point d'entree deleguant le lancement a l'application JavaFX. + */ public class Launcher { + /** + * Cree le lanceur de l'application. + */ + public Launcher() { + } + + /** + * Lance l'application JavaFX. + * + * @param args arguments de ligne de commande + */ public static void main(String[] args) { Application.launch(CyberLabApp.class, args); } diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 732d284..7228bab 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -11,6 +11,9 @@ import java.util.List; +/** + * Controleur principal coordonnant l'interface, la persistance et l'hyperviseur. + */ public class LabController { private IHypervisor hypervisor; private DatabaseManager databaseManager; @@ -19,9 +22,19 @@ public class LabController { @FXML private Label statusLabel; + /** + * Cree un controleur vide utilise par JavaFX lors du chargement FXML. + */ public LabController() { } + /** + * Cree un controleur avec ses dependances injectees. + * + * @param hypervisor service de pilotage de l'hyperviseur + * @param databaseManager service de persistance + * @param pdfExporter service d'export PDF + */ public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { this.hypervisor = hypervisor; this.databaseManager = databaseManager; @@ -35,54 +48,120 @@ private void initialize() { } } + /** + * Declenche le demarrage d'une machine virtuelle. + * + * @param vm machine virtuelle cible + */ public void onStartVMClicked(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'arret d'une machine virtuelle. + * + * @param vm machine virtuelle cible + */ public void onStopVMClicked(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche la sauvegarde d'etat d'une machine virtuelle. + * + * @param vm machine virtuelle cible + */ public void onSaveStateClicked(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche la creation d'un snapshot pour une machine virtuelle. + * + * @param vm machine virtuelle cible + * @param name nom du snapshot + * @param description description du snapshot + */ public void onTakeSnapshotClicked(VirtualMachine vm, String name, String description) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche la restauration d'un snapshot pour une machine virtuelle. + * + * @param vm machine virtuelle cible + * @param snapshotName nom ou identifiant du snapshot + */ public void onRestoreSnapshotClicked(VirtualMachine vm, String snapshotName) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Ajoute une note d'analyse a une machine virtuelle. + * + * @param vm machine virtuelle cible + * @param text contenu de la note + */ public void onAddJournalEntry(VirtualMachine vm, String text) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'import des machines virtuelles disponibles. + */ public void onImportVMsClicked() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Confirme l'import de machines virtuelles selectionnees. + * + * @param selectedVMs machines virtuelles selectionnees + * @param labId identifiant du laboratoire cible, ou {@code null} + */ public void onImportConfirm(List selectedVMs, Integer labId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'export PDF d'un laboratoire. + * + * @param lab laboratoire a exporter + */ public void onExportLabClicked(Lab lab) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Declenche l'export des machines virtuelles sans laboratoire. + */ public void onExportOrphansClicked() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Retourne tous les laboratoires connus. + * + * @return liste des laboratoires + */ public List getAllLabs() { return databaseManager.getAllLabs(); } + /** + * Retourne les machines virtuelles non rattachees a un laboratoire. + * + * @return liste des machines virtuelles orphelines + */ public List getOrphanVMs() { return databaseManager.getOrphanVMs(); } + /** + * Persiste la configuration mise a jour. + * + * @param newConfig nouvelle configuration applicative + */ public void onConfigUpdated(AppConfig newConfig) { databaseManager.saveConfig(newConfig); } diff --git a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java index 9cd04a9..833413f 100644 --- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java +++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java @@ -9,67 +9,167 @@ import java.util.List; +/** + * Service d'acces aux donnees de l'application. + * + *

Il masque la persistance SQLite derriere des operations de haut niveau sur + * les entites metier.

+ */ public class DatabaseManager { + /** + * Cree un gestionnaire de base de donnees. + */ + public DatabaseManager() { + } + + /** + * Enregistre ou met a jour un laboratoire. + * + * @param lab laboratoire a persister + */ public void saveLab(Lab lab) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere un laboratoire par son identifiant local. + * + * @param id identifiant local du laboratoire + * @return laboratoire correspondant + */ public Lab getLab(int id) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere tous les laboratoires. + * + * @return liste des laboratoires + */ public List getAllLabs() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Supprime un laboratoire sans supprimer les VM physiques. + * + * @param lab laboratoire a supprimer + */ public void deleteLab(Lab lab) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Enregistre ou met a jour une machine virtuelle. + * + * @param vm machine virtuelle a persister + * @param labId identifiant du laboratoire rattache, ou {@code null} + */ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les machines virtuelles rattachees a un laboratoire. + * + * @param labId identifiant local du laboratoire + * @return liste des machines virtuelles rattachees + */ public List getVMsForLab(int labId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les machines virtuelles sans laboratoire. + * + * @return liste des machines virtuelles orphelines + */ public List getOrphanVMs() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Detache une machine virtuelle de son laboratoire. + * + * @param vm machine virtuelle a detacher + */ public void removeVMFromLab(VirtualMachine vm) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Enregistre un snapshot pour une machine virtuelle. + * + * @param snap snapshot a persister + * @param vmId identifiant local de la machine virtuelle + */ public void saveSnapshot(Snapshot snap, int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les snapshots d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des snapshots rattaches + */ public List getSnapshotsForVM(int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Ajoute une note d'analyse pour une machine virtuelle. + * + * @param entry note d'analyse a persister + * @param vmId identifiant local de la machine virtuelle + */ public void addJournalEntry(JournalEntry entry, int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les notes d'analyse d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des notes d'analyse + */ public List getJournalEntriesForVM(int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Ajoute une entree d'audit. + * + * @param entry entree d'audit a persister + */ public void addAuditEntry(AuditEntry entry) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere les entrees d'audit d'une machine virtuelle. + * + * @param vmId identifiant local de la machine virtuelle + * @return liste des entrees d'audit + */ public List getAuditLogsForVM(int vmId) { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Recupere la configuration applicative. + * + * @return configuration applicative + */ public AppConfig getConfig() { throw new UnsupportedOperationException("Not implemented yet"); } + /** + * Enregistre la configuration applicative. + * + * @param config configuration a persister + */ public void saveConfig(AppConfig config) { throw new UnsupportedOperationException("Not implemented yet"); } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java b/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java index e4c0b3c..d59bf62 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/HypervisorException.java @@ -1,10 +1,24 @@ package tg.cyberlabmanager.hypervisor; +/** + * Exception commune aux erreurs d'orchestration d'hyperviseur. + */ public class HypervisorException extends Exception { + /** + * Cree une exception d'hyperviseur avec un message. + * + * @param message message decrivant l'erreur + */ public HypervisorException(String message) { super(message); } + /** + * Cree une exception d'hyperviseur avec un message et une cause. + * + * @param message message decrivant l'erreur + * @param cause cause initiale de l'erreur + */ public HypervisorException(String message, Throwable cause) { super(message, cause); } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java index c83757b..99ad3cf 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/IHypervisor.java @@ -4,18 +4,67 @@ import java.util.List; +/** + * Contrat minimal pour piloter un hyperviseur depuis l'application. + */ public interface IHypervisor { + /** + * Liste les machines virtuelles disponibles dans l'hyperviseur. + * + * @return liste de couples ou tableaux de donnees representant les VM + * @throws HypervisorException si la recuperation echoue + */ List listVMs() throws HypervisorException; + /** + * Retourne le statut courant d'une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @return statut courant de la VM + * @throws HypervisorException si le statut ne peut pas etre recupere + */ VMStatus getStatus(String uuid) throws HypervisorException; + /** + * Demarre une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si le demarrage echoue + */ void startVM(String uuid) throws HypervisorException; + /** + * Arrete une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si l'arret echoue + */ void stopVM(String uuid) throws HypervisorException; + /** + * Sauvegarde l'etat d'une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @throws HypervisorException si la sauvegarde d'etat echoue + */ void saveState(String uuid) throws HypervisorException; + /** + * Cree un snapshot pour une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @param name nom du snapshot + * @param description description du snapshot + * @throws HypervisorException si la creation du snapshot echoue + */ void takeSnapshot(String uuid, String name, String description) throws HypervisorException; + /** + * Restaure un snapshot pour une machine virtuelle. + * + * @param uuid UUID de la machine virtuelle + * @param snapshotName nom ou identifiant du snapshot + * @throws HypervisorException si la restauration echoue + */ void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException; } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java index 7a513e0..9851744 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxException.java @@ -1,19 +1,41 @@ package tg.cyberlabmanager.hypervisor; +/** + * Exception specifique aux commandes VirtualBox executees via VBoxManage. + */ public class VBoxException extends HypervisorException { + /** Code de sortie retourne par VBoxManage. */ private final int exitCode; + /** Sortie standard ou erreur retournee par VBoxManage. */ private final String output; + /** + * Cree une exception VirtualBox avec le code de sortie et la sortie texte. + * + * @param message message decrivant l'erreur + * @param exitCode code de sortie de VBoxManage + * @param output sortie standard ou erreur produite par VBoxManage + */ public VBoxException(String message, int exitCode, String output) { super(message); this.exitCode = exitCode; this.output = output; } + /** + * Retourne le code de sortie de VBoxManage. + * + * @return code de sortie + */ public int getExitCode() { return exitCode; } + /** + * Retourne la sortie produite par VBoxManage. + * + * @return sortie standard ou erreur + */ public String getOutput() { return output; } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java index 4228933..b8f432d 100644 --- a/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java +++ b/src/main/java/tg/cyberlabmanager/hypervisor/VBoxOrchestrator.java @@ -4,37 +4,53 @@ import java.util.List; +/** + * Implementation de {@link IHypervisor} pour VirtualBox via VBoxManage. + */ public class VBoxOrchestrator implements IHypervisor { + /** + * Cree un orchestrateur VirtualBox. + */ + public VBoxOrchestrator() { + } + + /** {@inheritDoc} */ @Override public List listVMs() throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public VMStatus getStatus(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void startVM(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void stopVM(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void saveState(String uuid) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void takeSnapshot(String uuid, String name, String description) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); } + /** {@inheritDoc} */ @Override public void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException { throw new UnsupportedOperationException("Not implemented yet"); diff --git a/src/main/java/tg/cyberlabmanager/model/AppConfig.java b/src/main/java/tg/cyberlabmanager/model/AppConfig.java index 6d6c891..04d3f60 100644 --- a/src/main/java/tg/cyberlabmanager/model/AppConfig.java +++ b/src/main/java/tg/cyberlabmanager/model/AppConfig.java @@ -1,21 +1,53 @@ package tg.cyberlabmanager.model; +/** + * Configuration persistante de l'application. + * + *

Elle contient les chemins utilises par les services techniques, notamment + * VBoxManage et le repertoire d'export PDF.

+ */ public class AppConfig { private String vboxManagePath; private String pdfExportDirectory; + /** + * Cree une configuration vide. + */ + public AppConfig() { + } + + /** + * Retourne le chemin de l'executable VBoxManage. + * + * @return chemin configure vers VBoxManage + */ public String getVboxManagePath() { return vboxManagePath; } + /** + * Definit le chemin de l'executable VBoxManage. + * + * @param vboxManagePath chemin vers VBoxManage + */ public void setVboxManagePath(String vboxManagePath) { this.vboxManagePath = vboxManagePath; } + /** + * Retourne le repertoire d'export des rapports PDF. + * + * @return repertoire d'export PDF + */ public String getPdfExportDirectory() { return pdfExportDirectory; } + /** + * Definit le repertoire d'export des rapports PDF. + * + * @param pdfExportDirectory repertoire d'export PDF + */ public void setPdfExportDirectory(String pdfExportDirectory) { this.pdfExportDirectory = pdfExportDirectory; } diff --git a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java index b4fee56..03a8ec2 100644 --- a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java @@ -3,6 +3,12 @@ import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +/** + * Trace automatique d'une action de controle effectuee sur une machine virtuelle. + * + *

Cette entite est separee de {@link VirtualMachine} afin de conserver un + * historique potentiellement volumineux sans alourdir le modele de la VM.

+ */ public class AuditEntry { private static final DateTimeFormatter LOG_TIMESTAMP_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; @@ -13,54 +19,125 @@ public class AuditEntry { private String details; private String labName; + /** + * Cree une entree d'audit vide. + */ + public AuditEntry() { + } + + /** + * Retourne l'identifiant local de l'entree d'audit. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Definit l'identifiant local de l'entree d'audit. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'horodatage de l'action. + * + * @return date et heure de l'action + */ public LocalDateTime getTimestamp() { return timestamp; } + /** + * Definit l'horodatage de l'action. + * + * @param timestamp date et heure de l'action + */ public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + /** + * Retourne l'identifiant local de la VM concernee. + * + * @return identifiant de la VM, ou {@code null} si aucune VM n'est liee + */ public Integer getVmId() { return vmId; } + /** + * Definit l'identifiant local de la VM concernee. + * + * @param vmId identifiant de la VM, ou {@code null} + */ public void setVmId(Integer vmId) { this.vmId = vmId; } + /** + * Retourne l'action auditee. + * + * @return action auditee + */ public String getAction() { return action; } + /** + * Definit l'action auditee. + * + * @param action action auditee + */ public void setAction(String action) { this.action = action; } + /** + * Retourne les details de l'action. + * + * @return details de l'action + */ public String getDetails() { return details; } + /** + * Definit les details de l'action. + * + * @param details details de l'action + */ public void setDetails(String details) { this.details = details; } + /** + * Retourne le nom du laboratoire concerne. + * + * @return nom du laboratoire + */ public String getLabName() { return labName; } + /** + * Definit le nom du laboratoire concerne. + * + * @param labName nom du laboratoire + */ public void setLabName(String labName) { this.labName = labName; } + /** + * Construit une representation textuelle de l'entree d'audit. + * + * @return ligne de log lisible + */ public String getLog() { StringBuilder log = new StringBuilder(); log.append(timestamp == null ? "unknown-time" : timestamp.format(LOG_TIMESTAMP_FORMATTER)); diff --git a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java index ec7031f..76fca92 100644 --- a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java @@ -2,31 +2,73 @@ import java.time.LocalDateTime; +/** + * Note d'analyse horodatee saisie par l'analyste pour une machine virtuelle. + * + *

Une entree de journal est rattachee au cycle de vie logique d'une + * {@link VirtualMachine}.

+ */ public class JournalEntry { private int id; private LocalDateTime timestamp; private String content; + /** + * Cree une entree de journal vide. + */ + public JournalEntry() { + } + + /** + * Retourne l'identifiant local de l'entree de journal. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Definit l'identifiant local de l'entree de journal. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'horodatage de la note. + * + * @return date et heure de la note + */ public LocalDateTime getTimestamp() { return timestamp; } + /** + * Definit l'horodatage de la note. + * + * @param timestamp date et heure de la note + */ public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; } + /** + * Retourne le contenu de la note. + * + * @return contenu saisi par l'analyste + */ public String getContent() { return content; } + /** + * Definit le contenu de la note. + * + * @param content contenu saisi par l'analyste + */ public void setContent(String content) { this.content = content; } diff --git a/src/main/java/tg/cyberlabmanager/model/Lab.java b/src/main/java/tg/cyberlabmanager/model/Lab.java index 9e02ad8..2aac0df 100644 --- a/src/main/java/tg/cyberlabmanager/model/Lab.java +++ b/src/main/java/tg/cyberlabmanager/model/Lab.java @@ -4,6 +4,13 @@ import java.util.Collections; import java.util.List; +/** + * Entite racine representant un laboratoire thematique. + * + *

Ses attributs correspondent aux colonnes de la table {@code lab}. Les VM + * rattachees sont exposees en lecture seule et modifiees via les methodes + * d'agregation.

+ */ public class Lab { private int id; private String title; @@ -11,9 +18,20 @@ public class Lab { private String category; private final List virtualMachines = new ArrayList<>(); + /** + * Cree un laboratoire vide. + */ public Lab() { } + /** + * Cree un laboratoire avec ses champs principaux. + * + * @param id identifiant local du laboratoire + * @param title titre du laboratoire + * @param description description du laboratoire + * @param category categorie du laboratoire + */ public Lab(int id, String title, String description, String category) { this.id = id; this.title = title; @@ -21,46 +39,101 @@ public Lab(int id, String title, String description, String category) { this.category = category; } + /** + * Retourne l'identifiant local du laboratoire. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Definit l'identifiant local du laboratoire. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne le titre du laboratoire. + * + * @return titre du laboratoire + */ public String getTitle() { return title; } + /** + * Definit le titre du laboratoire. + * + * @param title titre du laboratoire + */ public void setTitle(String title) { this.title = title; } + /** + * Retourne la description du laboratoire. + * + * @return description du laboratoire + */ public String getDescription() { return description; } + /** + * Definit la description du laboratoire. + * + * @param description description du laboratoire + */ public void setDescription(String description) { this.description = description; } + /** + * Retourne la categorie du laboratoire. + * + * @return categorie du laboratoire + */ public String getCategory() { return category; } + /** + * Definit la categorie du laboratoire. + * + * @param category categorie du laboratoire + */ public void setCategory(String category) { this.category = category; } + /** + * Retourne les machines virtuelles rattachees au laboratoire. + * + * @return liste non modifiable des machines virtuelles + */ public List getVirtualMachines() { return Collections.unmodifiableList(virtualMachines); } + /** + * Rattache une machine virtuelle au laboratoire. + * + * @param virtualMachine machine virtuelle a ajouter + */ public void addVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.add(virtualMachine); } + /** + * Retire une machine virtuelle du laboratoire. + * + * @param virtualMachine machine virtuelle a retirer + */ public void removeVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.remove(virtualMachine); } diff --git a/src/main/java/tg/cyberlabmanager/model/Snapshot.java b/src/main/java/tg/cyberlabmanager/model/Snapshot.java index 03eed48..8b8c396 100644 --- a/src/main/java/tg/cyberlabmanager/model/Snapshot.java +++ b/src/main/java/tg/cyberlabmanager/model/Snapshot.java @@ -2,6 +2,13 @@ import java.time.LocalDateTime; +/** + * Instantane VirtualBox rattache a une machine virtuelle. + * + *

Le modele conserve l'identite locale en base, l'UUID VirtualBox, les + * metadonnees d'affichage et l'information indiquant si l'instantane a ete pris + * pendant que la VM etait en execution.

+ */ public class Snapshot { private int id; private String uuid; @@ -10,50 +17,116 @@ public class Snapshot { private String description; private boolean online; + /** + * Cree un snapshot vide. + */ + public Snapshot() { + } + + /** + * Retourne l'identifiant local du snapshot. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Definit l'identifiant local du snapshot. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne l'UUID VirtualBox du snapshot. + * + * @return UUID VirtualBox + */ public String getUuid() { return uuid; } + /** + * Definit l'UUID VirtualBox du snapshot. + * + * @param uuid UUID VirtualBox + */ public void setUuid(String uuid) { this.uuid = uuid; } + /** + * Retourne le nom du snapshot. + * + * @return nom du snapshot + */ public String getName() { return name; } + /** + * Definit le nom du snapshot. + * + * @param name nom du snapshot + */ public void setName(String name) { this.name = name; } + /** + * Retourne la date de creation du snapshot. + * + * @return date de creation + */ public LocalDateTime getCreatedAt() { return createdAt; } + /** + * Definit la date de creation du snapshot. + * + * @param createdAt date de creation + */ public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + /** + * Retourne la description du snapshot. + * + * @return description du snapshot + */ public String getDescription() { return description; } + /** + * Definit la description du snapshot. + * + * @param description description du snapshot + */ public void setDescription(String description) { this.description = description; } + /** + * Indique si le snapshot a ete pris VM allumee. + * + * @return {@code true} si le snapshot inclut l'etat en execution + */ public boolean isOnline() { return online; } + /** + * Definit si le snapshot a ete pris VM allumee. + * + * @param online {@code true} si le snapshot inclut l'etat en execution + */ public void setOnline(boolean online) { this.online = online; } diff --git a/src/main/java/tg/cyberlabmanager/model/VMStatus.java b/src/main/java/tg/cyberlabmanager/model/VMStatus.java index b6a8744..b014192 100644 --- a/src/main/java/tg/cyberlabmanager/model/VMStatus.java +++ b/src/main/java/tg/cyberlabmanager/model/VMStatus.java @@ -1,9 +1,17 @@ package tg.cyberlabmanager.model; +/** + * Etats stables d'une machine virtuelle tels qu'exposes par l'hyperviseur. + */ public enum VMStatus { + /** La VM est en cours d'execution. */ RUNNING, + /** La VM est eteinte. */ POWERED_OFF, + /** La VM est dans un etat sauvegarde. */ SAVED, + /** La VM est en pause. */ PAUSED, + /** Le statut de la VM n'a pas pu etre determine. */ UNKNOWN } diff --git a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java index f031a89..18d8f75 100644 --- a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java +++ b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java @@ -4,6 +4,13 @@ import java.util.Collections; import java.util.List; +/** + * Machine virtuelle importee depuis VirtualBox. + * + *

L'UUID VirtualBox fournit une reference stable. Les snapshots et notes + * d'analyse sont encapsules pour eviter l'exposition directe des listes + * internes.

+ */ public class VirtualMachine { private int id; private String name; @@ -13,59 +20,135 @@ public class VirtualMachine { private final List snapshots = new ArrayList<>(); private final List journalEntries = new ArrayList<>(); + /** + * Cree une machine virtuelle vide. + */ + public VirtualMachine() { + } + + /** + * Retourne l'identifiant local de la VM. + * + * @return identifiant local + */ public int getId() { return id; } + /** + * Definit l'identifiant local de la VM. + * + * @param id identifiant local + */ public void setId(int id) { this.id = id; } + /** + * Retourne le nom de la VM. + * + * @return nom de la VM + */ public String getName() { return name; } + /** + * Definit le nom de la VM. + * + * @param name nom de la VM + */ public void setName(String name) { this.name = name; } + /** + * Retourne l'UUID VirtualBox de la VM. + * + * @return UUID VirtualBox + */ public String getUuid() { return uuid; } + /** + * Definit l'UUID VirtualBox de la VM. + * + * @param uuid UUID VirtualBox + */ public void setUuid(String uuid) { this.uuid = uuid; } + /** + * Retourne l'URL de documentation associee a la VM. + * + * @return URL de documentation + */ public String getDocumentationUrl() { return documentationUrl; } + /** + * Definit l'URL de documentation associee a la VM. + * + * @param documentationUrl URL de documentation + */ public void setDocumentationUrl(String documentationUrl) { this.documentationUrl = documentationUrl; } + /** + * Retourne le statut courant de la VM. + * + * @return statut de la VM + */ public VMStatus getStatus() { return status; } + /** + * Definit le statut courant de la VM. + * + * @param status statut de la VM + */ public void setStatus(VMStatus status) { this.status = status; } + /** + * Retourne les snapshots rattaches a la VM. + * + * @return liste non modifiable des snapshots + */ public List getSnapshots() { return Collections.unmodifiableList(snapshots); } + /** + * Ajoute un snapshot a la VM. + * + * @param snapshot snapshot a ajouter + */ public void addSnapshot(Snapshot snapshot) { snapshots.add(snapshot); } + /** + * Retourne les notes d'analyse rattachees a la VM. + * + * @return liste non modifiable des notes d'analyse + */ public List getJournalEntries() { return Collections.unmodifiableList(journalEntries); } - public void addJournalEntry(JournalEntry journalEntry) { + /** + * Ajoute une note d'analyse a la VM. + * + * @param journalEntry note d'analyse a ajouter + */ + public void addJournalEntry(JournalEntry journalEntry) { journalEntries.add(journalEntry); } } diff --git a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java index 5c670d7..2ffbcb6 100644 --- a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java +++ b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java @@ -8,7 +8,26 @@ import java.io.IOException; import java.util.List; +/** + * Service responsable de l'export d'un laboratoire au format PDF. + */ public class PdfExporter { + /** + * Cree un exporteur PDF. + */ + public PdfExporter() { + } + + /** + * Exporte les donnees d'un laboratoire dans un fichier PDF. + * + * @param lab laboratoire exporte + * @param vms machines virtuelles du laboratoire + * @param snapshots snapshots a inclure + * @param journals notes d'analyse a inclure + * @param filePath chemin du fichier PDF cible + * @throws IOException si l'ecriture du PDF echoue + */ public void exportLabToPdf( Lab lab, List vms, diff --git a/src/main/java/tg/cyberlabmanager/ui/LabListView.java b/src/main/java/tg/cyberlabmanager/ui/LabListView.java index 7a10e42..8e06fb6 100644 --- a/src/main/java/tg/cyberlabmanager/ui/LabListView.java +++ b/src/main/java/tg/cyberlabmanager/ui/LabListView.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue chargee d'afficher la liste des laboratoires. + */ public class LabListView { + /** + * Cree la vue de liste des laboratoires. + */ + public LabListView() { + } } diff --git a/src/main/java/tg/cyberlabmanager/ui/MainView.java b/src/main/java/tg/cyberlabmanager/ui/MainView.java index 0e763a1..b38c414 100644 --- a/src/main/java/tg/cyberlabmanager/ui/MainView.java +++ b/src/main/java/tg/cyberlabmanager/ui/MainView.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue principale de l'application. + */ public class MainView { + /** + * Cree la vue principale. + */ + public MainView() { + } } diff --git a/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java b/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java index 49b4eea..e0692bc 100644 --- a/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java +++ b/src/main/java/tg/cyberlabmanager/ui/VmControlPanel.java @@ -1,4 +1,12 @@ package tg.cyberlabmanager.ui; +/** + * Vue chargee des controles utilisateur pour les machines virtuelles. + */ public class VmControlPanel { + /** + * Cree le panneau de controle des machines virtuelles. + */ + public VmControlPanel() { + } } From daaddcb8aecf0b69ab795427a5e2b2b69315746f Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Fri, 29 May 2026 20:25:53 +0000 Subject: [PATCH 05/27] Add unit tests for model classes --- .../java/tg/cyberlabmanager/model/.gitkeep | 1 - .../cyberlabmanager/model/AppConfigTest.java | 18 ++++++ .../cyberlabmanager/model/AuditEntryTest.java | 51 +++++++++++++++ .../model/JournalEntryTest.java | 23 +++++++ .../tg/cyberlabmanager/model/LabTest.java | 64 +++++++++++++++++++ .../cyberlabmanager/model/SnapshotTest.java | 30 +++++++++ .../cyberlabmanager/model/VMStatusTest.java | 18 ++++++ .../model/VirtualMachineTest.java | 61 ++++++++++++++++++ 8 files changed, 265 insertions(+), 1 deletion(-) delete mode 100644 src/test/java/tg/cyberlabmanager/model/.gitkeep create mode 100644 src/test/java/tg/cyberlabmanager/model/AppConfigTest.java create mode 100644 src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java create mode 100644 src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java create mode 100644 src/test/java/tg/cyberlabmanager/model/LabTest.java create mode 100644 src/test/java/tg/cyberlabmanager/model/SnapshotTest.java create mode 100644 src/test/java/tg/cyberlabmanager/model/VMStatusTest.java create mode 100644 src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java diff --git a/src/test/java/tg/cyberlabmanager/model/.gitkeep b/src/test/java/tg/cyberlabmanager/model/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/src/test/java/tg/cyberlabmanager/model/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java b/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java new file mode 100644 index 0000000..5b24c55 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/AppConfigTest.java @@ -0,0 +1,18 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AppConfigTest { + @Test + void shouldStoreConfigurationPaths() { + AppConfig config = new AppConfig(); + + config.setVboxManagePath("/usr/bin/VBoxManage"); + config.setPdfExportDirectory("/tmp/reports"); + + assertEquals("/usr/bin/VBoxManage", config.getVboxManagePath()); + assertEquals("/tmp/reports", config.getPdfExportDirectory()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java b/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java new file mode 100644 index 0000000..493d86a --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/AuditEntryTest.java @@ -0,0 +1,51 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AuditEntryTest { + @Test + void shouldStoreAuditFields() { + LocalDateTime timestamp = LocalDateTime.of(2026, 5, 29, 20, 30); + AuditEntry entry = new AuditEntry(); + + entry.setId(7); + entry.setTimestamp(timestamp); + entry.setVmId(12); + entry.setAction("START"); + entry.setDetails("VM started from UI"); + entry.setLabName("Malware Lab"); + + assertEquals(7, entry.getId()); + assertEquals(timestamp, entry.getTimestamp()); + assertEquals(12, entry.getVmId()); + assertEquals("START", entry.getAction()); + assertEquals("VM started from UI", entry.getDetails()); + assertEquals("Malware Lab", entry.getLabName()); + } + + @Test + void shouldBuildReadableLogLine() { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.of(2026, 5, 29, 20, 30)); + entry.setVmId(12); + entry.setAction("SNAPSHOT_TAKEN"); + entry.setLabName("Pentest Lab"); + entry.setDetails("Snapshot baseline created"); + + assertEquals( + "2026-05-29T20:30:00 | action=SNAPSHOT_TAKEN | vmId=12 | lab=Pentest Lab | details=Snapshot baseline created", + entry.getLog() + ); + } + + @Test + void shouldBuildFallbackLogWhenOptionalFieldsAreMissing() { + AuditEntry entry = new AuditEntry(); + + assertEquals("unknown-time | action=UNKNOWN", entry.getLog()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java b/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java new file mode 100644 index 0000000..1abd563 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/JournalEntryTest.java @@ -0,0 +1,23 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JournalEntryTest { + @Test + void shouldStoreJournalEntryFields() { + LocalDateTime timestamp = LocalDateTime.of(2026, 5, 29, 21, 15); + JournalEntry entry = new JournalEntry(); + + entry.setId(3); + entry.setTimestamp(timestamp); + entry.setContent("Observed suspicious network traffic."); + + assertEquals(3, entry.getId()); + assertEquals(timestamp, entry.getTimestamp()); + assertEquals("Observed suspicious network traffic.", entry.getContent()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/LabTest.java b/src/test/java/tg/cyberlabmanager/model/LabTest.java new file mode 100644 index 0000000..e8d5bc8 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/LabTest.java @@ -0,0 +1,64 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LabTest { + @Test + void shouldCreateLabWithConstructorValues() { + Lab lab = new Lab(1, "Malware Analysis", "Static and dynamic malware lab", "malware"); + + assertEquals(1, lab.getId()); + assertEquals("Malware Analysis", lab.getTitle()); + assertEquals("Static and dynamic malware lab", lab.getDescription()); + assertEquals("malware", lab.getCategory()); + } + + @Test + void shouldUpdateLabFieldsWithSetters() { + Lab lab = new Lab(); + + lab.setId(2); + lab.setTitle("Pentest"); + lab.setDescription("Web attack lab"); + lab.setCategory("pentest"); + + assertEquals(2, lab.getId()); + assertEquals("Pentest", lab.getTitle()); + assertEquals("Web attack lab", lab.getDescription()); + assertEquals("pentest", lab.getCategory()); + } + + @Test + void shouldAddAndRemoveVirtualMachines() { + Lab lab = new Lab(); + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); + + lab.addVirtualMachine(vm); + + List virtualMachines = lab.getVirtualMachines(); + assertEquals(1, virtualMachines.size()); + assertSame(vm, virtualMachines.get(0)); + + lab.removeVirtualMachine(vm); + + assertTrue(lab.getVirtualMachines().isEmpty()); + } + + @Test + void shouldExposeUnmodifiableVirtualMachineList() { + Lab lab = new Lab(); + + assertThrows( + UnsupportedOperationException.class, + () -> lab.getVirtualMachines().add(new VirtualMachine()) + ); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java b/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java new file mode 100644 index 0000000..9501f63 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/SnapshotTest.java @@ -0,0 +1,30 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SnapshotTest { + @Test + void shouldStoreSnapshotFields() { + LocalDateTime createdAt = LocalDateTime.of(2026, 5, 29, 22, 0); + Snapshot snapshot = new Snapshot(); + + snapshot.setId(4); + snapshot.setUuid("snap-uuid-123"); + snapshot.setName("baseline"); + snapshot.setCreatedAt(createdAt); + snapshot.setDescription("Clean VM state"); + snapshot.setOnline(true); + + assertEquals(4, snapshot.getId()); + assertEquals("snap-uuid-123", snapshot.getUuid()); + assertEquals("baseline", snapshot.getName()); + assertEquals(createdAt, snapshot.getCreatedAt()); + assertEquals("Clean VM state", snapshot.getDescription()); + assertTrue(snapshot.isOnline()); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java b/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java new file mode 100644 index 0000000..5f86ffe --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/VMStatusTest.java @@ -0,0 +1,18 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +class VMStatusTest { + @Test + void shouldExposeExpectedStableStatuses() { + assertEquals(5, VMStatus.values().length); + assertSame(VMStatus.RUNNING, VMStatus.valueOf("RUNNING")); + assertSame(VMStatus.POWERED_OFF, VMStatus.valueOf("POWERED_OFF")); + assertSame(VMStatus.SAVED, VMStatus.valueOf("SAVED")); + assertSame(VMStatus.PAUSED, VMStatus.valueOf("PAUSED")); + assertSame(VMStatus.UNKNOWN, VMStatus.valueOf("UNKNOWN")); + } +} diff --git a/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java b/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java new file mode 100644 index 0000000..c986258 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/model/VirtualMachineTest.java @@ -0,0 +1,61 @@ +package tg.cyberlabmanager.model; + +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VirtualMachineTest { + @Test + void shouldUseUnknownStatusByDefault() { + VirtualMachine vm = new VirtualMachine(); + + assertSame(VMStatus.UNKNOWN, vm.getStatus()); + } + + @Test + void shouldStoreVirtualMachineFields() { + VirtualMachine vm = new VirtualMachine(); + + vm.setId(5); + vm.setName("Ubuntu Lab"); + vm.setUuid("vm-uuid-456"); + vm.setDocumentationUrl("https://example.test/doc"); + vm.setStatus(VMStatus.RUNNING); + + assertEquals(5, vm.getId()); + assertEquals("Ubuntu Lab", vm.getName()); + assertEquals("vm-uuid-456", vm.getUuid()); + assertEquals("https://example.test/doc", vm.getDocumentationUrl()); + assertSame(VMStatus.RUNNING, vm.getStatus()); + } + + @Test + void shouldAddSnapshotsAndJournalEntries() { + VirtualMachine vm = new VirtualMachine(); + Snapshot snapshot = new Snapshot(); + JournalEntry journalEntry = new JournalEntry(); + + vm.addSnapshot(snapshot); + vm.addJournalEntry(journalEntry); + + List snapshots = vm.getSnapshots(); + List journalEntries = vm.getJournalEntries(); + + assertEquals(1, snapshots.size()); + assertSame(snapshot, snapshots.get(0)); + assertEquals(1, journalEntries.size()); + assertSame(journalEntry, journalEntries.get(0)); + } + + @Test + void shouldExposeUnmodifiableCollections() { + VirtualMachine vm = new VirtualMachine(); + + assertThrows(UnsupportedOperationException.class, () -> vm.getSnapshots().add(new Snapshot())); + assertThrows(UnsupportedOperationException.class, () -> vm.getJournalEntries().add(new JournalEntry())); + } +} From 9d130aa38bcbf5a3366a59f2e92759b9bc479657 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Sat, 30 May 2026 02:09:29 +0000 Subject: [PATCH 06/27] Corrections mineures orthographe du JavaDoc --- .../tg/cyberlabmanager/model/AppConfig.java | 20 ++++----- .../tg/cyberlabmanager/model/AuditEntry.java | 42 +++++++++---------- .../cyberlabmanager/model/JournalEntry.java | 14 +++---- .../java/tg/cyberlabmanager/model/Lab.java | 32 +++++++------- .../tg/cyberlabmanager/model/Snapshot.java | 34 +++++++-------- .../tg/cyberlabmanager/model/VMStatus.java | 10 ++--- .../cyberlabmanager/model/VirtualMachine.java | 34 +++++++-------- 7 files changed, 93 insertions(+), 93 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/model/AppConfig.java b/src/main/java/tg/cyberlabmanager/model/AppConfig.java index 04d3f60..2a3fa9a 100644 --- a/src/main/java/tg/cyberlabmanager/model/AppConfig.java +++ b/src/main/java/tg/cyberlabmanager/model/AppConfig.java @@ -3,30 +3,30 @@ /** * Configuration persistante de l'application. * - *

Elle contient les chemins utilises par les services techniques, notamment - * VBoxManage et le repertoire d'export PDF.

+ *

Elle contient les chemins utilisés par les services techniques, notamment + * VBoxManage et le répertoire d'export PDF.

*/ public class AppConfig { private String vboxManagePath; private String pdfExportDirectory; /** - * Cree une configuration vide. + * Crée une configuration vide. */ public AppConfig() { } /** - * Retourne le chemin de l'executable VBoxManage. + * Retourne le chemin de l'exécutable VBoxManage. * - * @return chemin configure vers VBoxManage + * @return chemin configuré vers VBoxManage */ public String getVboxManagePath() { return vboxManagePath; } /** - * Definit le chemin de l'executable VBoxManage. + * Définit le chemin de l'exécutable VBoxManage. * * @param vboxManagePath chemin vers VBoxManage */ @@ -35,18 +35,18 @@ public void setVboxManagePath(String vboxManagePath) { } /** - * Retourne le repertoire d'export des rapports PDF. + * Retourne le répertoire d'export des rapports PDF. * - * @return repertoire d'export PDF + * @return répertoire d'export PDF */ public String getPdfExportDirectory() { return pdfExportDirectory; } /** - * Definit le repertoire d'export des rapports PDF. + * Définit le répertoire d'export des rapports PDF. * - * @param pdfExportDirectory repertoire d'export PDF + * @param pdfExportDirectory répertoire d'export PDF */ public void setPdfExportDirectory(String pdfExportDirectory) { this.pdfExportDirectory = pdfExportDirectory; diff --git a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java index 03a8ec2..7c87fea 100644 --- a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java @@ -4,10 +4,10 @@ import java.time.format.DateTimeFormatter; /** - * Trace automatique d'une action de controle effectuee sur une machine virtuelle. + * Trace automatique d'une action de contrôle effectuée sur une machine virtuelle. * - *

Cette entite est separee de {@link VirtualMachine} afin de conserver un - * historique potentiellement volumineux sans alourdir le modele de la VM.

+ *

Cette entité est séparée de {@link VirtualMachine} afin de conserver un + * historique potentiellement volumineux sans alourdir le modèle de la VM.

*/ public class AuditEntry { private static final DateTimeFormatter LOG_TIMESTAMP_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; @@ -20,13 +20,13 @@ public class AuditEntry { private String labName; /** - * Cree une entree d'audit vide. + * Crée une entrée d'audit vide. */ public AuditEntry() { } /** - * Retourne l'identifiant local de l'entree d'audit. + * Retourne l'identifiant local de l'entrée d'audit. * * @return identifiant local */ @@ -35,7 +35,7 @@ public int getId() { } /** - * Definit l'identifiant local de l'entree d'audit. + * Définit l'identifiant local de l'entrée d'audit. * * @param id identifiant local */ @@ -53,7 +53,7 @@ public LocalDateTime getTimestamp() { } /** - * Definit l'horodatage de l'action. + * Définit l'horodatage de l'action. * * @param timestamp date et heure de l'action */ @@ -62,16 +62,16 @@ public void setTimestamp(LocalDateTime timestamp) { } /** - * Retourne l'identifiant local de la VM concernee. + * Retourne l'identifiant local de la VM concernée. * - * @return identifiant de la VM, ou {@code null} si aucune VM n'est liee + * @return identifiant de la VM, ou {@code null} si aucune VM n'est liée */ public Integer getVmId() { return vmId; } /** - * Definit l'identifiant local de la VM concernee. + * Définit l'identifiant local de la VM concernée. * * @param vmId identifiant de la VM, ou {@code null} */ @@ -80,43 +80,43 @@ public void setVmId(Integer vmId) { } /** - * Retourne l'action auditee. + * Retourne l'action auditée. * - * @return action auditee + * @return action auditée */ public String getAction() { return action; } /** - * Definit l'action auditee. + * Définit l'action auditée. * - * @param action action auditee + * @param action action auditée */ public void setAction(String action) { this.action = action; } /** - * Retourne les details de l'action. + * Retourne les détails de l'action. * - * @return details de l'action + * @return détails de l'action */ public String getDetails() { return details; } /** - * Definit les details de l'action. + * Définit les détails de l'action. * - * @param details details de l'action + * @param details détails de l'action */ public void setDetails(String details) { this.details = details; } /** - * Retourne le nom du laboratoire concerne. + * Retourne le nom du laboratoire concerné. * * @return nom du laboratoire */ @@ -125,7 +125,7 @@ public String getLabName() { } /** - * Definit le nom du laboratoire concerne. + * Définit le nom du laboratoire concerné. * * @param labName nom du laboratoire */ @@ -134,7 +134,7 @@ public void setLabName(String labName) { } /** - * Construit une representation textuelle de l'entree d'audit. + * Construit une représentation textuelle de l'entrée d'audit. * * @return ligne de log lisible */ diff --git a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java index 76fca92..d2287ad 100644 --- a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java @@ -3,9 +3,9 @@ import java.time.LocalDateTime; /** - * Note d'analyse horodatee saisie par l'analyste pour une machine virtuelle. + * Note d'analyse horodatée saisie par l'analyste pour une machine virtuelle. * - *

Une entree de journal est rattachee au cycle de vie logique d'une + *

Une entrée de journal est rattachée au cycle de vie logique d'une * {@link VirtualMachine}.

*/ public class JournalEntry { @@ -14,13 +14,13 @@ public class JournalEntry { private String content; /** - * Cree une entree de journal vide. + * Crée une entrée de journal vide. */ public JournalEntry() { } /** - * Retourne l'identifiant local de l'entree de journal. + * Retourne l'identifiant local de l'entrée de journal. * * @return identifiant local */ @@ -29,7 +29,7 @@ public int getId() { } /** - * Definit l'identifiant local de l'entree de journal. + * Définit l'identifiant local de l'entrée de journal. * * @param id identifiant local */ @@ -47,7 +47,7 @@ public LocalDateTime getTimestamp() { } /** - * Definit l'horodatage de la note. + * Définit l'horodatage de la note. * * @param timestamp date et heure de la note */ @@ -65,7 +65,7 @@ public String getContent() { } /** - * Definit le contenu de la note. + * Définit le contenu de la note. * * @param content contenu saisi par l'analyste */ diff --git a/src/main/java/tg/cyberlabmanager/model/Lab.java b/src/main/java/tg/cyberlabmanager/model/Lab.java index 2aac0df..8f82900 100644 --- a/src/main/java/tg/cyberlabmanager/model/Lab.java +++ b/src/main/java/tg/cyberlabmanager/model/Lab.java @@ -5,11 +5,11 @@ import java.util.List; /** - * Entite racine representant un laboratoire thematique. + * Entité racine représentant un laboratoire thématique. * *

Ses attributs correspondent aux colonnes de la table {@code lab}. Les VM - * rattachees sont exposees en lecture seule et modifiees via les methodes - * d'agregation.

+ * rattachées sont exposées en lecture seule et modifiées via les méthodes + * d'agrégation.

*/ public class Lab { private int id; @@ -19,18 +19,18 @@ public class Lab { private final List virtualMachines = new ArrayList<>(); /** - * Cree un laboratoire vide. + * Crée un laboratoire vide. */ public Lab() { } /** - * Cree un laboratoire avec ses champs principaux. + * Crée un laboratoire avec ses champs principaux. * * @param id identifiant local du laboratoire * @param title titre du laboratoire * @param description description du laboratoire - * @param category categorie du laboratoire + * @param category catégorie du laboratoire */ public Lab(int id, String title, String description, String category) { this.id = id; @@ -49,7 +49,7 @@ public int getId() { } /** - * Definit l'identifiant local du laboratoire. + * Définit l'identifiant local du laboratoire. * * @param id identifiant local */ @@ -67,7 +67,7 @@ public String getTitle() { } /** - * Definit le titre du laboratoire. + * Définit le titre du laboratoire. * * @param title titre du laboratoire */ @@ -85,7 +85,7 @@ public String getDescription() { } /** - * Definit la description du laboratoire. + * Définit la description du laboratoire. * * @param description description du laboratoire */ @@ -94,25 +94,25 @@ public void setDescription(String description) { } /** - * Retourne la categorie du laboratoire. + * Retourne la catégorie du laboratoire. * - * @return categorie du laboratoire + * @return catégorie du laboratoire */ public String getCategory() { return category; } /** - * Definit la categorie du laboratoire. + * Définit la catégorie du laboratoire. * - * @param category categorie du laboratoire + * @param category catégorie du laboratoire */ public void setCategory(String category) { this.category = category; } /** - * Retourne les machines virtuelles rattachees au laboratoire. + * Retourne les machines virtuelles rattachées au laboratoire. * * @return liste non modifiable des machines virtuelles */ @@ -123,7 +123,7 @@ public List getVirtualMachines() { /** * Rattache une machine virtuelle au laboratoire. * - * @param virtualMachine machine virtuelle a ajouter + * @param virtualMachine machine virtuelle à ajouter */ public void addVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.add(virtualMachine); @@ -132,7 +132,7 @@ public void addVirtualMachine(VirtualMachine virtualMachine) { /** * Retire une machine virtuelle du laboratoire. * - * @param virtualMachine machine virtuelle a retirer + * @param virtualMachine machine virtuelle à retirer */ public void removeVirtualMachine(VirtualMachine virtualMachine) { virtualMachines.remove(virtualMachine); diff --git a/src/main/java/tg/cyberlabmanager/model/Snapshot.java b/src/main/java/tg/cyberlabmanager/model/Snapshot.java index 8b8c396..cf69b33 100644 --- a/src/main/java/tg/cyberlabmanager/model/Snapshot.java +++ b/src/main/java/tg/cyberlabmanager/model/Snapshot.java @@ -3,11 +3,11 @@ import java.time.LocalDateTime; /** - * Instantane VirtualBox rattache a une machine virtuelle. + * Instantané VirtualBox rattaché à une machine virtuelle. * - *

Le modele conserve l'identite locale en base, l'UUID VirtualBox, les - * metadonnees d'affichage et l'information indiquant si l'instantane a ete pris - * pendant que la VM etait en execution.

+ *

Le modèle conserve l'identité locale en base, l'UUID VirtualBox, les + * métadonnées d'affichage et l'information indiquant si l'instantané a été pris + * pendant que la VM était en exécution.

*/ public class Snapshot { private int id; @@ -18,7 +18,7 @@ public class Snapshot { private boolean online; /** - * Cree un snapshot vide. + * Crée un snapshot vide. */ public Snapshot() { } @@ -33,7 +33,7 @@ public int getId() { } /** - * Definit l'identifiant local du snapshot. + * Définit l'identifiant local du snapshot. * * @param id identifiant local */ @@ -51,7 +51,7 @@ public String getUuid() { } /** - * Definit l'UUID VirtualBox du snapshot. + * Définit l'UUID VirtualBox du snapshot. * * @param uuid UUID VirtualBox */ @@ -69,7 +69,7 @@ public String getName() { } /** - * Definit le nom du snapshot. + * Définit le nom du snapshot. * * @param name nom du snapshot */ @@ -78,18 +78,18 @@ public void setName(String name) { } /** - * Retourne la date de creation du snapshot. + * Retourne la date de création du snapshot. * - * @return date de creation + * @return date de création */ public LocalDateTime getCreatedAt() { return createdAt; } /** - * Definit la date de creation du snapshot. + * Définit la date de création du snapshot. * - * @param createdAt date de creation + * @param createdAt date de création */ public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; @@ -105,7 +105,7 @@ public String getDescription() { } /** - * Definit la description du snapshot. + * Définit la description du snapshot. * * @param description description du snapshot */ @@ -114,18 +114,18 @@ public void setDescription(String description) { } /** - * Indique si le snapshot a ete pris VM allumee. + * Indique si le snapshot a été pris VM allumée. * - * @return {@code true} si le snapshot inclut l'etat en execution + * @return {@code true} si le snapshot inclut l'état en exécution */ public boolean isOnline() { return online; } /** - * Definit si le snapshot a ete pris VM allumee. + * Définit si le snapshot a été pris VM allumée. * - * @param online {@code true} si le snapshot inclut l'etat en execution + * @param online {@code true} si le snapshot inclut l'état en exécution */ public void setOnline(boolean online) { this.online = online; diff --git a/src/main/java/tg/cyberlabmanager/model/VMStatus.java b/src/main/java/tg/cyberlabmanager/model/VMStatus.java index b014192..3b0130a 100644 --- a/src/main/java/tg/cyberlabmanager/model/VMStatus.java +++ b/src/main/java/tg/cyberlabmanager/model/VMStatus.java @@ -1,17 +1,17 @@ package tg.cyberlabmanager.model; /** - * Etats stables d'une machine virtuelle tels qu'exposes par l'hyperviseur. + * États stables d'une machine virtuelle tels qu'exposés par l'hyperviseur. */ public enum VMStatus { - /** La VM est en cours d'execution. */ + /** La VM est en cours d'exécution. */ RUNNING, - /** La VM est eteinte. */ + /** La VM est éteinte. */ POWERED_OFF, - /** La VM est dans un etat sauvegarde. */ + /** La VM est dans un état sauvegardé. */ SAVED, /** La VM est en pause. */ PAUSED, - /** Le statut de la VM n'a pas pu etre determine. */ + /** Le statut de la VM n'a pas pu être déterminé. */ UNKNOWN } diff --git a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java index 18d8f75..e8442cd 100644 --- a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java +++ b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java @@ -5,11 +5,11 @@ import java.util.List; /** - * Machine virtuelle importee depuis VirtualBox. + * Machine virtuelle importée depuis VirtualBox. * - *

L'UUID VirtualBox fournit une reference stable. Les snapshots et notes - * d'analyse sont encapsules pour eviter l'exposition directe des listes - * internes.

+ *

L'UUID VirtualBox fournit une référence stable. Les snapshots et notes + * d'analyse sont encapsulés pour éviter l'exposition directe des listes + * internes.

*/ public class VirtualMachine { private int id; @@ -21,7 +21,7 @@ public class VirtualMachine { private final List journalEntries = new ArrayList<>(); /** - * Cree une machine virtuelle vide. + * Crée une machine virtuelle vide. */ public VirtualMachine() { } @@ -36,7 +36,7 @@ public int getId() { } /** - * Definit l'identifiant local de la VM. + * Définit l'identifiant local de la VM. * * @param id identifiant local */ @@ -54,7 +54,7 @@ public String getName() { } /** - * Definit le nom de la VM. + * Définit le nom de la VM. * * @param name nom de la VM */ @@ -72,7 +72,7 @@ public String getUuid() { } /** - * Definit l'UUID VirtualBox de la VM. + * Définit l'UUID VirtualBox de la VM. * * @param uuid UUID VirtualBox */ @@ -81,7 +81,7 @@ public void setUuid(String uuid) { } /** - * Retourne l'URL de documentation associee a la VM. + * Retourne l'URL de documentation associée à la VM. * * @return URL de documentation */ @@ -90,7 +90,7 @@ public String getDocumentationUrl() { } /** - * Definit l'URL de documentation associee a la VM. + * Définit l'URL de documentation associée à la VM. * * @param documentationUrl URL de documentation */ @@ -108,7 +108,7 @@ public VMStatus getStatus() { } /** - * Definit le statut courant de la VM. + * Définit le statut courant de la VM. * * @param status statut de la VM */ @@ -117,7 +117,7 @@ public void setStatus(VMStatus status) { } /** - * Retourne les snapshots rattaches a la VM. + * Retourne les snapshots rattachés à la VM. * * @return liste non modifiable des snapshots */ @@ -126,16 +126,16 @@ public List getSnapshots() { } /** - * Ajoute un snapshot a la VM. + * Ajoute un snapshot à la VM. * - * @param snapshot snapshot a ajouter + * @param snapshot snapshot à ajouter */ public void addSnapshot(Snapshot snapshot) { snapshots.add(snapshot); } /** - * Retourne les notes d'analyse rattachees a la VM. + * Retourne les notes d'analyse rattachées à la VM. * * @return liste non modifiable des notes d'analyse */ @@ -144,9 +144,9 @@ public List getJournalEntries() { } /** - * Ajoute une note d'analyse a la VM. + * Ajoute une note d'analyse à la VM. * - * @param journalEntry note d'analyse a ajouter + * @param journalEntry note d'analyse à ajouter */ public void addJournalEntry(JournalEntry journalEntry) { journalEntries.add(journalEntry); From 38ac373c115035a6f067dff420262941ac4bc821 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Sat, 30 May 2026 14:49:12 +0000 Subject: [PATCH 07/27] feat(ui): validate final mockup & add data stubs --- docs/Images/mockup-final.png | Bin 0 -> 147376 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/Images/mockup-final.png diff --git a/docs/Images/mockup-final.png b/docs/Images/mockup-final.png new file mode 100644 index 0000000000000000000000000000000000000000..f57ca06abb19fd5e19cdba03609cbd4c7879a28c GIT binary patch literal 147376 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�~ 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 literal 0 HcmV?d00001 From 51ac4bdb50b709442dcc4b3d718d1d3d0023fc06 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Sat, 30 May 2026 15:00:12 +0000 Subject: [PATCH 08/27] chore: add CI workflow and PR template --- .github/workflows/ci.yml | 34 ++++++++++++++++++++++ .github/workflows/pull_request_template.md | 25 ++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pull_request_template.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d07d6d8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,34 @@ +name: CI + +on: + push: + branches: [ "main", "develop", "feature/**", "fix/**" ] + pull_request: + branches: [ "develop", "main" ] + +jobs: + build-and-test: + name: Build & Test (Java 17) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: maven # met le dépôt Maven en cache → builds 2x plus rapides + + - name: Build & run tests + run: ./mvnw test --no-transfer-progress + + - name: Upload Surefire reports on failure + if: failure() # s'exécute UNIQUEMENT si les tests échouent + uses: actions/upload-artifact@v4 + with: + name: surefire-reports + path: target/surefire-reports/ + retention-days: 7 diff --git a/.github/workflows/pull_request_template.md b/.github/workflows/pull_request_template.md new file mode 100644 index 0000000..b7bbc66 --- /dev/null +++ b/.github/workflows/pull_request_template.md @@ -0,0 +1,25 @@ +## Ce que fait ce MR + + + +## Couche concernée + +- [ ] `model` — entités et POJOs +- [ ] `hypervisor` — IHypervisor / VBoxOrchestrator +- [ ] `data` — DatabaseManager / schema SQL +- [ ] `pdf` — PdfExporter +- [ ] `controller` — LabController +- [ ] `ui` — vues JavaFX +- [ ] Config / build (pom.xml, module-info, CI) + +## Checklist avant review + +- [ ] `./mvnw test` passe en local (0 failure, 0 error) +- [ ] Javadoc présent sur toutes les classes et méthodes publiques ajoutées +- [ ] Pas de `System.out.println` de debug oublié +- [ ] Pas de TODO non intentionnel +- [ ] Les imports inutilisés ont été supprimés (IntelliJ : Ctrl+Alt+O) + +## Pour le reviewer + + From cac0d7bc6dbd80422abd6da80962958fda03668d Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Sat, 30 May 2026 15:16:44 +0000 Subject: [PATCH 09/27] fix: move PR template to correct location --- .github/{workflows => }/pull_request_template.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/{workflows => }/pull_request_template.md (100%) diff --git a/.github/workflows/pull_request_template.md b/.github/pull_request_template.md similarity index 100% rename from .github/workflows/pull_request_template.md rename to .github/pull_request_template.md From 4ab5d25f976a50a84cb9c652fcd1c931d1184515 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Sat, 30 May 2026 15:28:41 +0000 Subject: [PATCH 10/27] Add: Adding CONTRIBUTING to the repo --- CONTRIBUTING.md | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..baae740 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Guide de contribution — Cyber Lab Manager + +## Branches + +| Préfixe | Usage | Exemple | +|---|---|---| +| `feature/` | Nouvelle fonctionnalité | `feature/vbox-orchestrator` | +| `fix/` | Correction de bug | `fix/snapshot-uuid-null` | +| `chore/` | Config, CI, docs | `chore/github-config` | +| `test/` | Ajout ou correction de tests seuls | `test/hypervisor-coverage` | + +**Règle absolue :** on ne pousse jamais directement sur `develop` ou `main`. +Toujours passer par un MR depuis une branche `feature/` ou `fix/`. + +## Format des commits + +On suit [Conventional Commits](https://www.conventionalcommits.org/fr) : + +``` +(): +``` + +| Type | Quand l'utiliser | +|---|---| +| `feat` | Ajout d'une fonctionnalité | +| `fix` | Correction d'un bug | +| `test` | Ajout ou modification de tests | +| `docs` | Documentation (Javadoc, Markdown) | +| `refactor` | Refactoring sans changement de comportement | +| `chore` | Config build, CI, dépendances | + +**Exemples :** +``` +feat(model): add Snapshot uuid and online fields +fix(hypervisor): handle VBoxManage non-zero exit code +test(model): add LabTest unmodifiable list assertion +docs(hypervisor): complete Javadoc on IHypervisor +chore: add GitHub Actions CI workflow +``` + +## Ouvrir un MR + +1. Ta branche est à jour avec `develop` (`git rebase develop` ou `git merge develop`) +2. `./mvnw test` passe en local — **0 failure, 0 error** +3. Javadoc présent sur toutes les classes et méthodes publiques ajoutées +4. Ouvre le MR vers `develop` sur GitHub +5. Remplis le template de MR (pré-rempli automatiquement) +6. Assigne au moins un reviewer + +## Revue de code + +- Le reviewer a **48h** pour donner son retour +- Un commentaire `[bloquant]` doit être résolu avant le merge +- Un commentaire `[suggestion]` est optionnel +- Approuver = confirmer que les tests passent ET que le code est lisible + +## Standards de code + +- **Java 17**, style standard IntelliJ (Ctrl+Alt+L pour formatter) +- Javadoc obligatoire sur toutes les classes et méthodes `public` +- Pas de `System.out.println` → utiliser `java.util.logging.Logger` +- Imports inutilisés supprimés (Ctrl+Alt+O dans IntelliJ) +- Pas de TODO non documenté dans un MR + +## Responsabilités par package + +| Package | Responsable | +|---|---| +| `tg.cyberlabmanager.model` | Rôle 3 | +| `tg.cyberlabmanager.hypervisor` | Rôle 3 | +| `tg.cyberlabmanager.data` | Rôle 2 | +| `tg.cyberlabmanager.pdf` | Rôle 2 | +| `tg.cyberlabmanager.ui` | Rôle 1 | +| `tg.cyberlabmanager.controller` | Rôle 1 | From 68d90c3da3a6f6d5e3b70e6e6125c86363bfa31c Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Sat, 30 May 2026 15:30:35 +0000 Subject: [PATCH 11/27] chore: untrack target directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index eef09ab..a6268c3 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ build/ ### Mac OS ### .DS_Store +target/ From 6d2a438f8f940286d1d084222675fe80f4627455 Mon Sep 17 00:00:00 2001 From: TheSOCAnalyst Date: Sat, 30 May 2026 15:54:59 +0000 Subject: [PATCH 12/27] docs: add SETUP.md environment guide --- docs/Markdowns/SETUP.md | 89 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 docs/Markdowns/SETUP.md diff --git a/docs/Markdowns/SETUP.md b/docs/Markdowns/SETUP.md new file mode 100644 index 0000000..738100d --- /dev/null +++ b/docs/Markdowns/SETUP.md @@ -0,0 +1,89 @@ +# SETUP — Environnement de développement + +## Prérequis + +| Outil | Version minimale | Vérification | +|---|---|---| +| JDK | 17 (LTS) | `java -version` | +| Maven | 3.8+ | `./mvnw -version` | +| VirtualBox | 7.0+ | Pour les tests d'intégration | +| Git | 2.x | `git --version` | + +> Le projet utilise le Maven Wrapper (`./mvnw`) — pas besoin d'installer Maven globalement. + +--- + +## Configuration IDE — point critique à lire + +Le projet compile en **Java 17** même si tu as un JDK plus récent (21, 22…) sur ta machine. +C'est contrôlé par cette ligne dans `pom.xml` : + +```xml +17 +``` + +Cette propriété fixe simultanément la syntaxe acceptée, le bytecode cible (class file version 61) +et les APIs accessibles — le compilateur de ton JDK s'adapte automatiquement. + +### IntelliJ IDEA + +**File → Project Structure → Project** : +- SDK : ton JDK installé (17, 21, peu importe) +- Language level : **17** + +**File → Settings → Build → Compiler → Java Compiler** : +- Target bytecode version : **17** + +### Pourquoi c'est important + +Si quelqu'un compile avec bytecode Java 21 et pousse du code, les autres obtiendront : +``` +UnsupportedClassVersionError: Unsupported major.minor version 65.0 +``` +La version 65 = Java 21. La version 61 = Java 17 (notre cible). +Garder tout le monde sur la même cible évite ce type de conflit au merge. + +--- + +## Lancer le projet + +```bash +# Compiler et lancer tous les tests +./mvnw test + +# Lancer l'application JavaFX +./mvnw javafx:run + +# Générer la Javadoc dans target/reports/apidocs/ +./mvnw javadoc:javadoc +``` + +> Si tu utilises un JDK non standard, préfixe avec JAVA_HOME : +> `JAVA_HOME=/usr/lib/jvm/jdk-21.0.11-oracle-x64 ./mvnw test` + +--- + +## Structure des branches + +``` +main ← releases stables uniquement +develop ← intégration continue (branche de référence) +feature/x ← développement d'une fonctionnalité +fix/x ← correction de bug +chore/x ← config, CI, documentation +``` + +Voir [CONTRIBUTING.md](../CONTRIBUTING.md) pour les conventions complètes. + +--- + +## Responsabilités par package + +| Package | Rôle | Contenu | +|---|---|---| +| `tg.cyberlabmanager.model` | Rôle 3 | Entités métier (Lab, VM, Snapshot…) | +| `tg.cyberlabmanager.hypervisor` | Rôle 3 | IHypervisor, VBoxOrchestrator | +| `tg.cyberlabmanager.data` | Rôle 2 | DatabaseManager, SQLite | +| `tg.cyberlabmanager.pdf` | Rôle 2 | PdfExporter | +| `tg.cyberlabmanager.ui` | Rôle 1 | Vues JavaFX | +| `tg.cyberlabmanager.controller` | Rôle 1 | LabController | From 83f7deaea09acc87a84e18a342505c521e8a2cef Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Sat, 6 Jun 2026 15:43:14 +0000 Subject: [PATCH 13/27] feat(ui): sauvegarde avant merge role-3 --- .../tg/cyberlabmanager/app/CyberLabApp.java | 35 +++++- .../controller/LabController.java | 102 ++++++++++++------ 2 files changed, 100 insertions(+), 37 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java index 70b6866..a6f1f16 100644 --- a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java +++ b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java @@ -4,16 +4,41 @@ import javafx.fxml.FXMLLoader; import javafx.scene.Scene; import javafx.stage.Stage; +import tg.cyberlabmanager.controller.LabController; -import java.io.IOException; public class CyberLabApp extends Application { + + private LabController controller; + @Override - public void start(Stage stage) throws IOException { - FXMLLoader loader = new FXMLLoader(CyberLabApp.class.getResource("/tg/cyberlabmanager/ui/main-view.fxml")); - Scene scene = new Scene(loader.load(), 900, 600); + public void start(Stage stage) throws Exception { + + FXMLLoader loader = new FXMLLoader( + CyberLabApp.class.getResource("/tg/cyberlabmanager/ui/main-view.fxml") + ); + + Scene scene = new Scene(loader.load(), 1400, 820); + + controller = loader.getController(); + + stage.setTitle("Cyber Lab Manager"); + stage.setMinWidth(1100); + stage.setMinHeight(650); stage.setScene(scene); stage.show(); } -} + + + @Override + public void stop() { + if (controller != null) { + controller.shutdown(); + } + } + + public static void main(String[] args) { + launch(args); + } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 732d284..5de2a49 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -1,89 +1,127 @@ package tg.cyberlabmanager.controller; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Label; + import tg.cyberlabmanager.data.DatabaseManager; import tg.cyberlabmanager.hypervisor.IHypervisor; import tg.cyberlabmanager.model.AppConfig; import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.VirtualMachine; import tg.cyberlabmanager.pdf.PdfExporter; -import javafx.fxml.FXML; -import javafx.scene.control.Label; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; public class LabController { + private IHypervisor hypervisor; private DatabaseManager databaseManager; private PdfExporter pdfExporter; + private final ExecutorService executor = Executors.newCachedThreadPool(); + @FXML private Label statusLabel; + public LabController() { } - public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { + + + @FXML + private void initialize() { + if (statusLabel != null) { + statusLabel.setText("CyberLab Manager — Initialisation..."); + } + } + + + public void setDependencies(IHypervisor hypervisor, + DatabaseManager databaseManager, + PdfExporter pdfExporter) { this.hypervisor = hypervisor; this.databaseManager = databaseManager; this.pdfExporter = pdfExporter; } - @FXML - private void initialize() { + + public void initView() { if (statusLabel != null) { - statusLabel.setText("Cyber Lab Manager pret"); + statusLabel.setText("CyberLab Manager — Prêt"); } } - public void onStartVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + + public List getAllLabs() { + throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); } - public void onStopVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + public List getOrphanVMs() { + throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); } - public void onSaveStateClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + + public void onStartVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { + } - public void onTakeSnapshotClicked(VirtualMachine vm, String name, String description) { - throw new UnsupportedOperationException("Not implemented yet"); + + public void onStopVMClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { } - public void onRestoreSnapshotClicked(VirtualMachine vm, String snapshotName) { - throw new UnsupportedOperationException("Not implemented yet"); + + public void onSaveStateClicked(VirtualMachine vm, + Runnable onSuccess, + Consumer onError) { } - public void onAddJournalEntry(VirtualMachine vm, String text) { - throw new UnsupportedOperationException("Not implemented yet"); + + public void onTakeSnapshotClicked(VirtualMachine vm, + String name, + String description, + Runnable onSuccess, + Consumer onError) { } - public void onImportVMsClicked() { - throw new UnsupportedOperationException("Not implemented yet"); + + public void onRestoreSnapshotClicked(VirtualMachine vm, + String snapshotName, + Runnable onSuccess, + Consumer onError) { } - public void onImportConfirm(List selectedVMs, Integer labId) { - throw new UnsupportedOperationException("Not implemented yet"); + + public void onAddJournalEntry(VirtualMachine vm, String text) { } - public void onExportLabClicked(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + public void onImportVMsClicked(Consumer> onSuccess, + Consumer onError) { } - public void onExportOrphansClicked() { - throw new UnsupportedOperationException("Not implemented yet"); + public void onImportConfirm(List selectedVMs, Integer labId) { } - public List getAllLabs() { - return databaseManager.getAllLabs(); + public void onExportLabClicked(Lab lab, String filePath) { } - public List getOrphanVMs() { - return databaseManager.getOrphanVMs(); + public void onExportOrphansClicked(String filePath) { } public void onConfigUpdated(AppConfig newConfig) { - databaseManager.saveConfig(newConfig); } -} + + + + public void shutdown() { + executor.shutdownNow(); + } +} \ No newline at end of file From f77b570ac606fbba8e8ce712a98a5a2eeda722b6 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 11 Jun 2026 13:48:27 +0000 Subject: [PATCH 14/27] feat(ui): modifications LabController --- .../controller/LabController.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 5de2a49..bea63b1 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -120,6 +120,55 @@ public void onConfigUpdated(AppConfig newConfig) { } + // ───────────────────────────────────────────────────────────────────────── +// HANDLERS FXML — appelés par main-view.fxml via onAction="#xxx" +// Corps vides pour l'instant — à implémenter semaine 3 +// ───────────────────────────────────────────────────────────────────────── + +@FXML +private void handleImportVM() { + // TODO (S3) : ouvrir dialogue import VM +} + +@FXML +private void handleExportPDF() { + // TODO (S4) : ouvrir FileChooser + appeler onExportLabClicked() +} + +@FXML +private void handleStartVM() { + // TODO (S3) : appeler onStartVMClicked() +} + +@FXML +private void handleStopVM() { + // TODO (S3) : appeler onStopVMClicked() +} + +@FXML +private void handleSaveState() { + // TODO (S3) : appeler onSaveStateClicked() +} + +@FXML +private void handleTakeSnapshot() { + // TODO (S3) : appeler onTakeSnapshotClicked() +} + +@FXML +private void handleRestoreSnapshot() { + // TODO (S3) : appeler onRestoreSnapshotClicked() +} + +@FXML +private void handleAddJournalEntry() { + // TODO (S3) : lire journalInputField + appeler onAddJournalEntry() +} + +@FXML +private void handleToggleAuditLogs() { + // TODO (S3) : toggle visible/managed sur auditArea + rotation auditArrow +} public void shutdown() { executor.shutdownNow(); From eb159f90378c0ae7439fa8d4e9db2754e18cca81 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 25 Jun 2026 11:49:30 +0000 Subject: [PATCH 15/27] back --- src/main/java/module-info.java | 2 +- .../controller/LabController.java | 143 +----- .../tg/cyberlabmanager/ui/cyberlab.css | 457 ++++++++++++++++++ .../tg/cyberlabmanager/ui/main-view.fxml | 318 +++++++++++- 4 files changed, 774 insertions(+), 146 deletions(-) create mode 100644 src/main/resources/tg/cyberlabmanager/ui/cyberlab.css diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 930b3ae..74c3ae1 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -26,7 +26,7 @@ opens tg.cyberlabmanager.controller to javafx.fxml; opens tg.cyberlabmanager.ui to javafx.fxml; - // ── Exports publics ────────────────────────────────────────────────── + // ── Exports publics exports tg.cyberlabmanager.app; exports tg.cyberlabmanager.controller; exports tg.cyberlabmanager.model; diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 9bd9c44..d80fcda 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -1,6 +1,5 @@ package tg.cyberlabmanager.controller; -import javafx.application.Platform; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -16,9 +15,7 @@ import java.util.concurrent.Executors; import java.util.function.Consumer; -/** - * Controleur principal coordonnant l'interface, la persistance et l'hyperviseur. - */ + public class LabController { private IHypervisor hypervisor; @@ -29,12 +26,6 @@ public class LabController { @FXML private Label statusLabel; - -<<<<<<< HEAD - - public LabController() { - } - @FXML @@ -44,26 +35,16 @@ private void initialize() { } } + public LabController() { + } + public void setDependencies(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { -======= - /** - * Cree un controleur vide utilise par JavaFX lors du chargement FXML. - */ - public LabController() { } - - /** - * Cree un controleur avec ses dependances injectees. - * - * @param hypervisor service de pilotage de l'hyperviseur - * @param databaseManager service de persistance - * @param pdfExporter service d'export PDF - */ + public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { ->>>>>>> origin/develop this.hypervisor = hypervisor; this.databaseManager = databaseManager; this.pdfExporter = pdfExporter; @@ -76,115 +57,15 @@ public void initView() { } } -<<<<<<< HEAD -======= - /** - * Declenche le demarrage d'une machine virtuelle. - * - * @param vm machine virtuelle cible - */ - public void onStartVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Declenche l'arret d'une machine virtuelle. - * - * @param vm machine virtuelle cible - */ - public void onStopVMClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Declenche la sauvegarde d'etat d'une machine virtuelle. - * - * @param vm machine virtuelle cible - */ - public void onSaveStateClicked(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Declenche la creation d'un snapshot pour une machine virtuelle. - * - * @param vm machine virtuelle cible - * @param name nom du snapshot - * @param description description du snapshot - */ - public void onTakeSnapshotClicked(VirtualMachine vm, String name, String description) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Declenche la restauration d'un snapshot pour une machine virtuelle. - * - * @param vm machine virtuelle cible - * @param snapshotName nom ou identifiant du snapshot - */ - public void onRestoreSnapshotClicked(VirtualMachine vm, String snapshotName) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Ajoute une note d'analyse a une machine virtuelle. - * - * @param vm machine virtuelle cible - * @param text contenu de la note - */ - public void onAddJournalEntry(VirtualMachine vm, String text) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Declenche l'import des machines virtuelles disponibles. - */ - public void onImportVMsClicked() { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Confirme l'import de machines virtuelles selectionnees. - * - * @param selectedVMs machines virtuelles selectionnees - * @param labId identifiant du laboratoire cible, ou {@code null} - */ - public void onImportConfirm(List selectedVMs, Integer labId) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Declenche l'export PDF d'un laboratoire. - * - * @param lab laboratoire a exporter - */ - public void onExportLabClicked(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); - } - /** - * Declenche l'export des machines virtuelles sans laboratoire. - */ - public void onExportOrphansClicked() { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Retourne tous les laboratoires connus. - * - * @return liste des laboratoires - */ ->>>>>>> origin/develop + + public List getAllLabs() { throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); } - /** - * Retourne les machines virtuelles non rattachees a un laboratoire. - * - * @return liste des machines virtuelles orphelines - */ + public List getOrphanVMs() { throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); } @@ -225,15 +106,13 @@ public void onRestoreSnapshotClicked(VirtualMachine vm, public void onAddJournalEntry(VirtualMachine vm, String text) { + } public void onImportVMsClicked(Consumer> onSuccess, Consumer onError) { } - public void onImportConfirm(List selectedVMs, Integer labId) { - } - public void onExportLabClicked(Lab lab, String filePath) { } @@ -249,10 +128,6 @@ public void onConfigUpdated(AppConfig newConfig) { } - // ───────────────────────────────────────────────────────────────────────── -// HANDLERS FXML — appelés par main-view.fxml via onAction="#xxx" -// Corps vides pour l'instant — à implémenter semaine 3 -// ───────────────────────────────────────────────────────────────────────── @FXML private void handleImportVM() { diff --git a/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css new file mode 100644 index 0000000..0fe2552 --- /dev/null +++ b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css @@ -0,0 +1,457 @@ +/* +Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license +Click nbfs://nbhost/SystemFileSystem/Templates/Other/CascadeStyleSheet.css to edit this template +*/ +/* + Created on : 13 juin 2026, 18:34:24 + Author : fiodor +*/ + + +/* ── ROOT : fond global ──────────────────────────────────────────────── */ +.root { + -fx-background-color: #12151C; + -fx-font-family: "System"; +} + +/* ── SCROLL PANE : transparent partout ──────────────────────────────── */ +.scroll-pane { + -fx-background-color: transparent; + -fx-background: transparent; + -fx-border-color: transparent; + -fx-padding: 0; +} +.scroll-pane > .viewport { + -fx-background-color: transparent; +} +.scroll-pane .corner { + -fx-background-color: transparent; +} + +/* Scrollbars fines et discrètes */ +.scroll-bar:vertical, +.scroll-bar:horizontal { + -fx-background-color: #1A1E28; + -fx-pref-width: 6; + -fx-pref-height: 6; +} +.scroll-bar .thumb { + -fx-background-color: #2A2E3A; + -fx-background-radius: 3; +} +.scroll-bar .increment-button, +.scroll-bar .decrement-button { + -fx-background-color: transparent; + -fx-pref-width: 0; + -fx-pref-height: 0; +} + +/* ── BARRE DE TITRE ──────────────────────────────────────────────────── */ +.title-bar { + -fx-background-color: #0F131C; + -fx-border-color: #2A2E3A; + -fx-border-width: 0 0 1 0; + -fx-min-height: 50; + -fx-pref-height: 50; +} +.title-label { + -fx-text-fill: #E0E4F0; + -fx-font-family: "Monospaced"; + -fx-font-size: 16; + -fx-font-weight: bold; +} + +/* ── PANNEAU GAUCHE ──────────────────────────────────────────────────── */ +.left-panel { + -fx-background-color: #1A1E28; + -fx-border-color: #2A2E3A; + -fx-border-width: 0 1 0 0; + -fx-min-width: 240; + -fx-pref-width: 280; + -fx-max-width: 320; +} +.section-header { + -fx-background-color: #1A1E28; + -fx-border-color: #2A2E3A; + -fx-border-width: 0 0 1 0; + -fx-min-height: 40; + -fx-pref-height: 40; +} +.section-title { + -fx-text-fill: #6C7A8E; + -fx-font-family: "Monospaced"; + -fx-font-size: 10; + -fx-font-weight: bold; +} + +/* Carte labo — état normal */ +.lab-card { + -fx-background-color: transparent; + -fx-background-radius: 6; + -fx-border-radius: 6; + -fx-cursor: hand; + -fx-padding: 10 12 10 12; +} +.lab-card:hover { + -fx-background-color: #1E2533; +} +.lab-card-selected { + -fx-background-color: #1E2533; + -fx-border-color: #00E5FF; + -fx-border-width: 1; + -fx-border-radius: 6; + -fx-background-radius: 6; +} +.lab-card-title { + -fx-text-fill: #C8D0DC; + -fx-font-size: 13; + -fx-font-weight: bold; + -fx-wrap-text: true; +} +.lab-card-title-selected { + -fx-text-fill: #E0E4F0; + -fx-font-size: 13; + -fx-font-weight: bold; + -fx-wrap-text: true; +} + +/* Badges catégorie */ +.badge { + -fx-text-fill: white; + -fx-font-family: "Monospaced"; + -fx-font-size: 10; + -fx-background-radius: 10; + -fx-padding: 2 8 2 8; +} +.badge-malware { -fx-background-color: #7C3AED; } +.badge-pentest { -fx-background-color: #DC2626; } +.badge-forensics { -fx-background-color: #0891B2; } +.badge-network { -fx-background-color: #0D9488; } +.badge-default { -fx-background-color: #4B5563; } + +/* Bouton Import VM */ +.btn-import { + -fx-background-color: #1E2533; + -fx-text-fill: #A0AEC0; + -fx-font-family: "Monospaced"; + -fx-font-size: 12; + -fx-border-color: #2A2E3A; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-padding: 8 0 8 0; +} +.btn-import:hover { + -fx-background-color: #252C3D; + -fx-text-fill: #E0E4F0; +} + +/* ── PANNEAU CENTRAL ─────────────────────────────────────────────────── */ +.center-panel { + -fx-background-color: #12151C; +} +.lab-title { + -fx-text-fill: #E0E4F0; + -fx-font-size: 20; + -fx-font-weight: bold; +} +.stat-label-key { + -fx-text-fill: #6C7A8E; + -fx-font-size: 12; +} +.stat-total { -fx-text-fill: #E0E4F0; -fx-font-size: 12; -fx-font-weight: bold; } +.stat-running { -fx-text-fill: #4CD964; -fx-font-size: 12; -fx-font-weight: bold; } +.stat-snaps { -fx-text-fill: #FFB74D; -fx-font-size: 12; -fx-font-weight: bold; } +.stat-sep { -fx-text-fill: #2A2E3A; -fx-font-size: 12; } + +/* En-têtes colonnes tableau VM */ +.table-header-label { + -fx-text-fill: #BB86FC; + -fx-font-family: "Monospaced"; + -fx-font-size: 11; + -fx-font-weight: bold; +} +.table-header-row { + -fx-background-color: #1A1E28; + -fx-border-color: #2A2E3A; + -fx-border-width: 0 0 1 0; + -fx-min-height: 36; + -fx-pref-height: 36; +} + +/* Lignes VM */ +.vm-row { + -fx-background-color: transparent; + -fx-border-color: #2A2E3A; + -fx-border-width: 0 0 1 0; + -fx-cursor: hand; + -fx-padding: 12 20 12 20; + -fx-min-height: 48; +} +.vm-row:hover { + -fx-background-color: #1E2533; +} +.vm-row-selected { + -fx-background-color: #1E2533; + -fx-border-color: #2A2E3A; + -fx-border-width: 0 0 1 0; + -fx-padding: 12 20 12 20; + -fx-min-height: 48; +} +.vm-name { -fx-text-fill: #E0E4F0; -fx-font-size: 13; -fx-font-weight: bold; } +.vm-uuid { -fx-text-fill: #9AA6B9; -fx-font-family: "Monospaced"; -fx-font-size: 12; } +.vm-os { -fx-text-fill: #9AA6B9; -fx-font-size: 12; } + +/* Badge statut VM (pill) */ +.status-badge { + -fx-font-family: "Monospaced"; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-background-radius: 10; + -fx-border-radius: 10; + -fx-border-width: 1; + -fx-padding: 3 10 3 10; +} +.status-running { -fx-text-fill: #4CD964; -fx-background-color: #0D2818; -fx-border-color: #4CD964; } +.status-stopped { -fx-text-fill: #F87171; -fx-background-color: #2D1515; -fx-border-color: #F87171; } +.status-saved { -fx-text-fill: #60A5FA; -fx-background-color: #0F1D35; -fx-border-color: #60A5FA; } +.status-paused { -fx-text-fill: #FBBF24; -fx-background-color: #2D2010; -fx-border-color: #FBBF24; } +.status-unknown { -fx-text-fill: #9CA3AF; -fx-background-color: #1F2533; -fx-border-color: #9CA3AF; } + +/* Bouton Export PDF */ +.btn-export-pdf { + -fx-background-color: #3B82F6; + -fx-text-fill: white; + -fx-font-family: "Monospaced"; + -fx-font-size: 12; + -fx-font-weight: bold; + -fx-border-radius: 6; + -fx-background-radius: 6; + -fx-cursor: hand; + -fx-padding: 9 18 9 18; +} +.btn-export-pdf:hover { -fx-background-color: #2563EB; } + +/* ── PANNEAU DROIT ───────────────────────────────────────────────────── */ +.right-panel { + -fx-background-color: #1A1E28; + -fx-border-color: #2A2E3A; + -fx-border-width: 0 0 0 1; + -fx-min-width: 300; + -fx-pref-width: 380; + -fx-max-width: 420; +} +.right-section { + -fx-border-color: #2A2E3A; + -fx-border-width: 0 0 1 0; + -fx-padding: 14 16 16 16; +} +.right-section-title { + -fx-text-fill: #6C7A8E; + -fx-font-family: "Monospaced"; + -fx-font-size: 10; + -fx-font-weight: bold; +} +.vm-selected-name { + -fx-text-fill: #00E5FF; + -fx-font-family: "Monospaced"; + -fx-font-size: 12; +} +.controls-placeholder { + -fx-text-fill: #3D4555; + -fx-font-family: "Monospaced"; + -fx-font-size: 12; + -fx-wrap-text: true; +} + +/* Boutons contrôle VM */ +.btn-stop { + -fx-background-color: #C0392B; + -fx-text-fill: white; + -fx-font-family: "Monospaced"; + -fx-font-size: 13; + -fx-font-weight: bold; + -fx-border-color: #922B21; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 40; +} +.btn-stop:hover { -fx-background-color: #A93226; } + +.btn-start { + -fx-background-color: #14532D; + -fx-text-fill: #4CD964; + -fx-font-family: "Monospaced"; + -fx-font-size: 13; + -fx-font-weight: bold; + -fx-border-color: #166534; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 40; +} +.btn-start:hover { -fx-background-color: #1A6B3A; } + +.btn-secondary { + -fx-background-color: #1E2533; + -fx-text-fill: #E0E4F0; + -fx-font-family: "Monospaced"; + -fx-font-size: 13; + -fx-border-color: #2A2E3A; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-cursor: hand; + -fx-pref-height: 40; +} +.btn-secondary:hover { -fx-background-color: #252C3D; } + +/* ComboBox snapshot */ +.combo-box { + -fx-background-color: #1E2533; + -fx-border-color: #2A2E3A; + -fx-border-radius: 4; + -fx-background-radius: 4; +} +.combo-box .list-cell { + -fx-text-fill: #E0E4F0; + -fx-background-color: #1E2533; + -fx-font-family: "Monospaced"; + -fx-font-size: 12; +} +.combo-box-popup .list-view { + -fx-background-color: #1E2533; + -fx-border-color: #2A2E3A; +} +.combo-box-popup .list-view .list-cell:hover { + -fx-background-color: #252C3D; +} +.combo-box .arrow-button { + -fx-background-color: transparent; +} +.combo-box .arrow { + -fx-background-color: #6C7A8E; +} + +/* Bouton restore */ +.btn-restore { + -fx-background-color: transparent; + -fx-text-fill: #6C7A8E; + -fx-font-family: "Monospaced"; + -fx-font-size: 12; + -fx-border-color: #2A2E3A; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-cursor: hand; + -fx-pref-height: 36; +} +.btn-restore:hover { -fx-text-fill: #E0E4F0; -fx-border-color: #3D4555; } + +/* TextArea journal */ +.journal-area { + -fx-control-inner-background: #0D1118; + -fx-background-color: #0D1118; + -fx-text-fill: #86EFAC; + -fx-font-family: "Monospaced"; + -fx-font-size: 11; + -fx-border-color: #2A2E3A; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-highlight-fill: #2A2E3A; +} +.journal-area .content { -fx-background-color: #0D1118; } + +/* TextArea audit */ +.audit-area { + -fx-control-inner-background: #0D1118; + -fx-background-color: #0D1118; + -fx-text-fill: #6C7A8E; + -fx-font-family: "Monospaced"; + -fx-font-size: 10; + -fx-border-color: #2A2E3A; + -fx-border-radius: 4; + -fx-background-radius: 4; +} +.audit-area .content { -fx-background-color: #0D1118; } + +/* TextField saisie journal */ +.journal-input { + -fx-control-inner-background: #1E2533; + -fx-background-color: #1E2533; + -fx-text-fill: #E0E4F0; + -fx-prompt-text-fill: #3D4555; + -fx-font-family: "Monospaced"; + -fx-font-size: 11; + -fx-border-color: #2A2E3A; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-padding: 8; +} +.journal-input:focused { -fx-border-color: #00E5FF; } + +/* Bouton Add Note */ +.btn-add-note { + -fx-background-color: #3B82F6; + -fx-text-fill: white; + -fx-font-family: "Monospaced"; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-border-radius: 4; + -fx-background-radius: 4; + -fx-cursor: hand; + -fx-padding: 0 14 0 14; + -fx-pref-height: 36; +} +.btn-add-note:hover { -fx-background-color: #2563EB; } + +/* Section Audit Logs */ +.audit-section { + -fx-background-color: #161A24; +} +.audit-header { + -fx-cursor: hand; + -fx-pref-height: 44; + -fx-min-height: 44; +} +.audit-title { + -fx-text-fill: #6C7A8E; + -fx-font-family: "Monospaced"; + -fx-font-size: 10; + -fx-font-weight: bold; +} +.audit-arrow { + -fx-text-fill: #6C7A8E; + -fx-font-size: 18; +} + +/* ── BARRE DE STATUT ─────────────────────────────────────────────────── */ +.status-bar { + -fx-background-color: #2563EB; + -fx-border-color: #1D4ED8; + -fx-border-width: 1 0 0 0; + -fx-pref-height: 30; + -fx-min-height: 30; +} +.status-version { + -fx-text-fill: #BFDBFE; + -fx-font-family: "Monospaced"; + -fx-font-size: 11; +} +.status-connected { -fx-text-fill: #4CD964; -fx-font-family: "Monospaced"; -fx-font-size: 11; } +.status-dot-ok { -fx-text-fill: #4CD964; -fx-font-size: 12; } + +/* ── LABEL VIDE (placeholder) ────────────────────────────────────────── */ +.empty-label { + -fx-text-fill: #6C7A8E; + -fx-font-family: "Monospaced"; + -fx-font-size: 12; + -fx-wrap-text: true; +} + +/* ── SÉPARATEUR VERTICAL ─────────────────────────────────────────────── */ +.v-separator { + -fx-background-color: #2A2E3A; + -fx-pref-width: 1; + -fx-min-width: 1; + -fx-max-width: 1; +} \ No newline at end of file diff --git a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml index 73431e9..be4edd6 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml +++ b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml @@ -1,20 +1,316 @@ - - + + - + + + + + + + + + +
-
+ + + - + + + + +
From 1be93952c86c5294e00feef2dd80b15a419949cd Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 25 Jun 2026 15:10:46 +0000 Subject: [PATCH 16/27] UI: Alignement complet de l'interface avec la maquette et refactoring des composants --- .../controller/LabController.java | 253 +++++--- .../tg/cyberlabmanager/ui/LabListView.java | 29 +- .../tg/cyberlabmanager/ui/VmTableView.java | 46 ++ .../tg/cyberlabmanager/ui/cyberlab.css | 595 ++++++++++++------ .../tg/cyberlabmanager/ui/main-view.fxml | 2 - 5 files changed, 636 insertions(+), 289 deletions(-) create mode 100644 src/main/java/tg/cyberlabmanager/ui/VmTableView.java diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index d80fcda..441a120 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -15,7 +15,10 @@ import java.util.concurrent.Executors; import java.util.function.Consumer; - +/** + * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). + * Il gère les données de la maquette et agit comme relais visuel. + */ public class LabController { private IHypervisor hypervisor; @@ -26,91 +29,177 @@ public class LabController { @FXML private Label statusLabel; - - + @FXML + private javafx.scene.layout.VBox labListContainer; + @FXML + private Label labTitleLabel; + @FXML + private javafx.scene.control.Button btnExportPdf; + @FXML + private javafx.scene.layout.HBox statsBar; + @FXML + private Label statTotal; + @FXML + private Label statRunning; + @FXML + private Label statSnapshots; + @FXML + private javafx.scene.layout.HBox vmTableHeader; + @FXML + private javafx.scene.layout.VBox vmListContainer; + @FXML + private Label vmEmptyLabel; + @FXML + private Label selectedVmNameLabel; + @FXML + private javafx.scene.control.Button btnStart; + @FXML + private javafx.scene.control.Button btnStop; + @FXML + private javafx.scene.control.Button btnSaveState; + @FXML + private javafx.scene.control.Button btnTakeSnapshot; + @FXML + private Label controlsPlaceholder; + @FXML + private javafx.scene.control.ComboBox snapshotCombo; + @FXML + private javafx.scene.control.TextArea journalArea; + @FXML + private javafx.scene.layout.VBox snapshotSection; + @FXML private void initialize() { if (statusLabel != null) { statusLabel.setText("CyberLab Manager — Initialisation..."); } + setupMockup(); + } + + private void setupMockup() { + if (labListContainer == null) + return; + labListContainer.getChildren().clear(); + labListContainer.getChildren().add( + tg.cyberlabmanager.ui.LabListView.createLabCard("APT29 Analysis", "Malware", "badge-malware", false)); + labListContainer.getChildren().add( + tg.cyberlabmanager.ui.LabListView.createLabCard("Red Team Ops", "Pentest", "badge-pentest", false)); + labListContainer.getChildren().add(tg.cyberlabmanager.ui.LabListView.createLabCard("Network Traffic Analysis", + "Network", "badge-network", true)); + labListContainer.getChildren().add(tg.cyberlabmanager.ui.LabListView.createLabCard("Memory Forensics Lab", + "Forensics", "badge-forensics", false)); + labListContainer.getChildren().add(tg.cyberlabmanager.ui.LabListView.createLabCard("Ransomware Sandbox", + "Malware", "badge-malware", false)); + + labTitleLabel.setText("Network Traffic Analysis"); + btnExportPdf.setText("Export PDF"); + btnExportPdf.setVisible(true); + btnExportPdf.setManaged(true); + statsBar.setVisible(true); + statsBar.setManaged(true); + statTotal.setText("1"); + statRunning.setText("1"); + statSnapshots.setText("4"); + + vmTableHeader.setVisible(true); + vmTableHeader.setManaged(true); + vmEmptyLabel.setVisible(false); + vmEmptyLabel.setManaged(false); + vmListContainer.getChildren().clear(); + vmListContainer.getChildren().add(tg.cyberlabmanager.ui.VmTableView.createVmRow("Wireshark-Station", + "f8c9a7b6...", "Running", "Ubuntu 22.04")); + + selectedVmNameLabel.setText("Wireshark-Station"); + controlsPlaceholder.setVisible(false); + controlsPlaceholder.setManaged(false); + btnStop.setText(" STOP"); + btnStop.setVisible(true); + btnStop.setManaged(true); + btnSaveState.setText(" Save State"); + btnSaveState.setVisible(true); + btnSaveState.setManaged(true); + btnTakeSnapshot.setText(" Take Snapshot"); + btnTakeSnapshot.setVisible(true); + btnTakeSnapshot.setManaged(true); + + snapshotCombo.getItems().add("Select snapshot"); + snapshotCombo.getSelectionModel().selectFirst(); + + journalArea.setText("[19:23:15] Started malware execution in controlled environment\n" + + "[19:24:02] Detected outbound connection to 192.168.1.105\n" + + "[19:25:44] File system changes observed in C:\\Users\\Admin\\AppData\n" + + "[19:27:10] Registry modifications detected:\nHKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\n" + + + "[19:30:55] Network beacon established - C2 communication active\n" + + "[19:35:12] Process injection detected: svchost.exe -> explorer.exe"); } - public LabController() { + /** + * Constructeur par défaut initialisant le contrôleur du laboratoire. + */ + public LabController() { } - public void setDependencies(IHypervisor hypervisor, - DatabaseManager databaseManager, - PdfExporter pdfExporter) { + DatabaseManager databaseManager, + PdfExporter pdfExporter) { } - + public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { this.hypervisor = hypervisor; this.databaseManager = databaseManager; this.pdfExporter = pdfExporter; } - public void initView() { if (statusLabel != null) { statusLabel.setText("CyberLab Manager — Prêt"); } } - - - - public List getAllLabs() { throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); } - public List getOrphanVMs() { throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); } - public void onStartVMClicked(VirtualMachine vm, - Runnable onSuccess, - Consumer onError) { - + Runnable onSuccess, + Consumer onError) { + } - public void onStopVMClicked(VirtualMachine vm, - Runnable onSuccess, - Consumer onError) { + Runnable onSuccess, + Consumer onError) { } - public void onSaveStateClicked(VirtualMachine vm, - Runnable onSuccess, - Consumer onError) { + Runnable onSuccess, + Consumer onError) { } - public void onTakeSnapshotClicked(VirtualMachine vm, - String name, - String description, - Runnable onSuccess, - Consumer onError) { + String name, + String description, + Runnable onSuccess, + Consumer onError) { } - public void onRestoreSnapshotClicked(VirtualMachine vm, - String snapshotName, - Runnable onSuccess, - Consumer onError) { + String snapshotName, + Runnable onSuccess, + Consumer onError) { } - public void onAddJournalEntry(VirtualMachine vm, String text) { - + } public void onImportVMsClicked(Consumer> onSuccess, - Consumer onError) { + Consumer onError) { } public void onExportLabClicked(Lab lab, String filePath) { @@ -127,53 +216,51 @@ public void onExportOrphansClicked(String filePath) { public void onConfigUpdated(AppConfig newConfig) { } - - -@FXML -private void handleImportVM() { - // TODO (S3) : ouvrir dialogue import VM -} - -@FXML -private void handleExportPDF() { - // TODO (S4) : ouvrir FileChooser + appeler onExportLabClicked() -} - -@FXML -private void handleStartVM() { - // TODO (S3) : appeler onStartVMClicked() -} - -@FXML -private void handleStopVM() { - // TODO (S3) : appeler onStopVMClicked() -} - -@FXML -private void handleSaveState() { - // TODO (S3) : appeler onSaveStateClicked() -} - -@FXML -private void handleTakeSnapshot() { - // TODO (S3) : appeler onTakeSnapshotClicked() -} - -@FXML -private void handleRestoreSnapshot() { - // TODO (S3) : appeler onRestoreSnapshotClicked() -} - -@FXML -private void handleAddJournalEntry() { - // TODO (S3) : lire journalInputField + appeler onAddJournalEntry() -} - -@FXML -private void handleToggleAuditLogs() { - // TODO (S3) : toggle visible/managed sur auditArea + rotation auditArrow -} - + @FXML + private void handleImportVM() { + // TODO (S3) : ouvrir dialogue import VM + } + + @FXML + private void handleExportPDF() { + // TODO (S4) : ouvrir FileChooser + appeler onExportLabClicked() + } + + @FXML + private void handleStartVM() { + // TODO (S3) : appeler onStartVMClicked() + } + + @FXML + private void handleStopVM() { + // TODO (S3) : appeler onStopVMClicked() + } + + @FXML + private void handleSaveState() { + // TODO (S3) : appeler onSaveStateClicked() + } + + @FXML + private void handleTakeSnapshot() { + // TODO (S3) : appeler onTakeSnapshotClicked() + } + + @FXML + private void handleRestoreSnapshot() { + // TODO (S3) : appeler onRestoreSnapshotClicked() + } + + @FXML + private void handleAddJournalEntry() { + // TODO (S3) : lire journalInputField + appeler onAddJournalEntry() + } + + @FXML + private void handleToggleAuditLogs() { + // TODO (S3) : toggle visible/managed sur auditArea + rotation auditArrow + } + public void shutdown() { executor.shutdownNow(); } diff --git a/src/main/java/tg/cyberlabmanager/ui/LabListView.java b/src/main/java/tg/cyberlabmanager/ui/LabListView.java index 8e06fb6..930ea8f 100644 --- a/src/main/java/tg/cyberlabmanager/ui/LabListView.java +++ b/src/main/java/tg/cyberlabmanager/ui/LabListView.java @@ -1,12 +1,35 @@ package tg.cyberlabmanager.ui; +import javafx.geometry.Insets; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; + /** - * Vue chargee d'afficher la liste des laboratoires. + * Vue chargee d'afficher la liste des laboratoires et leurs cartes. */ public class LabListView { + /** - * Cree la vue de liste des laboratoires. + * Cree le composant visuel (VBox) representant la carte d'un laboratoire. + * + * @param name Le nom du laboratoire + * @param badgeText Le texte du theme (Malware, etc.) + * @param badgeStyleClass La classe CSS pour colorer le badge + * @param selected Si le laboratoire est le laboratoire actif + * @return Le VBox pre-configure */ - public LabListView() { + public static VBox createLabCard(String name, String badgeText, String badgeStyleClass, boolean selected) { + VBox card = new VBox(5); + card.setPadding(new Insets(10, 12, 10, 12)); + card.getStyleClass().add(selected ? "lab-card-selected" : "lab-card"); + + Label title = new Label(name); + title.getStyleClass().add(selected ? "lab-card-title-selected" : "lab-card-title"); + + Label badge = new Label(badgeText); + badge.getStyleClass().addAll("badge", badgeStyleClass); + + card.getChildren().addAll(title, badge); + return card; } } diff --git a/src/main/java/tg/cyberlabmanager/ui/VmTableView.java b/src/main/java/tg/cyberlabmanager/ui/VmTableView.java new file mode 100644 index 0000000..cd0dcce --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/ui/VmTableView.java @@ -0,0 +1,46 @@ +package tg.cyberlabmanager.ui; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; + +/** + * Vue chargee des elements du tableau des machines virtuelles. + */ +public class VmTableView { + + /** + * Cree la ligne HBox pour afficher une Machine Virtuelle dans le tableau. + * + * @param name Nom de la VM + * @param uuid UUID court + * @param status Statut en texte clair + * @param os OS de la machine + * @return La ligne d'affichage configurée + */ + public static HBox createVmRow(String name, String uuid, String status, String os) { + HBox row = new HBox(); + row.getStyleClass().add("vm-row-selected"); + row.setAlignment(Pos.CENTER_LEFT); + row.setPadding(new Insets(0, 20, 0, 22)); + + Label lName = new Label(name); + lName.setPrefWidth(240); + lName.getStyleClass().add("vm-name"); + + Label lUuid = new Label(uuid); + lUuid.setPrefWidth(210); + lUuid.getStyleClass().add("vm-uuid"); + + Label lStatus = new Label(status); + lStatus.setPrefWidth(150); + lStatus.getStyleClass().addAll("status-badge", "status-running"); + + Label lOs = new Label(os); + lOs.getStyleClass().add("vm-os"); + + row.getChildren().addAll(lName, lUuid, lStatus, lOs); + return row; + } +} diff --git a/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css index 0fe2552..2651f8a 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css +++ b/src/main/resources/tg/cyberlabmanager/ui/cyberlab.css @@ -1,44 +1,44 @@ -/* -Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license -Click nbfs://nbhost/SystemFileSystem/Templates/Other/CascadeStyleSheet.css to edit this template -*/ -/* - Created on : 13 juin 2026, 18:34:24 - Author : fiodor -*/ +/* ═══════════════════════════════════════════════════════════════════════ + CyberLab Manager — Thème Exact selon Spécifications de Maquette + Couleurs: VS Code Dark (#1E1E1E bg, #333333 borders, #007ACC accent) + Polices: Inter (texte général), JetBrains Mono (monospace) + ═══════════════════════════════════════════════════════════════════════ */ - -/* ── ROOT : fond global ──────────────────────────────────────────────── */ +/* ── ROOT ──────────────────────────────────────────────────────────────── */ .root { - -fx-background-color: #12151C; - -fx-font-family: "System"; + -fx-background-color: #1E1E1E; + -fx-font-family: "Inter", "Segoe UI", "Helvetica Neue", Arial, sans-serif; + -fx-font-size: 13; } -/* ── SCROLL PANE : transparent partout ──────────────────────────────── */ +/* ── SCROLL PANE ───────────────────────────────────────────────────────── */ .scroll-pane { -fx-background-color: transparent; -fx-background: transparent; -fx-border-color: transparent; -fx-padding: 0; } -.scroll-pane > .viewport { + +.scroll-pane>.viewport { -fx-background-color: transparent; } + .scroll-pane .corner { -fx-background-color: transparent; } -/* Scrollbars fines et discrètes */ .scroll-bar:vertical, .scroll-bar:horizontal { - -fx-background-color: #1A1E28; + -fx-background-color: #1E1E1E; -fx-pref-width: 6; -fx-pref-height: 6; } + .scroll-bar .thumb { - -fx-background-color: #2A2E3A; + -fx-background-color: #424242; -fx-background-radius: 3; } + .scroll-bar .increment-button, .scroll-bar .decrement-button { -fx-background-color: transparent; @@ -46,134 +46,204 @@ Click nbfs://nbhost/SystemFileSystem/Templates/Other/CascadeStyleSheet.css to ed -fx-pref-height: 0; } -/* ── BARRE DE TITRE ──────────────────────────────────────────────────── */ +/* ── BARRE DE TITRE (Top Header) ───────────────────────────────────────── */ .title-bar { - -fx-background-color: #0F131C; - -fx-border-color: #2A2E3A; + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; -fx-border-width: 0 0 1 0; - -fx-min-height: 50; - -fx-pref-height: 50; + -fx-min-height: 48; + -fx-pref-height: 48; } + .title-label { - -fx-text-fill: #E0E4F0; - -fx-font-family: "Monospaced"; - -fx-font-size: 16; + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 15; -fx-font-weight: bold; } -/* ── PANNEAU GAUCHE ──────────────────────────────────────────────────── */ +/* ── PANNEAU GAUCHE (Lab Inventory) ────────────────────────────────────── */ .left-panel { - -fx-background-color: #1A1E28; - -fx-border-color: #2A2E3A; + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; -fx-border-width: 0 1 0 0; - -fx-min-width: 240; - -fx-pref-width: 280; + -fx-min-width: 180; + -fx-pref-width: 240; -fx-max-width: 320; } + .section-header { - -fx-background-color: #1A1E28; - -fx-border-color: #2A2E3A; + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; -fx-border-width: 0 0 1 0; - -fx-min-height: 40; - -fx-pref-height: 40; + -fx-min-height: 44; + -fx-pref-height: 44; } + .section-title { - -fx-text-fill: #6C7A8E; - -fx-font-family: "Monospaced"; - -fx-font-size: 10; + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 11; -fx-font-weight: bold; } -/* Carte labo — état normal */ +/* Cartes de Lab */ .lab-card { - -fx-background-color: transparent; - -fx-background-radius: 6; - -fx-border-radius: 6; + -fx-background-color: #252526; + -fx-background-radius: 5; + -fx-border-color: transparent; + -fx-border-width: 1; + -fx-border-radius: 5; -fx-cursor: hand; -fx-padding: 10 12 10 12; } + .lab-card:hover { - -fx-background-color: #1E2533; + -fx-background-color: #2D2D30; } + .lab-card-selected { - -fx-background-color: #1E2533; - -fx-border-color: #00E5FF; + -fx-background-color: #2D2D30; + -fx-border-color: #007ACC; -fx-border-width: 1; - -fx-border-radius: 6; - -fx-background-radius: 6; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-padding: 10 12 10 12; } + .lab-card-title { - -fx-text-fill: #C8D0DC; + -fx-text-fill: #CCCCCC; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 13; - -fx-font-weight: bold; + -fx-font-weight: 500; -fx-wrap-text: true; } + .lab-card-title-selected { - -fx-text-fill: #E0E4F0; + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 13; - -fx-font-weight: bold; + -fx-font-weight: 500; -fx-wrap-text: true; } /* Badges catégorie */ .badge { -fx-text-fill: white; - -fx-font-family: "Monospaced"; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 10; - -fx-background-radius: 10; - -fx-padding: 2 8 2 8; + -fx-background-radius: 4; + -fx-border-radius: 4; + -fx-border-width: 1; + -fx-padding: 2 7 2 7; +} + +/* Malware: bg-red-900/50 text-red-300 border-red-800 */ +.badge-malware { + -fx-background-color: rgba(127, 29, 29, 0.6); + -fx-text-fill: #FCA5A5; + -fx-border-color: #991B1B; +} + +/* Pentest: bg-purple-900/50 text-purple-300 border-purple-800 */ +.badge-pentest { + -fx-background-color: rgba(76, 29, 149, 0.6); + -fx-text-fill: #C4B5FD; + -fx-border-color: #6D28D9; +} + +/* Forensics: bg-blue-900/50 text-blue-300 border-blue-800 */ +.badge-forensics { + -fx-background-color: rgba(30, 58, 138, 0.6); + -fx-text-fill: #93C5FD; + -fx-border-color: #1D4ED8; +} + +/* Network: bg-teal-900/50 text-teal-300 border-teal-800 */ +.badge-network { + -fx-background-color: rgba(19, 78, 74, 0.6); + -fx-text-fill: #5EEAD4; + -fx-border-color: #0F766E; +} + +.badge-default { + -fx-background-color: rgba(55, 65, 81, 0.6); + -fx-text-fill: #9CA3AF; + -fx-border-color: #4B5563; } -.badge-malware { -fx-background-color: #7C3AED; } -.badge-pentest { -fx-background-color: #DC2626; } -.badge-forensics { -fx-background-color: #0891B2; } -.badge-network { -fx-background-color: #0D9488; } -.badge-default { -fx-background-color: #4B5563; } /* Bouton Import VM */ .btn-import { - -fx-background-color: #1E2533; - -fx-text-fill: #A0AEC0; - -fx-font-family: "Monospaced"; + -fx-background-color: #252526; + -fx-text-fill: #CCCCCC; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 12; - -fx-border-color: #2A2E3A; + -fx-border-color: #3E3E42; -fx-border-radius: 5; -fx-background-radius: 5; + -fx-border-width: 1; -fx-cursor: hand; -fx-padding: 8 0 8 0; } + .btn-import:hover { - -fx-background-color: #252C3D; - -fx-text-fill: #E0E4F0; + -fx-background-color: #2D2D30; + -fx-border-color: #007ACC; + -fx-text-fill: #FFFFFF; } -/* ── PANNEAU CENTRAL ─────────────────────────────────────────────────── */ +/* ── PANNEAU CENTRAL (VM Dashboard) ────────────────────────────────────── */ .center-panel { - -fx-background-color: #12151C; + -fx-background-color: #1E1E1E; } + .lab-title { - -fx-text-fill: #E0E4F0; - -fx-font-size: 20; + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 18; -fx-font-weight: bold; } + +/* Stats bar */ .stat-label-key { - -fx-text-fill: #6C7A8E; + -fx-text-fill: #9D9D9D; + -fx-font-size: 12; +} + +.stat-total { + -fx-text-fill: #FFFFFF; + -fx-font-size: 12; + -fx-font-weight: bold; +} + +.stat-running { + -fx-text-fill: #4ADE80; + -fx-font-size: 12; + -fx-font-weight: bold; +} + +.stat-snaps { + -fx-text-fill: #60A5FA; + -fx-font-size: 12; + -fx-font-weight: bold; +} + +.stat-sep { + -fx-text-fill: #4B5563; -fx-font-size: 12; } -.stat-total { -fx-text-fill: #E0E4F0; -fx-font-size: 12; -fx-font-weight: bold; } -.stat-running { -fx-text-fill: #4CD964; -fx-font-size: 12; -fx-font-weight: bold; } -.stat-snaps { -fx-text-fill: #FFB74D; -fx-font-size: 12; -fx-font-weight: bold; } -.stat-sep { -fx-text-fill: #2A2E3A; -fx-font-size: 12; } /* En-têtes colonnes tableau VM */ .table-header-label { - -fx-text-fill: #BB86FC; - -fx-font-family: "Monospaced"; - -fx-font-size: 11; + -fx-text-fill: #9D9D9D; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 10; -fx-font-weight: bold; } + .table-header-row { - -fx-background-color: #1A1E28; - -fx-border-color: #2A2E3A; + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; -fx-border-width: 0 0 1 0; -fx-min-height: 36; -fx-pref-height: 36; @@ -182,275 +252,398 @@ Click nbfs://nbhost/SystemFileSystem/Templates/Other/CascadeStyleSheet.css to ed /* Lignes VM */ .vm-row { -fx-background-color: transparent; - -fx-border-color: #2A2E3A; + -fx-border-color: #333333; -fx-border-width: 0 0 1 0; -fx-cursor: hand; -fx-padding: 12 20 12 20; - -fx-min-height: 48; + -fx-min-height: 46; } + .vm-row:hover { - -fx-background-color: #1E2533; + -fx-background-color: #252526; } + .vm-row-selected { - -fx-background-color: #1E2533; - -fx-border-color: #2A2E3A; + -fx-background-color: #2D2D30; + -fx-border-color: #333333; -fx-border-width: 0 0 1 0; - -fx-padding: 12 20 12 20; - -fx-min-height: 48; + -fx-padding: 12 20 12 22; + -fx-min-height: 46; } -.vm-name { -fx-text-fill: #E0E4F0; -fx-font-size: 13; -fx-font-weight: bold; } -.vm-uuid { -fx-text-fill: #9AA6B9; -fx-font-family: "Monospaced"; -fx-font-size: 12; } -.vm-os { -fx-text-fill: #9AA6B9; -fx-font-size: 12; } -/* Badge statut VM (pill) */ +.vm-name { + -fx-text-fill: #FFFFFF; + -fx-font-size: 13; + -fx-font-weight: 500; +} + +.vm-uuid { + -fx-text-fill: #9D9D9D; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 12; +} + +.vm-os { + -fx-text-fill: #D4D4D4; + -fx-font-size: 12; +} + +/* Badge statut VM */ .status-badge { - -fx-font-family: "Monospaced"; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 11; -fx-font-weight: bold; - -fx-background-radius: 10; - -fx-border-radius: 10; + -fx-background-radius: 4; + -fx-border-radius: 4; -fx-border-width: 1; - -fx-padding: 3 10 3 10; + -fx-padding: 2 8 2 8; +} + +/* Running: bg-green-900/50 text-green-300 border-green-700 */ +.status-running { + -fx-text-fill: #86EFAC; + -fx-background-color: rgba(20, 83, 45, 0.6); + -fx-border-color: #15803D; +} + +/* Stopped: bg-red-900/30 text-red-300 border-red-700 */ +.status-stopped { + -fx-text-fill: #FCA5A5; + -fx-background-color: rgba(127, 29, 29, 0.4); + -fx-border-color: #B91C1C; +} + +.status-saved { + -fx-text-fill: #93C5FD; + -fx-background-color: rgba(30, 58, 138, 0.4); + -fx-border-color: #1D4ED8; +} + +.status-paused { + -fx-text-fill: #FCD34D; + -fx-background-color: rgba(120, 53, 15, 0.4); + -fx-border-color: #B45309; +} + +.status-unknown { + -fx-text-fill: #9CA3AF; + -fx-background-color: rgba(55, 65, 81, 0.4); + -fx-border-color: #4B5563; } -.status-running { -fx-text-fill: #4CD964; -fx-background-color: #0D2818; -fx-border-color: #4CD964; } -.status-stopped { -fx-text-fill: #F87171; -fx-background-color: #2D1515; -fx-border-color: #F87171; } -.status-saved { -fx-text-fill: #60A5FA; -fx-background-color: #0F1D35; -fx-border-color: #60A5FA; } -.status-paused { -fx-text-fill: #FBBF24; -fx-background-color: #2D2010; -fx-border-color: #FBBF24; } -.status-unknown { -fx-text-fill: #9CA3AF; -fx-background-color: #1F2533; -fx-border-color: #9CA3AF; } /* Bouton Export PDF */ .btn-export-pdf { -fx-background-color: #3B82F6; -fx-text-fill: white; - -fx-font-family: "Monospaced"; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 12; -fx-font-weight: bold; - -fx-border-radius: 6; - -fx-background-radius: 6; + -fx-border-radius: 5; + -fx-background-radius: 5; + -fx-border-width: 0; -fx-cursor: hand; - -fx-padding: 9 18 9 18; + -fx-padding: 7 16 7 16; +} + +.btn-export-pdf:hover { + -fx-background-color: #2563EB; } -.btn-export-pdf:hover { -fx-background-color: #2563EB; } -/* ── PANNEAU DROIT ───────────────────────────────────────────────────── */ +/* ── PANNEAU DROIT (Orchestration & Intel) ──────────────────────────────── */ .right-panel { - -fx-background-color: #1A1E28; - -fx-border-color: #2A2E3A; + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; -fx-border-width: 0 0 0 1; - -fx-min-width: 300; - -fx-pref-width: 380; - -fx-max-width: 420; + -fx-min-width: 280; + -fx-pref-width: 360; + -fx-max-width: 460; } + .right-section { - -fx-border-color: #2A2E3A; + -fx-border-color: #333333; -fx-border-width: 0 0 1 0; -fx-padding: 14 16 16 16; } + .right-section-title { - -fx-text-fill: #6C7A8E; - -fx-font-family: "Monospaced"; + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 10; -fx-font-weight: bold; } + .vm-selected-name { - -fx-text-fill: #00E5FF; - -fx-font-family: "Monospaced"; - -fx-font-size: 12; + -fx-text-fill: #6E6E6E; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 11; } + .controls-placeholder { - -fx-text-fill: #3D4555; - -fx-font-family: "Monospaced"; - -fx-font-size: 12; + -fx-text-fill: #4B5563; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 11; -fx-wrap-text: true; } -/* Boutons contrôle VM */ +/* Boutons STOP, Save State, Take Snapshot */ +/* STOP: bg-red-900/30 text-red-300 border-red-800 */ .btn-stop { - -fx-background-color: #C0392B; - -fx-text-fill: white; - -fx-font-family: "Monospaced"; + -fx-background-color: rgba(127, 29, 29, 0.35); + -fx-text-fill: #FCA5A5; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 13; -fx-font-weight: bold; - -fx-border-color: #922B21; + -fx-border-color: #991B1B; + -fx-border-width: 1; -fx-border-radius: 5; -fx-background-radius: 5; -fx-cursor: hand; - -fx-pref-height: 40; + -fx-pref-height: 42; } -.btn-stop:hover { -fx-background-color: #A93226; } +.btn-stop:hover { + -fx-background-color: rgba(127, 29, 29, 0.6); + -fx-text-fill: #FEE2E2; +} + +/* Start (caché par défaut, visible quand VM est stoppée) */ .btn-start { - -fx-background-color: #14532D; - -fx-text-fill: #4CD964; - -fx-font-family: "Monospaced"; + -fx-background-color: rgba(20, 83, 45, 0.4); + -fx-text-fill: #86EFAC; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 13; -fx-font-weight: bold; - -fx-border-color: #166534; + -fx-border-color: #15803D; + -fx-border-width: 1; -fx-border-radius: 5; -fx-background-radius: 5; -fx-cursor: hand; - -fx-pref-height: 40; + -fx-pref-height: 42; +} + +.btn-start:hover { + -fx-background-color: rgba(20, 83, 45, 0.7); } -.btn-start:hover { -fx-background-color: #1A6B3A; } +/* Save State / Take Snapshot: variant="outline" bg-[#252526] border-[#3E3E42] */ .btn-secondary { - -fx-background-color: #1E2533; - -fx-text-fill: #E0E4F0; - -fx-font-family: "Monospaced"; - -fx-font-size: 13; - -fx-border-color: #2A2E3A; + -fx-background-color: #252526; + -fx-text-fill: #FFFFFF; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; + -fx-font-size: 12; + -fx-border-color: #3E3E42; + -fx-border-width: 1; -fx-border-radius: 5; -fx-background-radius: 5; -fx-cursor: hand; - -fx-pref-height: 40; + -fx-pref-height: 38; +} + +.btn-secondary:hover { + -fx-background-color: #2D2D30; + -fx-border-color: #555555; } -.btn-secondary:hover { -fx-background-color: #252C3D; } /* ComboBox snapshot */ .combo-box { - -fx-background-color: #1E2533; - -fx-border-color: #2A2E3A; + -fx-background-color: #252526; + -fx-border-color: #3E3E42; + -fx-border-width: 1; -fx-border-radius: 4; -fx-background-radius: 4; } + .combo-box .list-cell { - -fx-text-fill: #E0E4F0; - -fx-background-color: #1E2533; - -fx-font-family: "Monospaced"; + -fx-text-fill: #FFFFFF; + -fx-background-color: #252526; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 12; } + .combo-box-popup .list-view { - -fx-background-color: #1E2533; - -fx-border-color: #2A2E3A; + -fx-background-color: #252526; + -fx-border-color: #3E3E42; } + .combo-box-popup .list-view .list-cell:hover { - -fx-background-color: #252C3D; + -fx-background-color: #2D2D30; } + .combo-box .arrow-button { -fx-background-color: transparent; } + .combo-box .arrow { - -fx-background-color: #6C7A8E; + -fx-background-color: #9D9D9D; } /* Bouton restore */ .btn-restore { -fx-background-color: transparent; - -fx-text-fill: #6C7A8E; - -fx-font-family: "Monospaced"; + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 12; - -fx-border-color: #2A2E3A; + -fx-border-color: #3E3E42; + -fx-border-width: 1; -fx-border-radius: 4; -fx-background-radius: 4; -fx-cursor: hand; - -fx-pref-height: 36; + -fx-pref-height: 34; +} + +.btn-restore:hover { + -fx-text-fill: #FFFFFF; + -fx-border-color: #555555; } -.btn-restore:hover { -fx-text-fill: #E0E4F0; -fx-border-color: #3D4555; } -/* TextArea journal */ +/* TextArea journal — bg-[#0D1117] font-mono text-xs */ .journal-area { - -fx-control-inner-background: #0D1118; - -fx-background-color: #0D1118; - -fx-text-fill: #86EFAC; - -fx-font-family: "Monospaced"; + -fx-control-inner-background: #0D1117; + -fx-background-color: #0D1117; + -fx-text-fill: #D4D4D4; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; -fx-font-size: 11; - -fx-border-color: #2A2E3A; + -fx-border-color: #30363D; + -fx-border-width: 1; -fx-border-radius: 4; -fx-background-radius: 4; - -fx-highlight-fill: #2A2E3A; + -fx-highlight-fill: #264F78; +} + +.journal-area .content { + -fx-background-color: #0D1117; } -.journal-area .content { -fx-background-color: #0D1118; } /* TextArea audit */ .audit-area { - -fx-control-inner-background: #0D1118; - -fx-background-color: #0D1118; - -fx-text-fill: #6C7A8E; - -fx-font-family: "Monospaced"; + -fx-control-inner-background: #0D1117; + -fx-background-color: #0D1117; + -fx-text-fill: #9D9D9D; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; -fx-font-size: 10; - -fx-border-color: #2A2E3A; + -fx-border-color: #30363D; + -fx-border-width: 1; -fx-border-radius: 4; -fx-background-radius: 4; } -.audit-area .content { -fx-background-color: #0D1118; } + +.audit-area .content { + -fx-background-color: #0D1117; +} /* TextField saisie journal */ .journal-input { - -fx-control-inner-background: #1E2533; - -fx-background-color: #1E2533; - -fx-text-fill: #E0E4F0; - -fx-prompt-text-fill: #3D4555; - -fx-font-family: "Monospaced"; + -fx-control-inner-background: #1E1E1E; + -fx-background-color: #1E1E1E; + -fx-text-fill: #FFFFFF; + -fx-prompt-text-fill: #6E6E6E; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 11; - -fx-border-color: #2A2E3A; + -fx-border-color: #3E3E42; + -fx-border-width: 1; -fx-border-radius: 4; -fx-background-radius: 4; - -fx-padding: 8; + -fx-padding: 6; } -.journal-input:focused { -fx-border-color: #00E5FF; } -/* Bouton Add Note */ +.journal-input:focused { + -fx-border-color: #007ACC; +} + +/* Bouton Add Note — bg-[#3B82F6] */ .btn-add-note { -fx-background-color: #3B82F6; -fx-text-fill: white; - -fx-font-family: "Monospaced"; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 11; -fx-font-weight: bold; + -fx-border-width: 0; -fx-border-radius: 4; -fx-background-radius: 4; -fx-cursor: hand; - -fx-padding: 0 14 0 14; - -fx-pref-height: 36; + -fx-padding: 0 12 0 12; + -fx-pref-height: 32; +} + +.btn-add-note:hover { + -fx-background-color: #2563EB; } -.btn-add-note:hover { -fx-background-color: #2563EB; } -/* Section Audit Logs */ +/* Section Audit Logs — bg-[#252526] header bg */ .audit-section { - -fx-background-color: #161A24; + -fx-background-color: #1E1E1E; + -fx-border-color: #333333; + -fx-border-width: 1; + -fx-border-radius: 4; + -fx-background-radius: 4; } + .audit-header { -fx-cursor: hand; - -fx-pref-height: 44; - -fx-min-height: 44; + -fx-background-color: #252526; + -fx-pref-height: 42; + -fx-min-height: 42; + -fx-background-radius: 4; } + +.audit-header:hover { + -fx-background-color: #2D2D30; +} + .audit-title { - -fx-text-fill: #6C7A8E; - -fx-font-family: "Monospaced"; + -fx-text-fill: #9D9D9D; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 10; -fx-font-weight: bold; } + .audit-arrow { - -fx-text-fill: #6C7A8E; - -fx-font-size: 18; + -fx-text-fill: #9D9D9D; + -fx-font-size: 16; } -/* ── BARRE DE STATUT ─────────────────────────────────────────────────── */ +/* ── BARRE DE STATUT — bg-[#007ACC] VS Code Blue ──────────────────────── */ .status-bar { - -fx-background-color: #2563EB; - -fx-border-color: #1D4ED8; + -fx-background-color: #007ACC; + -fx-border-color: #005A9E; -fx-border-width: 1 0 0 0; - -fx-pref-height: 30; - -fx-min-height: 30; + -fx-pref-height: 28; + -fx-min-height: 28; } + .status-version { - -fx-text-fill: #BFDBFE; - -fx-font-family: "Monospaced"; + -fx-text-fill: #FFFFFF; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 11; +} + +.status-connected { + -fx-text-fill: #FFFFFF; + -fx-font-family: "JetBrains Mono", "Cascadia Code", "Consolas", monospace; + -fx-font-size: 11; +} + +.status-dot-ok { + -fx-text-fill: #4ADE80; + -fx-font-size: 11; +} + +.status-dot-error { + -fx-text-fill: #F87171; -fx-font-size: 11; } -.status-connected { -fx-text-fill: #4CD964; -fx-font-family: "Monospaced"; -fx-font-size: 11; } -.status-dot-ok { -fx-text-fill: #4CD964; -fx-font-size: 12; } -/* ── LABEL VIDE (placeholder) ────────────────────────────────────────── */ +/* ── LABEL VIDE (placeholder) ──────────────────────────────────────────── */ .empty-label { - -fx-text-fill: #6C7A8E; - -fx-font-family: "Monospaced"; + -fx-text-fill: #6E6E6E; + -fx-font-family: "Inter", "Segoe UI", Arial, sans-serif; -fx-font-size: 12; -fx-wrap-text: true; } -/* ── SÉPARATEUR VERTICAL ─────────────────────────────────────────────── */ +/* ── SÉPARATEUR VERTICAL ───────────────────────────────────────────────── */ .v-separator { - -fx-background-color: #2A2E3A; + -fx-background-color: #333333; -fx-pref-width: 1; -fx-min-width: 1; -fx-max-width: 1; diff --git a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml index be4edd6..2f307af 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml +++ b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml @@ -115,7 +115,6 @@ - - From 787dbfc36ea98d5e3e4abe2817600789ed5ab56d Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 11:43:30 +0000 Subject: [PATCH 17/27] feat(ui): phase1A - affichage dynamique des labos via mockLabs --- .../controller/LabController.java | 122 ++++++++---------- 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 441a120..be07f1a 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -9,21 +9,25 @@ import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.VirtualMachine; import tg.cyberlabmanager.pdf.PdfExporter; +import tg.cyberlabmanager.model.VMStatus; +import tg.cyberlabmanager.ui.LabListView; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; +import java.util.ArrayList; /** - * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). - * Il gère les données de la maquette et agit comme relais visuel. + * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). Il + * gère les données de la maquette et agit comme relais visuel. */ public class LabController { private IHypervisor hypervisor; private DatabaseManager databaseManager; private PdfExporter pdfExporter; + private Lab selectedLab; private final ExecutorService executor = Executors.newCachedThreadPool(); @@ -70,68 +74,54 @@ public class LabController { @FXML private void initialize() { - if (statusLabel != null) { - statusLabel.setText("CyberLab Manager — Initialisation..."); - } - setupMockup(); + mockLabs(); + refreshLabList(); + } - private void setupMockup() { - if (labListContainer == null) - return; + // ── Données mock temporaires ────────────────────────────────────────── + private List mockLabs() { + List labs = new ArrayList<>(); + labs.add(new Lab(1, "APT29 Analysis", "Analyse APT", "Malware")); + labs.add(new Lab(2, "Red Team Ops", "Pentest réseau", "Pentest")); + labs.add(new Lab(3, "Network Traffic Analysis", "Analyse trafic", "Network")); + labs.add(new Lab(4, "Memory Forensics Lab", "Forensique mémoire", "Forensics")); + labs.add(new Lab(5, "Ransomware Sandbox", "Sandbox malware", "Malware")); + return labs; + } + +// ── Affichage de la liste des labos ────────────────────────────────── + private void refreshLabList() { labListContainer.getChildren().clear(); - labListContainer.getChildren().add( - tg.cyberlabmanager.ui.LabListView.createLabCard("APT29 Analysis", "Malware", "badge-malware", false)); - labListContainer.getChildren().add( - tg.cyberlabmanager.ui.LabListView.createLabCard("Red Team Ops", "Pentest", "badge-pentest", false)); - labListContainer.getChildren().add(tg.cyberlabmanager.ui.LabListView.createLabCard("Network Traffic Analysis", - "Network", "badge-network", true)); - labListContainer.getChildren().add(tg.cyberlabmanager.ui.LabListView.createLabCard("Memory Forensics Lab", - "Forensics", "badge-forensics", false)); - labListContainer.getChildren().add(tg.cyberlabmanager.ui.LabListView.createLabCard("Ransomware Sandbox", - "Malware", "badge-malware", false)); - - labTitleLabel.setText("Network Traffic Analysis"); - btnExportPdf.setText("Export PDF"); - btnExportPdf.setVisible(true); - btnExportPdf.setManaged(true); - statsBar.setVisible(true); - statsBar.setManaged(true); - statTotal.setText("1"); - statRunning.setText("1"); - statSnapshots.setText("4"); - - vmTableHeader.setVisible(true); - vmTableHeader.setManaged(true); - vmEmptyLabel.setVisible(false); - vmEmptyLabel.setManaged(false); - vmListContainer.getChildren().clear(); - vmListContainer.getChildren().add(tg.cyberlabmanager.ui.VmTableView.createVmRow("Wireshark-Station", - "f8c9a7b6...", "Running", "Ubuntu 22.04")); - - selectedVmNameLabel.setText("Wireshark-Station"); - controlsPlaceholder.setVisible(false); - controlsPlaceholder.setManaged(false); - btnStop.setText(" STOP"); - btnStop.setVisible(true); - btnStop.setManaged(true); - btnSaveState.setText(" Save State"); - btnSaveState.setVisible(true); - btnSaveState.setManaged(true); - btnTakeSnapshot.setText(" Take Snapshot"); - btnTakeSnapshot.setVisible(true); - btnTakeSnapshot.setManaged(true); - - snapshotCombo.getItems().add("Select snapshot"); - snapshotCombo.getSelectionModel().selectFirst(); - - journalArea.setText("[19:23:15] Started malware execution in controlled environment\n" + - "[19:24:02] Detected outbound connection to 192.168.1.105\n" + - "[19:25:44] File system changes observed in C:\\Users\\Admin\\AppData\n" + - "[19:27:10] Registry modifications detected:\nHKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\n" - + - "[19:30:55] Network beacon established - C2 communication active\n" + - "[19:35:12] Process injection detected: svchost.exe -> explorer.exe"); + + // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() + List labs = mockLabs(); + + for (Lab lab : labs) { + boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId(); + + String badgeStyle = switch (lab.getCategory().toLowerCase()) { + case "malware" -> + "badge-malware"; + case "pentest" -> + "badge-pentest"; + case "forensics" -> + "badge-forensics"; + case "network" -> + "badge-network"; + default -> + "badge-default"; + }; + + javafx.scene.layout.VBox card = LabListView.createLabCard( + lab.getTitle(), lab.getCategory(), badgeStyle, isSelected + ); + + // Listener de clic — Jour 2 + card.setOnMouseClicked(e -> onLabCardClicked(lab)); + + labListContainer.getChildren().add(card); + } } /** @@ -152,9 +142,7 @@ public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, Pd } public void initView() { - if (statusLabel != null) { - statusLabel.setText("CyberLab Manager — Prêt"); - } + } public List getAllLabs() { @@ -264,4 +252,8 @@ private void handleToggleAuditLogs() { public void shutdown() { executor.shutdownNow(); } -} \ No newline at end of file + + private void onLabCardClicked(Lab lab) { + throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + } +} From 9db79f0fc9fcffd1144b20dc736307ae3fa50c65 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 11:58:40 +0000 Subject: [PATCH 18/27] feat(ui): phase1B - selection labo affiche les VMs associees --- .../controller/LabController.java | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index be07f1a..f6554e3 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -11,6 +11,8 @@ import tg.cyberlabmanager.pdf.PdfExporter; import tg.cyberlabmanager.model.VMStatus; import tg.cyberlabmanager.ui.LabListView; +import tg.cyberlabmanager.ui.VmTableView; + import java.util.List; import java.util.concurrent.ExecutorService; @@ -28,6 +30,7 @@ public class LabController { private DatabaseManager databaseManager; private PdfExporter pdfExporter; private Lab selectedLab; + private VirtualMachine selectedVM; private final ExecutorService executor = Executors.newCachedThreadPool(); @@ -124,6 +127,92 @@ private void refreshLabList() { } } + private List mockVMsForLab(int labId) { + List vms = new ArrayList<>(); + + if (labId == 1) { // APT29 Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali-APT29"); + vm.setUuid("a1b2c3d4-1111-2222-3333-444455556666"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 2) { // Red Team Ops + VirtualMachine vm = new VirtualMachine(); + vm.setName("Metasploitable3"); + vm.setUuid("b2c3d4e5-2222-3333-4444-555566667777"); + vm.setStatus(VMStatus.POWERED_OFF); + vms.add(vm); + } else if (labId == 3) { // Network Traffic Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Wireshark-Station"); + vm.setUuid("f8c9a7b6-3333-4444-5555-666677778888"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 4) { // Memory Forensics + VirtualMachine vm = new VirtualMachine(); + vm.setName("REMnux"); + vm.setUuid("c3d4e5f6-4444-5555-6666-777788889999"); + vm.setStatus(VMStatus.SAVED); + vms.add(vm); + } + // labId == 5 (Ransomware Sandbox) → liste vide intentionnellement + return vms; + } + + private void refreshVmTable(List vms) { + vmListContainer.getChildren().clear(); + + if (vms.isEmpty()) { + vmEmptyLabel.setVisible(true); + vmEmptyLabel.setManaged(true); + statTotal.setText("0"); + statRunning.setText("0"); + statSnapshots.setText("0"); + return; + } + + vmEmptyLabel.setVisible(false); + vmEmptyLabel.setManaged(false); + + long runningCount = vms.stream() + .filter(v -> v.getStatus() == VMStatus.RUNNING) + .count(); + long snapCount = vms.stream() + .mapToLong(v -> v.getSnapshots().size()) + .sum(); + + statTotal.setText(String.valueOf(vms.size())); + statRunning.setText(String.valueOf(runningCount)); + statSnapshots.setText(String.valueOf(snapCount)); + + for (VirtualMachine vm : vms) { + String shortUuid = vm.getUuid() != null && vm.getUuid().length() > 8 + ? vm.getUuid().substring(0, 8) + "..." + : vm.getUuid(); + + String statusText = switch (vm.getStatus()) { + case RUNNING -> + "Running"; + case POWERED_OFF -> + "Stopped"; + case SAVED -> + "Saved"; + case PAUSED -> + "Paused"; + default -> + "Unknown"; + }; + + javafx.scene.layout.HBox row = VmTableView.createVmRow( + vm.getName(), shortUuid, statusText, "" + ); + + // Listener clic VM → Jour 3 + row.setOnMouseClicked(e -> onVmRowClicked(vm)); + vmListContainer.getChildren().add(row); + } + } + /** * Constructeur par défaut initialisant le contrôleur du laboratoire. */ @@ -142,7 +231,7 @@ public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, Pd } public void initView() { - + } public List getAllLabs() { @@ -254,6 +343,41 @@ public void shutdown() { } private void onLabCardClicked(Lab lab) { - throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody + this.selectedLab = lab; + this.selectedVM = null; + + // Mettre à jour le titre + labTitleLabel.setText(lab.getTitle()); + + // Afficher les éléments cachés + btnExportPdf.setVisible(true); + btnExportPdf.setManaged(true); + statsBar.setVisible(true); + statsBar.setManaged(true); + vmTableHeader.setVisible(true); + vmTableHeader.setManaged(true); + + // Remettre le panneau droit en état vide + selectedVmNameLabel.setText(""); + controlsPlaceholder.setVisible(true); + controlsPlaceholder.setManaged(true); + btnStart.setVisible(false); + btnStart.setManaged(false); + btnStop.setVisible(false); + btnStop.setManaged(false); + btnSaveState.setVisible(false); + btnSaveState.setManaged(false); + btnTakeSnapshot.setVisible(false); + btnTakeSnapshot.setManaged(false); + + // Rafraîchir la liste et le tableau + refreshLabList(); + + // TODO (Rôle 2) : remplacer mockVMsForLab par databaseManager.getVMsForLab + refreshVmTable(mockVMsForLab(lab.getId())); + } + + private void onVmRowClicked(VirtualMachine vm) { + // TODO Jour 3 : implémenter la sélection VM } } From 8db50264b9ff1e451ad12eac982ed914aedc1be5 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 12:15:39 +0000 Subject: [PATCH 19/27] feat(ui): selection VM active les controles contextuels --- .../controller/LabController.java | 202 ++++++++++-------- 1 file changed, 114 insertions(+), 88 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index f6554e3..ebd4492 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -13,12 +13,15 @@ import tg.cyberlabmanager.ui.LabListView; import tg.cyberlabmanager.ui.VmTableView; - import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.ArrayList; +import static tg.cyberlabmanager.model.VMStatus.PAUSED; +import static tg.cyberlabmanager.model.VMStatus.POWERED_OFF; +import static tg.cyberlabmanager.model.VMStatus.RUNNING; +import static tg.cyberlabmanager.model.VMStatus.SAVED; /** * Contrôleur principal responsable de l'affichage de l'interface (Rôle 1). Il @@ -127,92 +130,6 @@ private void refreshLabList() { } } - private List mockVMsForLab(int labId) { - List vms = new ArrayList<>(); - - if (labId == 1) { // APT29 Analysis - VirtualMachine vm = new VirtualMachine(); - vm.setName("Kali-APT29"); - vm.setUuid("a1b2c3d4-1111-2222-3333-444455556666"); - vm.setStatus(VMStatus.RUNNING); - vms.add(vm); - } else if (labId == 2) { // Red Team Ops - VirtualMachine vm = new VirtualMachine(); - vm.setName("Metasploitable3"); - vm.setUuid("b2c3d4e5-2222-3333-4444-555566667777"); - vm.setStatus(VMStatus.POWERED_OFF); - vms.add(vm); - } else if (labId == 3) { // Network Traffic Analysis - VirtualMachine vm = new VirtualMachine(); - vm.setName("Wireshark-Station"); - vm.setUuid("f8c9a7b6-3333-4444-5555-666677778888"); - vm.setStatus(VMStatus.RUNNING); - vms.add(vm); - } else if (labId == 4) { // Memory Forensics - VirtualMachine vm = new VirtualMachine(); - vm.setName("REMnux"); - vm.setUuid("c3d4e5f6-4444-5555-6666-777788889999"); - vm.setStatus(VMStatus.SAVED); - vms.add(vm); - } - // labId == 5 (Ransomware Sandbox) → liste vide intentionnellement - return vms; - } - - private void refreshVmTable(List vms) { - vmListContainer.getChildren().clear(); - - if (vms.isEmpty()) { - vmEmptyLabel.setVisible(true); - vmEmptyLabel.setManaged(true); - statTotal.setText("0"); - statRunning.setText("0"); - statSnapshots.setText("0"); - return; - } - - vmEmptyLabel.setVisible(false); - vmEmptyLabel.setManaged(false); - - long runningCount = vms.stream() - .filter(v -> v.getStatus() == VMStatus.RUNNING) - .count(); - long snapCount = vms.stream() - .mapToLong(v -> v.getSnapshots().size()) - .sum(); - - statTotal.setText(String.valueOf(vms.size())); - statRunning.setText(String.valueOf(runningCount)); - statSnapshots.setText(String.valueOf(snapCount)); - - for (VirtualMachine vm : vms) { - String shortUuid = vm.getUuid() != null && vm.getUuid().length() > 8 - ? vm.getUuid().substring(0, 8) + "..." - : vm.getUuid(); - - String statusText = switch (vm.getStatus()) { - case RUNNING -> - "Running"; - case POWERED_OFF -> - "Stopped"; - case SAVED -> - "Saved"; - case PAUSED -> - "Paused"; - default -> - "Unknown"; - }; - - javafx.scene.layout.HBox row = VmTableView.createVmRow( - vm.getName(), shortUuid, statusText, "" - ); - - // Listener clic VM → Jour 3 - row.setOnMouseClicked(e -> onVmRowClicked(vm)); - vmListContainer.getChildren().add(row); - } - } - /** * Constructeur par défaut initialisant le contrôleur du laboratoire. */ @@ -342,6 +259,92 @@ public void shutdown() { executor.shutdownNow(); } + private List mockVMsForLab(int labId) { + List vms = new ArrayList<>(); + + if (labId == 1) { // APT29 Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali-APT29"); + vm.setUuid("a1b2c3d4-1111-2222-3333-444455556666"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 2) { // Red Team Ops + VirtualMachine vm = new VirtualMachine(); + vm.setName("Metasploitable3"); + vm.setUuid("b2c3d4e5-2222-3333-4444-555566667777"); + vm.setStatus(VMStatus.POWERED_OFF); + vms.add(vm); + } else if (labId == 3) { // Network Traffic Analysis + VirtualMachine vm = new VirtualMachine(); + vm.setName("Wireshark-Station"); + vm.setUuid("f8c9a7b6-3333-4444-5555-666677778888"); + vm.setStatus(VMStatus.RUNNING); + vms.add(vm); + } else if (labId == 4) { // Memory Forensics + VirtualMachine vm = new VirtualMachine(); + vm.setName("REMnux"); + vm.setUuid("c3d4e5f6-4444-5555-6666-777788889999"); + vm.setStatus(VMStatus.SAVED); + vms.add(vm); + } + // labId == 5 (Ransomware Sandbox) → liste vide intentionnellement + return vms; + } + + private void refreshVmTable(List vms) { + vmListContainer.getChildren().clear(); + + if (vms.isEmpty()) { + vmEmptyLabel.setVisible(true); + vmEmptyLabel.setManaged(true); + statTotal.setText("0"); + statRunning.setText("0"); + statSnapshots.setText("0"); + return; + } + + vmEmptyLabel.setVisible(false); + vmEmptyLabel.setManaged(false); + + long runningCount = vms.stream() + .filter(v -> v.getStatus() == VMStatus.RUNNING) + .count(); + long snapCount = vms.stream() + .mapToLong(v -> v.getSnapshots().size()) + .sum(); + + statTotal.setText(String.valueOf(vms.size())); + statRunning.setText(String.valueOf(runningCount)); + statSnapshots.setText(String.valueOf(snapCount)); + + for (VirtualMachine vm : vms) { + String shortUuid = vm.getUuid() != null && vm.getUuid().length() > 8 + ? vm.getUuid().substring(0, 8) + "..." + : vm.getUuid(); + + String statusText = switch (vm.getStatus()) { + case RUNNING -> + "Running"; + case POWERED_OFF -> + "Stopped"; + case SAVED -> + "Saved"; + case PAUSED -> + "Paused"; + default -> + "Unknown"; + }; + + javafx.scene.layout.HBox row = VmTableView.createVmRow( + vm.getName(), shortUuid, statusText, "" + ); + + // Listener clic VM → Jour 3 + row.setOnMouseClicked(e -> onVmRowClicked(vm)); + vmListContainer.getChildren().add(row); + } + } + private void onLabCardClicked(Lab lab) { this.selectedLab = lab; this.selectedVM = null; @@ -378,6 +381,29 @@ private void onLabCardClicked(Lab lab) { } private void onVmRowClicked(VirtualMachine vm) { - // TODO Jour 3 : implémenter la sélection VM + this.selectedVM = vm; + + selectedVmNameLabel.setText(vm.getName()); + + // Cacher le placeholder "sélectionnez une VM" + controlsPlaceholder.setVisible(false); + controlsPlaceholder.setManaged(false); + + // Activer les bons boutons selon le statut de la VM + updateControlButtons(vm.getStatus()); + } + + private void updateControlButtons(VMStatus status) { + boolean isRunning = status == VMStatus.RUNNING; + boolean isStopped = status == VMStatus.POWERED_OFF || status == VMStatus.SAVED; + + btnStart.setVisible(isStopped); + btnStart.setManaged(isStopped); + btnStop.setVisible(isRunning); + btnStop.setManaged(isRunning); + btnSaveState.setVisible(isRunning); + btnSaveState.setManaged(isRunning); + btnTakeSnapshot.setVisible(true); + btnTakeSnapshot.setManaged(true); } } From ff897fdbe21f6ad06ca8c229073484ae1a045f3e Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 13:27:25 +0000 Subject: [PATCH 20/27] feat(hypervisor): mock hypervisor conforme au contrat IHypervisor reel --- .../controller/LabController.java | 78 ++++++++++++++++++- .../hypervisor/MockHypervisor.java | 60 ++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 src/main/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index ebd4492..48aba99 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -80,6 +80,8 @@ public class LabController { @FXML private void initialize() { + // TODO (Rôle 3) : remplacer par la vraie implémentation dès qu'elle est livrée + this.hypervisor = new tg.cyberlabmanager.hypervisor.MockHypervisor(); mockLabs(); refreshLabList(); @@ -163,11 +165,34 @@ public void onStartVMClicked(VirtualMachine vm, Runnable onSuccess, Consumer onError) { + executor.submit(() -> { + try { + hypervisor.startVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.RUNNING); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); } public void onStopVMClicked(VirtualMachine vm, Runnable onSuccess, Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.stopVM(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.POWERED_OFF); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); } public void onSaveStateClicked(VirtualMachine vm, @@ -222,12 +247,46 @@ private void handleExportPDF() { @FXML private void handleStartVM() { - // TODO (S3) : appeler onStartVMClicked() + if (selectedVM == null) { + return; + } + + btnStart.setDisable(true); + + onStartVMClicked(selectedVM, + () -> { + btnStart.setDisable(false); + updateControlButtons(VMStatus.RUNNING); + refreshVmTable(mockVMsForLab(selectedLab.getId())); + appendJournalLine("VM started successfully"); + }, + errorMsg -> { + btnStart.setDisable(false); + showError("Erreur démarrage VM", errorMsg); + } + ); } @FXML private void handleStopVM() { - // TODO (S3) : appeler onStopVMClicked() + if (selectedVM == null) { + return; + } + + btnStop.setDisable(true); + + onStopVMClicked(selectedVM, + () -> { + btnStop.setDisable(false); + updateControlButtons(VMStatus.POWERED_OFF); + refreshVmTable(mockVMsForLab(selectedLab.getId())); + appendJournalLine("VM stopped"); + }, + errorMsg -> { + btnStop.setDisable(false); + showError("Erreur arrêt VM", errorMsg); + } + ); } @FXML @@ -406,4 +465,19 @@ private void updateControlButtons(VMStatus status) { btnTakeSnapshot.setVisible(true); btnTakeSnapshot.setManaged(true); } + + private void appendJournalLine(String text) { + String time = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + journalArea.appendText("[" + time + "] " + text + "\n"); + } + + private void showError(String title, String msg) { + javafx.scene.control.Alert alert = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.showAndWait(); + } } diff --git a/src/main/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java b/src/main/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java new file mode 100644 index 0000000..6fe3480 --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/hypervisor/MockHypervisor.java @@ -0,0 +1,60 @@ +package tg.cyberlabmanager.hypervisor; + +import tg.cyberlabmanager.model.VMStatus; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implémentation factice de IHypervisor (Rôle 1 - Rail A). + * Simule le comportement d'un hyperviseur réel avec une latence réseau, + * pour permettre de tester Start/Stop/Snapshot sans dépendre de Rôle 3. + * + * TODO (Rôle 3) : remplacer par l'implémentation réelle une fois livrée. + */ +public class MockHypervisor implements IHypervisor { + + @Override + public List listVMs() throws HypervisorException { + return new ArrayList<>(); + } + + @Override + public VMStatus getStatus(String uuid) throws HypervisorException { + return VMStatus.POWERED_OFF; + } + + @Override + public void startVM(String uuid) throws HypervisorException { + sleep(1200); + } + + @Override + public void stopVM(String uuid) throws HypervisorException { + sleep(800); + } + + @Override + public void saveState(String uuid) throws HypervisorException { + sleep(600); + } + + @Override + public void takeSnapshot(String uuid, String name, String description) throws HypervisorException { + sleep(500); + } + + @Override + public void restoreSnapshot(String uuid, String snapshotName) throws HypervisorException { + sleep(700); + } + + private void sleep(long millis) throws HypervisorException { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new HypervisorException("Opération interrompue", e); + } + } +} \ No newline at end of file From cb3c964f0f32defa0fc2abbbc4eaac28db40e23a Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 14:28:59 +0000 Subject: [PATCH 21/27] feat(ui): snapshots take/restore en asynchrone --- .../controller/LabController.java | 125 +++++++++++++++++- 1 file changed, 119 insertions(+), 6 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 48aba99..3d086cf 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -36,6 +36,7 @@ public class LabController { private VirtualMachine selectedVM; private final ExecutorService executor = Executors.newCachedThreadPool(); + private final java.util.Map> vmsByLab = new java.util.HashMap<>(); @FXML private Label statusLabel; @@ -83,6 +84,7 @@ private void initialize() { // TODO (Rôle 3) : remplacer par la vraie implémentation dès qu'elle est livrée this.hypervisor = new tg.cyberlabmanager.hypervisor.MockHypervisor(); mockLabs(); + initMockVMs(); refreshLabList(); } @@ -98,6 +100,12 @@ private List mockLabs() { return labs; } + private void initMockVMs() { + for (int labId = 1; labId <= 5; labId++) { + vmsByLab.put(labId, mockVMsForLab(labId)); + } + } + // ── Affichage de la liste des labos ────────────────────────────────── private void refreshLabList() { labListContainer.getChildren().clear(); @@ -153,6 +161,11 @@ public void initView() { } + private List getVMsForLab(int labId) { + // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) + return vmsByLab.getOrDefault(labId, new ArrayList<>()); + } + public List getAllLabs() { throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); } @@ -198,6 +211,18 @@ public void onStopVMClicked(VirtualMachine vm, public void onSaveStateClicked(VirtualMachine vm, Runnable onSuccess, Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.saveState(vm.getUuid()); + javafx.application.Platform.runLater(() -> { + vm.setStatus(VMStatus.SAVED); + onSuccess.run(); + }); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); } public void onTakeSnapshotClicked(VirtualMachine vm, @@ -205,12 +230,30 @@ public void onTakeSnapshotClicked(VirtualMachine vm, String description, Runnable onSuccess, Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.takeSnapshot(vm.getUuid(), name, description); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); } public void onRestoreSnapshotClicked(VirtualMachine vm, String snapshotName, Runnable onSuccess, Consumer onError) { + + executor.submit(() -> { + try { + hypervisor.restoreSnapshot(vm.getUuid(), snapshotName); + javafx.application.Platform.runLater(onSuccess); + } catch (Exception e) { + javafx.application.Platform.runLater(() -> onError.accept(e.getMessage())); + } + }); } public void onAddJournalEntry(VirtualMachine vm, String text) { @@ -257,7 +300,7 @@ private void handleStartVM() { () -> { btnStart.setDisable(false); updateControlButtons(VMStatus.RUNNING); - refreshVmTable(mockVMsForLab(selectedLab.getId())); + refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM started successfully"); }, errorMsg -> { @@ -279,7 +322,7 @@ private void handleStopVM() { () -> { btnStop.setDisable(false); updateControlButtons(VMStatus.POWERED_OFF); - refreshVmTable(mockVMsForLab(selectedLab.getId())); + refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM stopped"); }, errorMsg -> { @@ -291,17 +334,87 @@ private void handleStopVM() { @FXML private void handleSaveState() { - // TODO (S3) : appeler onSaveStateClicked() + if (selectedVM == null) { + return; + } + + btnSaveState.setDisable(true); + + onSaveStateClicked(selectedVM, + () -> { + btnSaveState.setDisable(false); + updateControlButtons(VMStatus.SAVED); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM state saved"); + }, + errorMsg -> { + btnSaveState.setDisable(false); + showError("Erreur sauvegarde état", errorMsg); + } + ); } @FXML private void handleTakeSnapshot() { - // TODO (S3) : appeler onTakeSnapshotClicked() + if (selectedVM == null) { + return; + } + + javafx.scene.control.TextInputDialog dialog = new javafx.scene.control.TextInputDialog(); + dialog.setTitle("Nouveau snapshot"); + dialog.setHeaderText("Nom du snapshot"); + dialog.setContentText("Nom :"); + + dialog.showAndWait().ifPresent(name -> { + if (name.isBlank()) { + return; + } + + onTakeSnapshotClicked(selectedVM, name, "", + () -> { + tg.cyberlabmanager.model.Snapshot snapshot = new tg.cyberlabmanager.model.Snapshot(); + snapshot.setName(name); + snapshot.setDescription(""); + snapshot.setCreatedAt(java.time.LocalDateTime.now()); + snapshot.setOnline(selectedVM.getStatus() == VMStatus.RUNNING); + + selectedVM.addSnapshot(snapshot); + snapshotCombo.getItems().add(name); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("Snapshot \"" + name + "\" created"); + }, + errorMsg -> showError("Erreur snapshot", errorMsg) + ); + }); } @FXML private void handleRestoreSnapshot() { - // TODO (S3) : appeler onRestoreSnapshotClicked() + if (selectedVM == null) { + return; + } + + String snapshotName = snapshotCombo.getValue(); + if (snapshotName == null) { + return; + } + + javafx.scene.control.Alert confirm = new javafx.scene.control.Alert( + javafx.scene.control.Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirmer la restauration"); + confirm.setHeaderText(null); + confirm.setContentText("Restaurer le snapshot \"" + snapshotName + "\" ?\nL'état actuel de la VM sera perdu."); + + confirm.showAndWait().ifPresent(response -> { + if (response != javafx.scene.control.ButtonType.OK) { + return; + } + + onRestoreSnapshotClicked(selectedVM, snapshotName, + () -> appendJournalLine("Snapshot \"" + snapshotName + "\" restored"), + errorMsg -> showError("Erreur restauration snapshot", errorMsg) + ); + }); } @FXML @@ -436,7 +549,7 @@ private void onLabCardClicked(Lab lab) { refreshLabList(); // TODO (Rôle 2) : remplacer mockVMsForLab par databaseManager.getVMsForLab - refreshVmTable(mockVMsForLab(lab.getId())); + refreshVmTable(getVMsForLab(lab.getId())); } private void onVmRowClicked(VirtualMachine vm) { From 3fcd5501e2145a6dbe48527c3829e88fee4ffbf1 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 14:47:54 +0000 Subject: [PATCH 22/27] feat(ui): journal utilisateur dynamique avec horodatage --- .../tg/cyberlabmanager/controller/LabController.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 3d086cf..8e1fa44 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -78,6 +78,8 @@ public class LabController { private javafx.scene.control.TextArea journalArea; @FXML private javafx.scene.layout.VBox snapshotSection; + @FXML + private javafx.scene.control.TextField journalInputField; @FXML private void initialize() { @@ -419,7 +421,15 @@ private void handleRestoreSnapshot() { @FXML private void handleAddJournalEntry() { - // TODO (S3) : lire journalInputField + appeler onAddJournalEntry() + String text = journalInputField.getText(); + if (text == null || text.isBlank()) { + return; + } + + appendJournalLine(text); + journalInputField.clear(); + + // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) } @FXML From e5cc78111006897908277d035b2ce4923ede9bf8 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 15:17:29 +0000 Subject: [PATCH 23/27] feat(ui): audit automatique + accordeon system audit logs --- .../controller/LabController.java | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 8e1fa44..d50caee 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -34,6 +34,7 @@ public class LabController { private PdfExporter pdfExporter; private Lab selectedLab; private VirtualMachine selectedVM; + private boolean auditExpanded = false; private final ExecutorService executor = Executors.newCachedThreadPool(); private final java.util.Map> vmsByLab = new java.util.HashMap<>(); @@ -80,6 +81,10 @@ public class LabController { private javafx.scene.layout.VBox snapshotSection; @FXML private javafx.scene.control.TextField journalInputField; + @FXML + private javafx.scene.control.TextArea auditArea; + @FXML + private Label auditArrow; @FXML private void initialize() { @@ -304,6 +309,7 @@ private void handleStartVM() { updateControlButtons(VMStatus.RUNNING); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM started successfully"); + appendAuditLine("VM démarrée"); }, errorMsg -> { btnStart.setDisable(false); @@ -326,6 +332,7 @@ private void handleStopVM() { updateControlButtons(VMStatus.POWERED_OFF); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM stopped"); + appendAuditLine("VM arrêtée"); }, errorMsg -> { btnStop.setDisable(false); @@ -348,6 +355,7 @@ private void handleSaveState() { updateControlButtons(VMStatus.SAVED); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM state saved"); + appendAuditLine("État sauvegardé"); }, errorMsg -> { btnSaveState.setDisable(false); @@ -384,6 +392,7 @@ private void handleTakeSnapshot() { snapshotCombo.getItems().add(name); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("Snapshot \"" + name + "\" created"); + appendAuditLine("Snapshot \"" + name + "\" créé"); }, errorMsg -> showError("Erreur snapshot", errorMsg) ); @@ -416,6 +425,7 @@ private void handleRestoreSnapshot() { () -> appendJournalLine("Snapshot \"" + snapshotName + "\" restored"), errorMsg -> showError("Erreur restauration snapshot", errorMsg) ); + appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); }); } @@ -434,7 +444,10 @@ private void handleAddJournalEntry() { @FXML private void handleToggleAuditLogs() { - // TODO (S3) : toggle visible/managed sur auditArea + rotation auditArrow + auditExpanded = !auditExpanded; + auditArea.setVisible(auditExpanded); + auditArea.setManaged(auditExpanded); + auditArrow.setText(auditExpanded ? "⌄" : "›"); } public void shutdown() { @@ -603,4 +616,11 @@ private void showError(String title, String msg) { alert.setContentText(msg); alert.showAndWait(); } + + private void appendAuditLine(String action) { + String time = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")); + String vmName = selectedVM != null ? selectedVM.getName() : "?"; + auditArea.appendText("[" + time + "] [" + vmName + "] " + action + "\n"); + } } From bec3661926c5f0d599efb28c4f8a9a2df2a885ff Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 16:21:03 +0000 Subject: [PATCH 24/27] feat(ui): import VM avec FileChooser (stub) --- .../controller/LabController.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index d50caee..fcc8016 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -2,6 +2,8 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.stage.FileChooser; +import java.io.File; import tg.cyberlabmanager.data.DatabaseManager; import tg.cyberlabmanager.hypervisor.IHypervisor; @@ -286,10 +288,34 @@ public void onConfigUpdated(AppConfig newConfig) { } @FXML - private void handleImportVM() { - // TODO (S3) : ouvrir dialogue import VM +private void handleImportVM() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Importer une VM"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("VirtualBox Appliance", "*.ova"), + new FileChooser.ExtensionFilter("Disque virtuel", "*.vdi") + ); + + File selectedFile = fileChooser.showOpenDialog( + labListContainer.getScene().getWindow() + ); + + if (selectedFile == null) { + return; } + // TODO (Rôle 3) : remplacer par un appel réel à hypervisor.importVM() + onImportVMsClicked( + vms -> { + appendJournalLine("Import terminé : " + selectedFile.getName()); + appendAuditLine("VM importée depuis " + selectedFile.getName()); + // Pourrait rafraîchir la liste des VM du laboratoire courant + // if (selectedLab != null) refreshVmTable(getVMsForLab(selectedLab.getId())); + }, + errorMsg -> showError("Erreur d'import", errorMsg) + ); +} + @FXML private void handleExportPDF() { // TODO (S4) : ouvrir FileChooser + appeler onExportLabClicked() From a6bd6a3c441e3a286c737318433075540ed7e723 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 16:32:38 +0000 Subject: [PATCH 25/27] feat(ui): export PDF avec FileChooser (stub) --- .../controller/LabController.java | 84 ++++++++++++------- 1 file changed, 56 insertions(+), 28 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index fcc8016..7acea46 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -288,37 +288,65 @@ public void onConfigUpdated(AppConfig newConfig) { } @FXML -private void handleImportVM() { - FileChooser fileChooser = new FileChooser(); - fileChooser.setTitle("Importer une VM"); - fileChooser.getExtensionFilters().addAll( - new FileChooser.ExtensionFilter("VirtualBox Appliance", "*.ova"), - new FileChooser.ExtensionFilter("Disque virtuel", "*.vdi") - ); - - File selectedFile = fileChooser.showOpenDialog( - labListContainer.getScene().getWindow() - ); - - if (selectedFile == null) { - return; - } - - // TODO (Rôle 3) : remplacer par un appel réel à hypervisor.importVM() - onImportVMsClicked( - vms -> { - appendJournalLine("Import terminé : " + selectedFile.getName()); - appendAuditLine("VM importée depuis " + selectedFile.getName()); - // Pourrait rafraîchir la liste des VM du laboratoire courant - // if (selectedLab != null) refreshVmTable(getVMsForLab(selectedLab.getId())); - }, - errorMsg -> showError("Erreur d'import", errorMsg) - ); -} + private void handleImportVM() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Importer une VM"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("VirtualBox Appliance", "*.ova"), + new FileChooser.ExtensionFilter("Disque virtuel", "*.vdi") + ); + + File selectedFile = fileChooser.showOpenDialog( + labListContainer.getScene().getWindow() + ); + + if (selectedFile == null) { + return; + } + + // TODO (Rôle 3) : remplacer par un appel réel à hypervisor.importVM() + onImportVMsClicked( + vms -> { + appendJournalLine("Import terminé : " + selectedFile.getName()); + appendAuditLine("VM importée depuis " + selectedFile.getName()); + // Pourrait rafraîchir la liste des VM du laboratoire courant + // if (selectedLab != null) refreshVmTable(getVMsForLab(selectedLab.getId())); + }, + errorMsg -> showError("Erreur d'import", errorMsg) + ); + } @FXML private void handleExportPDF() { - // TODO (S4) : ouvrir FileChooser + appeler onExportLabClicked() + // Le bouton Export PDF n'est visible que si un labo est sélectionné, + // mais on vérifie par précaution. + if (selectedLab == null) { + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Exporter le rapport PDF"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("PDF Document", "*.pdf") + ); + // Suggestion de nom de fichier basé sur le titre du labo + fileChooser.setInitialFileName(selectedLab.getTitle().replace(" ", "_") + ".pdf"); + + File selectedFile = fileChooser.showSaveDialog( + labListContainer.getScene().getWindow() + ); + + if (selectedFile == null) { + return; // L'utilisateur a annulé + } + + // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() + // Pour l'instant, on appelle le stub métier (déjà présent dans LabController) + onExportLabClicked(selectedLab, selectedFile.getAbsolutePath()); + + // Ajout des traces immédiates (le stub ne fait rien pour l'instant) + appendJournalLine("Export PDF demandé pour " + selectedLab.getTitle()); + appendAuditLine("Export PDF demandé pour " + selectedLab.getTitle()); } @FXML From 757056dbe7a8554670dcaaa20222f1530fcae7e5 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 17:37:31 +0000 Subject: [PATCH 26/27] feat(ui): check --- .../tg/cyberlabmanager/controller/LabController.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 7acea46..0214b7f 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -158,6 +158,9 @@ public LabController() { public void setDependencies(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { + this.hypervisor = hypervisor; + this.databaseManager = databaseManager; + this.pdfExporter = pdfExporter; } public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, PdfExporter pdfExporter) { @@ -476,14 +479,17 @@ private void handleRestoreSnapshot() { } onRestoreSnapshotClicked(selectedVM, snapshotName, - () -> appendJournalLine("Snapshot \"" + snapshotName + "\" restored"), + () -> { + appendJournalLine("Snapshot \"" + snapshotName + "\" restored"); + appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); + }, errorMsg -> showError("Erreur restauration snapshot", errorMsg) ); - appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); }); } @FXML + private void handleAddJournalEntry() { String text = journalInputField.getText(); if (text == null || text.isBlank()) { From 8baa5866249acec6546dd0306cbd4256849a33f0 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 2 Jul 2026 17:43:13 +0000 Subject: [PATCH 27/27] fix(ui): implementer onImportVMsClicked (mock) + corriger accolades restore snapshot --- .../controller/LabController.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 0214b7f..6074c08 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -274,6 +274,25 @@ public void onAddJournalEntry(VirtualMachine vm, String text) { public void onImportVMsClicked(Consumer> onSuccess, Consumer onError) { + + executor.submit(() -> { + try { + Thread.sleep(800); // simule le temps d'import + javafx.application.Platform.runLater(() -> { + VirtualMachine imported = new VirtualMachine(); + imported.setName("Imported-VM-" + (System.currentTimeMillis() % 1000)); + imported.setUuid(java.util.UUID.randomUUID().toString()); + imported.setStatus(VMStatus.POWERED_OFF); + + List result = new ArrayList<>(); + result.add(imported); + onSuccess.accept(result); + }); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + javafx.application.Platform.runLater(() -> onError.accept("Import interrompu")); + } + }); } public void onExportLabClicked(Lab lab, String filePath) { @@ -310,10 +329,12 @@ private void handleImportVM() { // TODO (Rôle 3) : remplacer par un appel réel à hypervisor.importVM() onImportVMsClicked( vms -> { + if (selectedLab != null) { + getVMsForLab(selectedLab.getId()).addAll(vms); + refreshVmTable(getVMsForLab(selectedLab.getId())); + } appendJournalLine("Import terminé : " + selectedFile.getName()); appendAuditLine("VM importée depuis " + selectedFile.getName()); - // Pourrait rafraîchir la liste des VM du laboratoire courant - // if (selectedLab != null) refreshVmTable(getVMsForLab(selectedLab.getId())); }, errorMsg -> showError("Erreur d'import", errorMsg) );