From 989654a78e80bf711f6271296f7b371eaa9d770c Mon Sep 17 00:00:00 2001 From: akpmarcelin Date: Fri, 29 May 2026 19:20:06 +0000 Subject: [PATCH 01/28] feat(data): add DatabaseManager, PdfExporter and temporary model stubs --- .../cyberlabmanager/data/DatabaseManager.java | 532 +++++++++++++++++- .../tg/cyberlabmanager/model/AppConfig.java | 26 +- .../tg/cyberlabmanager/model/AuditEntry.java | 63 +-- .../cyberlabmanager/model/JournalEntry.java | 43 +- .../java/tg/cyberlabmanager/model/Lab.java | 78 +-- .../tg/cyberlabmanager/model/Snapshot.java | 53 +- .../tg/cyberlabmanager/model/VMStatus.java | 3 +- .../cyberlabmanager/model/VirtualMachine.java | 92 +-- .../tg/cyberlabmanager/pdf/PdfExporter.java | 297 +++++++++- 9 files changed, 882 insertions(+), 305 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java index 9cd04a9..56ac366 100644 --- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java +++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java @@ -1,76 +1,562 @@ package tg.cyberlabmanager.data; -import tg.cyberlabmanager.model.AppConfig; -import tg.cyberlabmanager.model.AuditEntry; -import tg.cyberlabmanager.model.JournalEntry; -import tg.cyberlabmanager.model.Lab; -import tg.cyberlabmanager.model.Snapshot; -import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.*; +import java.sql.*; +import java.util.ArrayList; import java.util.List; +/** + * Gestionnaire de la base de données SQLite de Cyber Lab Manager. + * Fournit toutes les opérations CRUD pour les labos, VMs, snapshots, + * journal de bord, audit et configuration. + * + * Utilisation normale : new DatabaseManager("jdbc:sqlite:/home/user/cyberlab.db") + * Utilisation en test : new DatabaseManager("jdbc:sqlite::memory:") + */ public class DatabaseManager { + + private final String jdbcUrl; + + // ========================================================================= + // Constructeur + initialisation + // ========================================================================= + + /** + * Crée le DatabaseManager et initialise les tables si elles n'existent pas. + * + * @param jdbcUrl URL JDBC SQLite, ex: "jdbc:sqlite:/home/user/cyberlab.db" + * ou "jdbc:sqlite::memory:" pour les tests + */ + public DatabaseManager(String jdbcUrl) { + this.jdbcUrl = jdbcUrl; + initDatabase(); + } + + /** + * Ouvre et retourne une connexion SQLite. + * Toujours utilisée dans un try-with-resources pour garantir la fermeture. + */ + private Connection getConnection() throws SQLException { + return DriverManager.getConnection(jdbcUrl); + } + + /** + * Crée toutes les tables si elles n'existent pas encore. + * Appelé automatiquement au démarrage. + */ + private void initDatabase() { + String[] tables = { + // Table des laboratoires + "CREATE TABLE IF NOT EXISTS labo (" + + " id_labo INTEGER PRIMARY KEY AUTOINCREMENT," + + " titre TEXT NOT NULL," + + " description TEXT," + + " categorie TEXT" + + ");", + + // Table des machines virtuelles + "CREATE TABLE IF NOT EXISTS vm (" + + " id_vm INTEGER PRIMARY KEY AUTOINCREMENT," + + " nom_vm TEXT NOT NULL," + + " uuid TEXT UNIQUE," + + " id_labo INTEGER REFERENCES labo(id_labo) ON DELETE SET NULL," + + " lien_doc TEXT," + + " chemin_vboxmanage TEXT" + + ");", + + // Table des snapshots + "CREATE TABLE IF NOT EXISTS snapshot (" + + " id_snap INTEGER PRIMARY KEY AUTOINCREMENT," + + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + + " nom_snap TEXT," + + " date_creation TEXT," + + " description TEXT" + + ");", + + // Table du journal de bord + "CREATE TABLE IF NOT EXISTS journal_entry (" + + " id_entry INTEGER PRIMARY KEY AUTOINCREMENT," + + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + + " horodatage TEXT NOT NULL," + + " contenu TEXT NOT NULL" + + ");", + + // Table d'audit automatique + "CREATE TABLE IF NOT EXISTS audit_log (" + + " id_log INTEGER PRIMARY KEY AUTOINCREMENT," + + " horodatage TEXT NOT NULL," + + " id_vm INTEGER REFERENCES vm(id_vm) ON DELETE SET NULL," + + " action TEXT NOT NULL," + + " details TEXT" + + ");", + + // Table de configuration (clé/valeur) + "CREATE TABLE IF NOT EXISTS app_config (" + + " cle TEXT PRIMARY KEY," + + " valeur TEXT" + + ");" + }; + + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + for (String sql : tables) { + stmt.execute(sql); + } + } catch (SQLException e) { + throw new RuntimeException("Impossible d'initialiser la base de données : " + e.getMessage(), e); + } + } + + // ========================================================================= + // LABORATOIRES + // ========================================================================= + + /** + * Sauvegarde un laboratoire. Fait un INSERT si l'id est 0, un UPDATE sinon. + * Après INSERT, l'id généré est assigné à l'objet lab. + * + * @param lab le laboratoire à sauvegarder + */ public void saveLab(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + if (lab.getId() == 0) { + // INSERT + String sql = "INSERT INTO labo (titre, description, categorie) VALUES (?, ?, ?)"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, lab.getTitre()); + ps.setString(2, lab.getDescription()); + ps.setString(3, lab.getCategorie()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) { + lab.setId(keys.getInt(1)); + } + } catch (SQLException e) { + throw new RuntimeException("Erreur saveLab (INSERT) : " + e.getMessage(), e); + } + } else { + // UPDATE + String sql = "UPDATE labo SET titre=?, description=?, categorie=? WHERE id_labo=?"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, lab.getTitre()); + ps.setString(2, lab.getDescription()); + ps.setString(3, lab.getCategorie()); + ps.setInt(4, lab.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveLab (UPDATE) : " + e.getMessage(), e); + } + } } + /** + * Récupère un laboratoire par son identifiant. + * + * @param id l'identifiant du laboratoire + * @return le Lab correspondant, ou null si introuvable + */ public Lab getLab(int id) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "SELECT id_labo, titre, description, categorie FROM labo WHERE id_labo=?"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, id); + ResultSet rs = ps.executeQuery(); + if (rs.next()) { + return mapLab(rs); + } + } catch (SQLException e) { + throw new RuntimeException("Erreur getLab : " + e.getMessage(), e); + } + return null; } + /** + * Retourne tous les laboratoires enregistrés. + * + * @return liste de tous les Lab, vide si aucun + */ public List getAllLabs() { - throw new UnsupportedOperationException("Not implemented yet"); + List labs = new ArrayList<>(); + String sql = "SELECT id_labo, titre, description, categorie FROM labo ORDER BY titre"; + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) { + labs.add(mapLab(rs)); + } + } catch (SQLException e) { + throw new RuntimeException("Erreur getAllLabs : " + e.getMessage(), e); + } + return labs; } + /** + * Supprime un laboratoire. Les VMs associées deviennent orphelines (id_labo → NULL). + * + * @param lab le laboratoire à supprimer + */ public void deleteLab(Lab lab) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "DELETE FROM labo WHERE id_labo=?"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, lab.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur deleteLab : " + e.getMessage(), e); + } + } + + /** Construit un objet Lab depuis un ResultSet. */ + private Lab mapLab(ResultSet rs) throws SQLException { + Lab lab = new Lab(); + lab.setId(rs.getInt("id_labo")); + lab.setTitre(rs.getString("titre")); + lab.setDescription(rs.getString("description")); + lab.setCategorie(rs.getString("categorie")); + return lab; } + // ========================================================================= + // MACHINES VIRTUELLES + // ========================================================================= + + /** + * Sauvegarde une VM. INSERT si id==0, UPDATE sinon. + * Si labId est null, la VM est orpheline (non rattachée à un labo). + * + * @param vm la machine virtuelle à sauvegarder + * @param labId l'identifiant du labo associé, ou null pour une VM orpheline + */ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { - throw new UnsupportedOperationException("Not implemented yet"); + if (vm.getId() == 0) { + String sql = "INSERT INTO vm (nom_vm, uuid, id_labo, lien_doc, chemin_vboxmanage) VALUES (?,?,?,?,?)"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, vm.getNomVm()); + ps.setString(2, vm.getUuid()); + if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); + ps.setString(4, vm.getLienDoc()); + ps.setString(5, vm.getCheminVboxmanage()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) vm.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveVirtualMachine (INSERT) : " + e.getMessage(), e); + } + } else { + String sql = "UPDATE vm SET nom_vm=?, uuid=?, id_labo=?, lien_doc=?, chemin_vboxmanage=? WHERE id_vm=?"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, vm.getNomVm()); + ps.setString(2, vm.getUuid()); + if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); + ps.setString(4, vm.getLienDoc()); + ps.setString(5, vm.getCheminVboxmanage()); + ps.setInt(6, vm.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveVirtualMachine (UPDATE) : " + e.getMessage(), e); + } + } } + /** + * Retourne toutes les VMs rattachées à un laboratoire. + * + * @param labId l'identifiant du laboratoire + * @return liste des VMs du labo + */ public List getVMsForLab(int labId) { - throw new UnsupportedOperationException("Not implemented yet"); + List vms = new ArrayList<>(); + String sql = "SELECT * FROM vm WHERE id_labo=?"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, labId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) vms.add(mapVM(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getVMsForLab : " + e.getMessage(), e); + } + return vms; } + /** + * Retourne les VMs sans laboratoire associé. + * + * @return liste des VMs orphelines + */ public List getOrphanVMs() { - throw new UnsupportedOperationException("Not implemented yet"); + List vms = new ArrayList<>(); + String sql = "SELECT * FROM vm WHERE id_labo IS NULL"; + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery(sql)) { + while (rs.next()) vms.add(mapVM(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getOrphanVMs : " + e.getMessage(), e); + } + return vms; } + /** + * Détache une VM de son laboratoire (id_labo devient NULL). + * Ne supprime pas la VM de l'application ni de VirtualBox. + * + * @param vm la VM à détacher + */ public void removeVMFromLab(VirtualMachine vm) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "UPDATE vm SET id_labo=NULL WHERE id_vm=?"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, vm.getId()); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur removeVMFromLab : " + e.getMessage(), e); + } + } + + /** Construit un objet VirtualMachine depuis un ResultSet. */ + private VirtualMachine mapVM(ResultSet rs) throws SQLException { + VirtualMachine vm = new VirtualMachine(); + vm.setId(rs.getInt("id_vm")); + vm.setNomVm(rs.getString("nom_vm")); + vm.setUuid(rs.getString("uuid")); + vm.setLienDoc(rs.getString("lien_doc")); + vm.setCheminVboxmanage(rs.getString("chemin_vboxmanage")); + return vm; } + // ========================================================================= + // SNAPSHOTS + // ========================================================================= + + /** + * Enregistre un snapshot associé à une VM. + * + * @param snap le snapshot à persister + * @param vmId l'identifiant de la VM concernée + */ public void saveSnapshot(Snapshot snap, int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO snapshot (id_vm, nom_snap, date_creation, description) VALUES (?,?,?,?)"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setInt(1, vmId); + ps.setString(2, snap.getNomSnap()); + ps.setString(3, snap.getDateCreation()); + ps.setString(4, snap.getDescription()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) snap.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur saveSnapshot : " + e.getMessage(), e); + } } + /** + * Retourne tous les snapshots d'une VM, du plus récent au plus ancien. + * + * @param vmId l'identifiant de la VM + * @return liste des snapshots + */ public List getSnapshotsForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List snaps = new ArrayList<>(); + String sql = "SELECT * FROM snapshot WHERE id_vm=? ORDER BY date_creation DESC"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) snaps.add(mapSnapshot(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getSnapshotsForVM : " + e.getMessage(), e); + } + return snaps; } + /** Construit un objet Snapshot depuis un ResultSet. */ + private Snapshot mapSnapshot(ResultSet rs) throws SQLException { + Snapshot s = new Snapshot(); + s.setId(rs.getInt("id_snap")); + s.setNomSnap(rs.getString("nom_snap")); + s.setDateCreation(rs.getString("date_creation")); + s.setDescription(rs.getString("description")); + return s; + } + + // ========================================================================= + // JOURNAL DE BORD + // ========================================================================= + + /** + * Ajoute une entrée dans le journal de bord d'une VM. + * + * @param entry l'entrée de journal à persister + * @param vmId l'identifiant de la VM concernée + */ public void addJournalEntry(JournalEntry entry, int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO journal_entry (id_vm, horodatage, contenu) VALUES (?,?,?)"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setInt(1, vmId); + ps.setString(2, entry.getHorodatage()); + ps.setString(3, entry.getContenu()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) entry.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur addJournalEntry : " + e.getMessage(), e); + } } + /** + * Retourne toutes les entrées du journal d'une VM, de la plus récente à la plus ancienne. + * + * @param vmId l'identifiant de la VM + * @return liste des entrées de journal + */ public List getJournalEntriesForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List entries = new ArrayList<>(); + String sql = "SELECT * FROM journal_entry WHERE id_vm=? ORDER BY horodatage DESC"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) entries.add(mapJournalEntry(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getJournalEntriesForVM : " + e.getMessage(), e); + } + return entries; + } + + /** Construit un objet JournalEntry depuis un ResultSet. */ + private JournalEntry mapJournalEntry(ResultSet rs) throws SQLException { + JournalEntry e = new JournalEntry(); + e.setId(rs.getInt("id_entry")); + e.setHorodatage(rs.getString("horodatage")); + e.setContenu(rs.getString("contenu")); + return e; } + // ========================================================================= + // AUDIT + // ========================================================================= + + /** + * Enregistre une entrée d'audit (appelé automatiquement après chaque action sur une VM). + * + * @param entry l'entrée d'audit à persister + */ public void addAuditEntry(AuditEntry entry) { - throw new UnsupportedOperationException("Not implemented yet"); + String sql = "INSERT INTO audit_log (horodatage, id_vm, action, details) VALUES (?,?,?,?)"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + ps.setString(1, entry.getHorodatage()); + if (entry.getVmId() > 0) ps.setInt(2, entry.getVmId()); else ps.setNull(2, Types.INTEGER); + ps.setString(3, entry.getAction()); + ps.setString(4, entry.getDetails()); + ps.executeUpdate(); + ResultSet keys = ps.getGeneratedKeys(); + if (keys.next()) entry.setId(keys.getInt(1)); + } catch (SQLException e) { + throw new RuntimeException("Erreur addAuditEntry : " + e.getMessage(), e); + } } + /** + * Retourne l'historique d'audit d'une VM. + * + * @param vmId l'identifiant de la VM + * @return liste des entrées d'audit, de la plus récente à la plus ancienne + */ public List getAuditLogsForVM(int vmId) { - throw new UnsupportedOperationException("Not implemented yet"); + List logs = new ArrayList<>(); + String sql = "SELECT * FROM audit_log WHERE id_vm=? ORDER BY horodatage DESC"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setInt(1, vmId); + ResultSet rs = ps.executeQuery(); + while (rs.next()) logs.add(mapAuditEntry(rs)); + } catch (SQLException e) { + throw new RuntimeException("Erreur getAuditLogsForVM : " + e.getMessage(), e); + } + return logs; + } + + /** Construit un objet AuditEntry depuis un ResultSet. */ + private AuditEntry mapAuditEntry(ResultSet rs) throws SQLException { + AuditEntry a = new AuditEntry(); + a.setId(rs.getInt("id_log")); + a.setHorodatage(rs.getString("horodatage")); + a.setVmId(rs.getInt("id_vm")); + a.setAction(rs.getString("action")); + a.setDetails(rs.getString("details")); + return a; } + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + /** + * Charge la configuration applicative depuis la base. + * Retourne une configuration avec des valeurs par défaut si rien n'est enregistré. + * + * @return la configuration courante + */ public AppConfig getConfig() { - throw new UnsupportedOperationException("Not implemented yet"); + AppConfig config = new AppConfig(); + config.setVboxManagePath(getConfigValue("vboxManagePath", "VBoxManage")); + config.setDefaultExportDir(getConfigValue("defaultExportDir", + System.getProperty("user.home"))); + return config; } + /** + * Sauvegarde la configuration applicative. + * + * @param config la configuration à persister + */ public void saveConfig(AppConfig config) { - throw new UnsupportedOperationException("Not implemented yet"); + upsertConfig("vboxManagePath", config.getVboxManagePath()); + upsertConfig("defaultExportDir", config.getDefaultExportDir()); + } + + /** + * Lit une valeur de configuration par sa clé. + * + * @param key la clé de configuration + * @param defaultValue valeur retournée si la clé est absente + * @return la valeur stockée ou la valeur par défaut + */ + private String getConfigValue(String key, String defaultValue) { + String sql = "SELECT valeur FROM app_config WHERE cle=?"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, key); + ResultSet rs = ps.executeQuery(); + if (rs.next()) return rs.getString("valeur"); + } catch (SQLException e) { + throw new RuntimeException("Erreur getConfigValue : " + e.getMessage(), e); + } + return defaultValue; + } + + /** + * Insère ou met à jour une valeur de configuration (UPSERT). + * + * @param key la clé + * @param value la valeur + */ + private void upsertConfig(String key, String value) { + String sql = "INSERT OR REPLACE INTO app_config (cle, valeur) VALUES (?,?)"; + try (Connection conn = getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, key); + ps.setString(2, value); + ps.executeUpdate(); + } catch (SQLException e) { + throw new RuntimeException("Erreur upsertConfig : " + e.getMessage(), e); + } } -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/model/AppConfig.java b/src/main/java/tg/cyberlabmanager/model/AppConfig.java index 6d6c891..6ecdfc9 100644 --- a/src/main/java/tg/cyberlabmanager/model/AppConfig.java +++ b/src/main/java/tg/cyberlabmanager/model/AppConfig.java @@ -1,22 +1,12 @@ package tg.cyberlabmanager.model; - + public class AppConfig { private String vboxManagePath; - private String pdfExportDirectory; - - public String getVboxManagePath() { - return vboxManagePath; - } - - public void setVboxManagePath(String vboxManagePath) { - this.vboxManagePath = vboxManagePath; - } - - public String getPdfExportDirectory() { - return pdfExportDirectory; - } - - public void setPdfExportDirectory(String pdfExportDirectory) { - this.pdfExportDirectory = pdfExportDirectory; - } + private String defaultExportDir; + + public String getVboxManagePath() { return vboxManagePath; } + public void setVboxManagePath(String p) { this.vboxManagePath = p; } + public String getDefaultExportDir() { return defaultExportDir; } + public void setDefaultExportDir(String d) { this.defaultExportDir = d; } } + \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java index 4c6731f..27759e1 100644 --- a/src/main/java/tg/cyberlabmanager/model/AuditEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/AuditEntry.java @@ -1,51 +1,20 @@ package tg.cyberlabmanager.model; - -import java.time.LocalDateTime; - + public class AuditEntry { - private int id; - private LocalDateTime timestamp; - private Integer vmId; + private int id; + private String horodatage; + private int vmId; private String action; private String details; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public Integer getVmId() { - return vmId; - } - - public void setVmId(Integer vmId) { - this.vmId = vmId; - } - - public String getAction() { - return action; - } - - public void setAction(String action) { - this.action = action; - } - - public String getDetails() { - return details; - } - - public void setDetails(String details) { - this.details = details; - } -} + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getHorodatage() { return horodatage; } + public void setHorodatage(String h){ this.horodatage = h; } + public int getVmId() { return vmId; } + public void setVmId(int v) { this.vmId = v; } + public String getAction() { return action; } + public void setAction(String a) { this.action = a; } + public String getDetails() { return details; } + public void setDetails(String d) { this.details = d; } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java index ec7031f..4b1b025 100644 --- a/src/main/java/tg/cyberlabmanager/model/JournalEntry.java +++ b/src/main/java/tg/cyberlabmanager/model/JournalEntry.java @@ -1,33 +1,14 @@ package tg.cyberlabmanager.model; - -import java.time.LocalDateTime; - + public class JournalEntry { - private int id; - private LocalDateTime timestamp; - private String content; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public LocalDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - } - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } -} + private int id; + private String horodatage; + private String contenu; + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getHorodatage() { return horodatage; } + public void setHorodatage(String h){ this.horodatage = h; } + public String getContenu() { return contenu; } + public void setContenu(String c) { this.contenu = c; } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/model/Lab.java b/src/main/java/tg/cyberlabmanager/model/Lab.java index 9e02ad8..64ab47d 100644 --- a/src/main/java/tg/cyberlabmanager/model/Lab.java +++ b/src/main/java/tg/cyberlabmanager/model/Lab.java @@ -1,67 +1,17 @@ package tg.cyberlabmanager.model; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - + public class Lab { - private int id; - private String title; + private int id; + private String titre; private String description; - private String category; - private final List virtualMachines = new ArrayList<>(); - - public Lab() { - } - - public Lab(int id, String title, String description, String category) { - this.id = id; - this.title = title; - this.description = description; - this.category = category; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - public String getCategory() { - return category; - } - - public void setCategory(String category) { - this.category = category; - } - - public List getVirtualMachines() { - return Collections.unmodifiableList(virtualMachines); - } - - public void addVirtualMachine(VirtualMachine virtualMachine) { - virtualMachines.add(virtualMachine); - } - - public void removeVirtualMachine(VirtualMachine virtualMachine) { - virtualMachines.remove(virtualMachine); - } -} + private String categorie; + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getTitre() { return titre; } + public void setTitre(String t) { this.titre = t; } + public String getDescription() { return description; } + public void setDescription(String d) { this.description = d; } + public String getCategorie() { return categorie; } + public void setCategorie(String c) { this.categorie = c; } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/model/Snapshot.java b/src/main/java/tg/cyberlabmanager/model/Snapshot.java index bd5e879..a4ed027 100644 --- a/src/main/java/tg/cyberlabmanager/model/Snapshot.java +++ b/src/main/java/tg/cyberlabmanager/model/Snapshot.java @@ -1,42 +1,17 @@ package tg.cyberlabmanager.model; - -import java.time.LocalDateTime; - + public class Snapshot { - private int id; - private String name; - private LocalDateTime createdAt; + private int id; + private String nomSnap; + private String dateCreation; private String description; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public LocalDateTime getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(LocalDateTime createdAt) { - this.createdAt = createdAt; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } -} + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getNomSnap() { return nomSnap; } + public void setNomSnap(String n){ this.nomSnap = n; } + public String getDateCreation() { return dateCreation; } + public void setDateCreation(String d){ this.dateCreation = d; } + public String getDescription() { return description; } + public void setDescription(String d){ this.description = d; } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/model/VMStatus.java b/src/main/java/tg/cyberlabmanager/model/VMStatus.java index b6a8744..fa252ef 100644 --- a/src/main/java/tg/cyberlabmanager/model/VMStatus.java +++ b/src/main/java/tg/cyberlabmanager/model/VMStatus.java @@ -5,5 +5,6 @@ public enum VMStatus { POWERED_OFF, SAVED, PAUSED, - UNKNOWN + UNKNOWN, + ABORTED } diff --git a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java index f031a89..632a857 100644 --- a/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java +++ b/src/main/java/tg/cyberlabmanager/model/VirtualMachine.java @@ -1,71 +1,27 @@ package tg.cyberlabmanager.model; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - + public class VirtualMachine { - private int id; - private String name; + private int id; + private String nomVm; private String uuid; - private String documentationUrl; - private VMStatus status = VMStatus.UNKNOWN; - private final List snapshots = new ArrayList<>(); - private final List journalEntries = new ArrayList<>(); - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getUuid() { - return uuid; - } - - public void setUuid(String uuid) { - this.uuid = uuid; - } - - public String getDocumentationUrl() { - return documentationUrl; - } - - public void setDocumentationUrl(String documentationUrl) { - this.documentationUrl = documentationUrl; - } - - public VMStatus getStatus() { - return status; - } - - public void setStatus(VMStatus status) { - this.status = status; - } - - public List getSnapshots() { - return Collections.unmodifiableList(snapshots); - } - - public void addSnapshot(Snapshot snapshot) { - snapshots.add(snapshot); - } - - public List getJournalEntries() { - return Collections.unmodifiableList(journalEntries); - } - - public void addJournalEntry(JournalEntry journalEntry) { - journalEntries.add(journalEntry); - } -} + private String lienDoc; + private String cheminVboxmanage; + private VMStatus status; + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getNomVm() { return nomVm; } + public void setNomVm(String n) { this.nomVm = n; } + public String getUuid() { return uuid; } + public void setUuid(String u) { this.uuid = u; } + public String getLienDoc() { return lienDoc; } + public void setLienDoc(String l) { this.lienDoc = l; } + public String getCheminVboxmanage() { return cheminVboxmanage; } + public void setCheminVboxmanage(String c){ this.cheminVboxmanage = c; } + public VMStatus getStatus() { return status; } + public void setStatus(VMStatus s) { this.status = s; } + // Label lisible pour le PDF + public String getStatusLabel() { + return status != null ? status.name() : "UNKNOWN"; + } +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java index 5c670d7..18490ef 100644 --- a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java +++ b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java @@ -1,21 +1,290 @@ package tg.cyberlabmanager.pdf; -import tg.cyberlabmanager.model.JournalEntry; -import tg.cyberlabmanager.model.Lab; -import tg.cyberlabmanager.model.Snapshot; -import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.*; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.List; +/** + * Génère un rapport PDF récapitulatif d'un laboratoire Cyber Lab Manager. + * Le rapport contient : en-tête du labo, liste des VMs, snapshots et journal de bord. + * + * Utilise Apache PDFBox 3.x. + */ public class PdfExporter { - public void exportLabToPdf( - Lab lab, - List vms, - List snapshots, - List journals, - String filePath - ) throws IOException { - throw new UnsupportedOperationException("Not implemented yet"); - } -} + + // ── Mise en page ────────────────────────────────────────────────────────── + private static final float MARGIN = 50f; + private static final float PAGE_HEIGHT = PDRectangle.A4.getHeight(); // ~842 + private static final float PAGE_WIDTH = PDRectangle.A4.getWidth(); // ~595 + private static final float CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN; + + // ── Tailles de police ───────────────────────────────────────────────────── + private static final float FONT_TITLE = 18f; + private static final float FONT_HEADING = 13f; + private static final float FONT_SUBHEAD = 11f; + private static final float FONT_BODY = 10f; + + // ── Polices PDFBox 3.x ──────────────────────────────────────────────────── + private static final PDType1Font FONT_BOLD = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + private static final PDType1Font FONT_REGULAR = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + private static final PDType1Font FONT_OBLIQUE = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); + + // ── État interne de pagination ───────────────────────────────────────────── + private PDDocument document; + private PDPageContentStream content; + private float cursorY; // position verticale courante (descend vers 0) + + // ========================================================================= + // Méthode publique principale + // ========================================================================= + + /** + * Génère et sauvegarde le rapport PDF d'un laboratoire. + * + * @param lab le laboratoire exporté + * @param vms liste des VMs du labo (déjà filtrée par le contrôleur) + * @param snapshots liste des snapshots (déjà filtrée par le contrôleur) + * @param journals liste des entrées de journal (déjà filtrée par le contrôleur) + * @param filePath chemin complet du fichier PDF à créer + * @throws IOException si l'écriture du fichier échoue + */ + public void exportLabToPdf(Lab lab, + List vms, + List snapshots, + List journals, + String filePath) throws IOException { + + document = new PDDocument(); + newPage(); // crée la première page et initialise cursorY + + // ── En-tête du rapport ──────────────────────────────────────────────── + writeLine("Rapport – " + lab.getTitre(), FONT_BOLD, FONT_TITLE, true); + writeLine("Catégorie : " + nvl(lab.getCategorie(), "Non définie"), FONT_REGULAR, FONT_BODY, false); + writeLine("Généré le : " + now(), FONT_REGULAR, FONT_BODY, false); + if (lab.getDescription() != null && !lab.getDescription().isBlank()) { + writeLine("Description : " + lab.getDescription(), FONT_OBLIQUE, FONT_BODY, false); + } + spacer(12f); + separator(); + spacer(8f); + + // ── Section 1 : Machines virtuelles ─────────────────────────────────── + writeLine("1. Machines virtuelles (" + vms.size() + ")", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (vms.isEmpty()) { + writeLine(" Aucune machine virtuelle dans ce laboratoire.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (VirtualMachine vm : vms) { + checkPageBreak(60f); + writeLine(" ▸ " + vm.getNomVm(), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" UUID : " + nvl(vm.getUuid(), "—"), FONT_REGULAR, FONT_BODY, false); + writeLine(" Statut : " + nvl(vm.getStatusLabel(), "Inconnu"), FONT_REGULAR, FONT_BODY, false); + if (vm.getLienDoc() != null && !vm.getLienDoc().isBlank()) { + writeLine(" Doc : " + vm.getLienDoc(), FONT_REGULAR, FONT_BODY, false); + } + spacer(5f); + } + } + + spacer(8f); + separator(); + spacer(8f); + + // ── Section 2 : Snapshots ───────────────────────────────────────────── + writeLine("2. Snapshots (" + snapshots.size() + ")", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (snapshots.isEmpty()) { + writeLine(" Aucun snapshot enregistré.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (Snapshot snap : snapshots) { + checkPageBreak(40f); + writeLine(" ▸ " + nvl(snap.getNomSnap(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" Date : " + nvl(snap.getDateCreation(), "—"), FONT_REGULAR, FONT_BODY, false); + if (snap.getDescription() != null && !snap.getDescription().isBlank()) { + writeLine(" Note : " + snap.getDescription(), FONT_OBLIQUE, FONT_BODY, false); + } + spacer(4f); + } + } + + spacer(8f); + separator(); + spacer(8f); + + // ── Section 3 : Journal de bord ─────────────────────────────────────── + writeLine("3. Journal de bord (" + journals.size() + " entrée(s))", FONT_BOLD, FONT_HEADING, false); + spacer(6f); + + if (journals.isEmpty()) { + writeLine(" Aucune note dans le journal.", FONT_OBLIQUE, FONT_BODY, false); + } else { + for (JournalEntry entry : journals) { + checkPageBreak(40f); + writeLine(" [" + nvl(entry.getHorodatage(), "—") + "]", FONT_BOLD, FONT_BODY, false); + // Le contenu peut être long : on le découpe en lignes + writeWrappedText(" " + nvl(entry.getContenu(), ""), FONT_REGULAR, FONT_BODY); + spacer(5f); + } + } + + // ── Pied de page de la dernière page ────────────────────────────────── + writeFooter(); + + content.close(); + document.save(filePath); + document.close(); + } + + // ========================================================================= + // Gestion des pages + // ========================================================================= + + /** + * Crée une nouvelle page A4 et réinitialise le curseur en haut. + */ + private void newPage() throws IOException { + if (content != null) { + content.close(); + } + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + content = new PDPageContentStream(document, page); + cursorY = PAGE_HEIGHT - MARGIN; + } + + /** + * Si l'espace restant est insuffisant pour écrire {@code needed} points, + * crée une nouvelle page. + * + * @param needed espace vertical nécessaire en points + */ + private void checkPageBreak(float needed) throws IOException { + if (cursorY - needed < MARGIN + 20f) { + newPage(); + } + } + + // ========================================================================= + // Écriture de contenu + // ========================================================================= + + /** + * Écrit une ligne de texte simple et fait descendre le curseur. + * + * @param text le texte à écrire + * @param font la police PDFBox + * @param size la taille en points + * @param underline true pour ajouter une ligne sous le texte (simulée par un trait) + */ + private void writeLine(String text, PDType1Font font, float size, boolean underline) + throws IOException { + checkPageBreak(size + 6f); + cursorY -= size + 2f; + + content.beginText(); + content.setFont(font, size); + content.newLineAtOffset(MARGIN, cursorY); + content.showText(sanitize(text)); + content.endText(); + + if (underline) { + float textWidth = font.getStringWidth(sanitize(text)) / 1000 * size; + content.moveTo(MARGIN, cursorY - 2f); + content.lineTo(MARGIN + textWidth, cursorY - 2f); + content.stroke(); + cursorY -= 4f; + } + } + + /** + * Écrit un texte long en le découpant sur plusieurs lignes si nécessaire. + * + * @param text le texte (potentiellement long) + * @param font la police + * @param size la taille + */ + private void writeWrappedText(String text, PDType1Font font, float size) throws IOException { + // Largeur max en points, convertie en "unités police" + float maxWidth = CONTENT_WIDTH; + + String[] words = sanitize(text).split(" "); + StringBuilder line = new StringBuilder(); + + for (String word : words) { + String candidate = line.isEmpty() ? word : line + " " + word; + float w = font.getStringWidth(candidate) / 1000 * size; + if (w > maxWidth && !line.isEmpty()) { + writeLine(line.toString(), font, size, false); + line = new StringBuilder(word); + } else { + line = new StringBuilder(candidate); + } + } + if (!line.isEmpty()) { + writeLine(line.toString(), font, size, false); + } + } + + /** Ajoute un espace vertical. */ + private void spacer(float points) { + cursorY -= points; + } + + /** Dessine un trait de séparation horizontal. */ + private void separator() throws IOException { + content.setLineWidth(0.5f); + content.moveTo(MARGIN, cursorY); + content.lineTo(PAGE_WIDTH - MARGIN, cursorY); + content.stroke(); + cursorY -= 2f; + } + + /** Écrit un pied de page discret en bas de la page courante. */ + private void writeFooter() throws IOException { + float footerY = MARGIN - 10f; + content.beginText(); + content.setFont(FONT_OBLIQUE, 8f); + content.newLineAtOffset(MARGIN, footerY); + content.showText("Cyber Lab Manager – Rapport généré le " + now()); + content.endText(); + } + + // ========================================================================= + // Utilitaires + // ========================================================================= + + /** Retourne la valeur ou le fallback si null/blank. */ + private String nvl(String value, String fallback) { + return (value != null && !value.isBlank()) ? value : fallback; + } + + /** Date/heure courante formatée pour affichage. */ + private String now() { + return LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")); + } + + /** + * Nettoie les caractères non supportés par les polices standard PDF (Latin-1 uniquement). + * Remplace les caractères hors plage par '?'. + */ + private String sanitize(String text) { + if (text == null) return ""; + StringBuilder sb = new StringBuilder(); + for (char c : text.toCharArray()) { + sb.append(c <= 255 ? c : '?'); + } + return sb.toString(); + } +} \ No newline at end of file From 38ac373c115035a6f067dff420262941ac4bc821 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Sat, 30 May 2026 14:49:12 +0000 Subject: [PATCH 02/28] 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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�~ z7tv&*@((7#4XxR`$|QS1dU;mk`TR_?+={&|EVYpQHGkcd;Utp~&k7c{=ua<8D$8HB zvNXPo?epd^N}7r)uRD7dnw|Oh;if^enJ4>QTv?5lDw0?Gvj~=d(i@)LY{cfPBnFxk z6K$rs@A98U!s+=hZ!+10)yKp;Hn)C)Qr;3*FrsC6{yKi*BQePU=_2p36aRw?ZFdw_ z$H*?n+kdoxwO*F2=-`S1IDN=^kSp1Ou&#J)zRT&ESv-J!h-IL8w0!tbX(qxukFqNw zczMtZZJ>Z%Vc{<cV z;ttbujc!lGK3zwZq@FC|zw?Pfq-fkanP{B%b%zhWO#_pP=MTJ1Jzxuh*PnHN{6)}^ zQ4cQ-sa_MV6V~CtKItf^>}Y(?D)+qQrG-bxxMX(FCu>7d$u*ne7=8*T)gKM(&*Wt9 zJ~s)@0SZ-sZWXva1k-!vZ6vN1JbqF2I;Z2T)Pv=xYU;u9!PdlmE6wL|9KeeYM##tW zQ|2%3mzq3MN*$L=Wdw^XxrHvIQe~vZ`lIBWQq4t6(|)jRGNl+OZs`l$Q+fVEAJrtf z1>k&k(d5C08%2kHFdLq%>4^yobFJMcE?Zlk_?m?uoYi$h5GW+x;;j6*Js~4Pqb9w5 z6S#s~nX1LtPMYL7VUH)q^^^(v>J5qcYZJQYg9HWa| zu?pPs%JcoDT`ab3Q2OGS9(Tj7Qti0A9cQfW4NYl$5xBI%y&_}-mDtn6G~TZGc;?u% z0rW3^M|npdu-%iPipHG{Z*Ehrrcc3FL(*o*e9i<&nY`pEefQI-S_Og^p`x|7>(z)A z5ydQLHOt{8R=9=)tiRz&h`Tm1_l{uNTLlfZkN0#8%SzbT*;TCz94zM=roW3|Q&$$^ z`z68gYOA)&O<$VF+R3#JQdkDLW)iSl$0>W*D6XLZp4*SdAuAVG+Q0M@{S}lf5SZ00 zXS3@}h2d|cq+bUGEt*3&f8z5)A)RAw%=Eq(@#N@9?B2acF~`<>pGwiuZ`;V++4ksj zH+jb(ks8i!L+@?uhY#;rIn#d?FKpg%t}9soH8k>_JqKgi*RZP;Vcl~Q$U8yLkLB=0 zHcxLFZ3SA6*XZf;Sy=Y}`qAm-ZsOIz&OMO@+_F+rC=6&qaDK`=2P{cPO!eP7$r?b?sB|Gu-AAGf+M7O-`#f1r$iF z^#D?W9ZqGzOHQX)DEirS>sBe_6vRnW65Iz;y;CltYK<#xsm`R-`HF9h6x#DQgC)<< z_T&(Yt7&B?o>Z~852k=W6v0X|6p#U>C#}@ zmY1_luFdphkI*> zmQ5R;ar&Lj0%;oC<&#N85XWa65H<7qs%4*PW*J9QhD`^c^bS%=__Y)KSrT?`E=yt7a2et4J3T3 z=qO%!D&y&^v!jSE=c2QKCf9@0<85^^5Z%7Ps$kmBJK2TEgJ`U~>t13J`=uz$$w+>P zPV2oSC9pBbTgfhKj4MrDw~bfL&<|~Px({mt2imTj=vJm9Rw#V8_}#_NdL-B2%|7y* zqRsGnl8y>=?K6dfUswL>z_4dkn=t|&=(lb3U;A!y1}~!{pBn{+X|ERN9YF>Rvn%CB zT&Fh-RZ_(u!ZmtO_cRf)e0^TnyEc&&!!LrG)fS}^pEHVvTqMDA?(C})`BKn$YeUo5 zqphe4*Yfs=jGW01VY6dvjj{t@-xB!dp4+7HVyN<$k(u>d4bizR{F{TF;hjs4B58to zzUvYZyf&2{Eu5$%f!C)o3uXo`37ZYei!t9hW)Hu{Z#KGKDACz&@;i-5Kkl!_!)S|a za369&_?`@OW#<>L(S{eg8Jhq<^*{C>@RfFwmL02~8D?$P85Jgh>J%aU_Z7_iR*b|? z)rwy8XPRIP2XjQ}S73YVdIfpqO~tcfIM0dutOd8Nx8Z(;ebK%7Z&(`qRVc*|JsNK? zXpP_2T3QBUC#w~7VVjjJtL6SDdG^)I^R5pI!Z?d&8{MHVvRJ*gCn;BJ$&zSAd^YW@ z-|Qy(9nM$pMMyT}!iDqR9!Z~Kg8dOeF@YcE7C_rOV$;8Vs(WpfKL==R4mGoQstjzB zvpXGAVta+)@gwvg!7jFyZwY(LLb>0P4KbS;Tq7_r^AOeSNtNH}I{5ya(nh2|CR!36 zb)C)cd8xc59Dh2QfbBfMXCozbpxJ#qRuiCVm^klRrN@ZjUMp==p~ETuitof#PK{>e z-HgLDQe~Q(L9UkgFp%UC)XNAjvV)<%%R(k`=aVFEhOoKVIhdmm^c)E znFZ^m!#N|#wGMZw#!OARhO(*_u35>KHMI=Mn~zC1Hai71ZLh3g-_yls0XKXxNH565 z$?%7?EvwS)TnW1q1;eQs+iPBLb)e_tp4)FA2vyEdgVU?D%T0_%s3HxpyxttcaUmpfD zAJ;St_$WHi=(t>L2G5H5Md0^-(N-LB`VUvR;L|Q~@t2jC^Ca-pT_6HJla`iVwD9y4 z!7=`a^}oDAW6amAJ_TEk(ODYcxGIMG9?&B-6H)d|$nS+-ZJS zTso`n>-!xme7EM$}aOLy94+6`I3uApO{VV zSNC3{EHG^w&@4T&(kDG1=Fc9*qu5lZ`O5EW%4%xTkP=_w>m!3cBM6qSNWvi;a0lmM;$3W%%DPKdhoQNuGC@Z4t&P;L*bjkrDE335{fqr-051EWR^c3%HHo#X z&e9Fpe`*!`HTFhQd395hQp?+4YCDx!)3;6p> zGTmnrk@IVg67jx3wE9hc_U+RfYrnH%BYZJ8+4{k4;PLpaKi@`$@Uacx!AD+%#6JiB zDij-RFO|ZI3TPn1qI)h|GpRx&B4Rog9ukCfX22yw=vn3QT>QB4Ezw-X)ptz3w%7i9 z$-cVCh~}LuPZqB_pz(e_ig2m3Z}5#gX1kM6$4>MD25XQ$qx8TEgz*$Vn@lPQfguaE4;p#}cTT&Zehb8MQx zB941fApzz#wg^?bHBpcwOxCA6Dc@&EnI@Z+>h}{0Jtgeuub~A`q?h*kzV`eofV@qsvX^{t z&{dW>ZcqQzx#8H{O*4F2qonDjj5`c5GY?FgdhNaQjGT#AZCahmROr;1Wfp!?oT7np z+~KHKpYE8=(>pUR7E8`*Q1$GV4UG}k%T#pW(kSJZSuj`rQq@%87A&S15ynvDB&5e` zf>8i@baL@OR6mst*KXdl3Ux^&9i_qis?tPx^!jFVxtm4cDq#KY(th=u`*RVPYs`Z| zLJ0)|Wr{&xgJvLIwQJFLpUtUl_?$5@Sqrnii|kWto?94&>q3k-jE-!=T;FxjGd;HP z;mX0hQB3*@X|<6bHCw&w=0s>Y4(*nFvu>V)9pojVwAh;#$o1#Lp~y=+vo~_+0?4_> zjnmOo5pPm9YbFW@u2-@5(;|Y%pEHTLFcrVa*L-1A%>C4LM7TcPdtp7UeS3QSc;`Ae ze_vpcV9EfxGd#OYQjrbW-sa9R-a|3g7p1#=*VaFVxPh(EdH(OPxbOK`*8wLqCT4>^ z&RW(_z22Qq%kE8oDsm28JAK zodz`($`<$d*vT{VNtqniS6YmP0ri+PY?mz~8r}1Qa0X-TtcqEX99>x5h5?LnrtoVt z@I?+htq5w}Q?aQM@OmOn!WMEoS#90dAPylLUqVYLX-6?bQK4)cReb2gneG(f=i67p zDvUKle5y>{74P35|+#mx&@H6%6ehPUiLz(UM*N@WN*DHwGS4-s%~ z5y>qhAFy^BvsI*1$*LqnLV;?5cHLqCSYBbBxP!Gp(>>jUp=4nZOl$pstlEB!4!!MU z7M5&AHE*DhI{<7m1sI{$+k=r7y_B@=u5eRt#^<3bb~H6fOyYRaxAnz2lsj(Qs*vZr zyz~ql%Qp^l!hd0wV2pQ&+N&3<=c^*tbf0gKv{egl8Wt})I}MXjz+BgND^{26j^01-+rk+!8~-RMuDqE2!k=$az4kGlknA7v5^u!yJ2c*?_(FC%NZF!TP&sv-furvs zWE2I0umnE!qMonrd02*`8WY{h>RhGh@NxBAU+<{S37ap&lE(CNl%;txAI%S1$botU z8vRRynQle%cz&ST9NkVAOGVY) z$I4wKAbfvF2@*2BA@OqBP36d2ez7t%BcPIFqE<>XFb}>&&-7iC1S}jzpo&Y^q zRr>hB8_3BYU9`=Jq@jNN1*jr^>%Lp{*XV3!U%n;DWH_Jyf*%yMoqU_yAjc&Ai|Mh3 zONEMCqy}4ukp3{B^&Y2o^}gOhvlL=$&KxIMrm~3tLW4>-JD>|WjZI@#8G_kC!fj)f z$8dts&Wu8X#;**V0jwt+lhsmVTI06I8mfAbw=cBW)YL8$G1;pKrAT9~>d1So#Rvyj z08jBVR*Dweg8bJN`^%yB4zi*jhi9hm#bL{A;(0N1_wWIUEqSo_kJm~gGzyD z*E&!ouNgjZNO{$M8?-aAjpmrW|AMc5k~^G9y&s6}FUzC=PXfw2LIPj2zjcE? z3Tmzj`o14rWKi8zpz3bMd==g0qmH<98;>eU*P+q+t`gjydX$}Tv(Du2Db{XLwd;da zH=X25ol+UXMgSZ+%90Zb+R&O()5_?Hy9b3?k!&NFc;#!(wb#2G*?HNu`u8wyQd|65 zEZlLN2$gcDT*+i{oolcXw2e*5mE`AuQDo0*+7nEkPc+HvKr|$|nD2o{fUb1)%QiZ- zZ`TWKX9dla6CrCtQ5u>$rQHf-8hRQWoGua9CDRh0c>A#EyWmRr>2In%o{=wLP_SWj z!4fOD7OgK1j#)g>kB1u%D58_5HAy@)<@Jdn7V>GKf^n&JtF}K5G|n2Jqv8O$2z3t8 z#R@<{cACPw@K4;K+7Xcg`8yHcDgyIGa z4XPX39s|`~Bfg;VXZeru?n3@Aw(9_s+k}8 zi5$CrztWn~s#V23IqS#zoxTC_g;JF_)AMX;syaTkcmK$A^QpeGNS7{2JTA^VfS9ri*>1+NYU*x-ut$VmVbGlX-sZypQ~hU7{Ivv(rBTH3K67TQ zIG_wmmwAYu2CDfwzgrg|PkpL6F%to5b_|C27}14juc4Aw+&oVjc-8oc^!zw5ii#&r zwfl0IePv+`F3d(R?1-4W<9Fe6oe_`YThSeR8tq{&BF#6CB9X%r@?%iIv zGl}#KcCC*P@~^O&=?14cv`~I>M*szBvg&kv$jR>!|4^9;z9&c= zV}5#pi7QHddGKa;84aYQbDA1P%&>TEJZ?M>S|mG~K5i)Dt4^a4Zfu-vI1HfK!Sl{#IMU;Ubwt6o^8D43!zSg-G%V$^5sgZ4-%b`m#ly+_!sX3S;WF%QIGAEs^H zZ;36TE&OGvpK{5NRYcpwVkW=zQoWz)_30hHAU)rF%i&%T`qXwdTcTCV$W8Y?Su&fh zs*+HMVq9ts4b7*D#TvyC7dz)qtss{{jk9Q_zRsuAv=)}b)WObuot>Q#pz>bNf*GpO zixX~k<|Dq9NB6k4C$UNF?q(uITel}%F&e>M@x%alg_X#N4MZtOnxcfAzeT0mH6# z3BGY)@&4Ma8C$3T?A^%!BOA}lm-ssRHK=g}e^h1R_bLu?6WmJj%O z3mT0r@eE7jE|b@8a+62!QoAM;n(lg|^qrl#f;5XB%nnUMlrnO=a)xPaYBb#(liaE6 zl#BB9a0nQ(rs1K*S>9+|}e9Ea2BbKm=r({POcN=G;3hgU!WZfH%gCNX8&sSdfgoh90fNk`H! z^5z1-dH-tLgw&1}lNKIQ2^>xqiM>FSrZl7JjumDe9WLW%e%cDE6D{(VLB-Vz6mj*O zc=2@=!u{_l&0>QH)SC)V!VgQO@-1|Qd6FPm5Ej*5VxKltachKX*umfz23nqajH@=G zrsZ?{^-P7bnOfs{xhLy)z&-Kuc9?^5#Fzqq-tlpXVDSznI<&p;`bD-2zFTH%7Zj@D)({uY2`a6*qGRIM+IXUSa}gPK`1}-P$+k`ZwYIV-LBS@{qkxH@g)R4=wpXYCu4G z6#~zpuxFEp2R5yk6=rT!(L{k6+|HDMGZX2VUSGTZS8lE^Br>$QwHvOt){H7%Ao>|< zgss;)v%$7kSsDX;`BIZj~*20^PQD7BQ$axJ{KYce+U+UnbW?%px9=;Sw(>T6J7EFn_mwtiB5}b)d zYZv3k&=$pb1lpq*UtP%|C|SG{q3kFI3?clzjI@$5dGa?Tnn?vMf@wd!qbBW@T<5MB zGrJXo#6`1+pIdt&+N0`tt-(jh;2@@W9U8n6} zrz#F_o>${G_s$g-pa^E(rLkW2Zy+&&yRf?(?Npd@h3J{a?oorY_Y_defSCYm0a}Iq zsW~4PP2l3yb~n}3nysAkRyuFTjKelw;Ye|qC6Oj=x8V4f%j+AfMyV-(R=F@D#H&?E z@^L3Qp=0b|D69(OTrMv9x>^n#O$Nze%VdOt)-7WZYOW+2ZnTFffy5t9Xjd?B+5xo@g zkP4W>Rz{Z~j~hIi&QYxQk8jjoy|dq=>mlY$NcC(l?0Sc7MJw>HZ2$E;6nx%me@Fi} z=kia}{Lj!>OEsf}uKKSq2;}AEb*|Nm&LBiT+xs;eGDkhy*?sW`C|A8JD9F7tg2BGB z|DD1w$SBOGpD=7z8Y%uz1Kchedjz5AYR^mi^ds51NODXn4{{fR4{pQDD~ z%pyq%+}QhmuAtxl=b>gtW$ynUVdbr&Hm-NK`@{e%^P=wfU9H0Kh{& z$Saie80r_CVpgn?LDRfa!%yA2RbGH@q3pm&|#^KycDN;QTVN$9~kFurK4<0FDi z$t*lEuZt=OY5`H$VbmUi<$Ran^XNT5}*wOyZHfh~i zhP`boC)D8&+)qAmF0$1WOhanG;>Ab4d(=IW?oI+Nu-}4~XA3U~Mt)xE- z;o;mE>)6{&uhVKIkV)TyX>KzPe)oi1wMSHUeaT`Yn;BmEcrJHW!^2_dA<|cIzPWO}bP6T+@HS=f2oz=VDs(+0*MKxJ zzd`dAN9EVOT~myET7G!Ac1wc{_Bb9wg|n;QAwy%kyz_15OBB6)>uE9{Sq{cx#&YZf z4Nj2t96Lf1{^ig#=Yl%7+*)SD3y2a8QR-&8NXX&K@yP-MlH1}r$lIG= z>&dqWBHYoP0SODg+-TCSJeq4IwCemU@NdhALG~*e4U6z&k0t5|I;Q~pjD0yez!g-v zUz3)dCG#k;X4Nve9sMy|UxP^jy}LM^2JxmBoVc^;tvDcQm<@$9=d5L6&ZrlT%_^)REFsz^c82V`R4p_@lIxJ=0XFc8miZG&Qyl#n`R6Nu(Y; zGsQXkB_27;Mx~}3UC+oK&NOLHemsLxLs(=*ffA9TLuVi4>MPngL>$_(n1v`A# zr4J3_mkAKWj1Io-o|FGF0u1ecSDUIPzITp+_nsLOY32TJ z?Wv#U*c~&Zy8utgx6?%w=0nsHZk^AJgE~p%91}aRnVB6}cp?m)&abL&;rpB=>@+7` zUng6g&Nx#AqqTu3&{eNAYjgK~*FO0LO{!&8>Q-Jm3h@5aK2WEb(OH6F86aRRP^s@F z@{;dByB_JOtw@c`rg^F7MIL9w8@n^mEU6LKyHDjCeMR@$pTpO=t~5n9(F1d#>+I^T z2d(PK1^|a#KCJ<59n*k;nKuijE%s!S#>%qs3e2AOpC7crcd#|$YCdJRisqM>v*6LW zpDs(apn}@7sxR4lUR1L-{(4;pCy@IwFlsG{MldfNpKDKd%bKyNmg`^rKugPQYj7$W zRBzqIM(NG<&x%Q!*B5ZyJ}K}}RB#8n23OnOzk+gpDZe$X;AHWHc@IY|c)CN<|5}V{ z%wc{bSppKgj({6QK}ZqSScI{N*S@+}3hZg}4w6~@sMI*_l|388TDa$=>I1=A6Dmy? zAkE1GuBLZ)7>;hg7C6u~VUbJx2w*ud=|3o$e(1ZnKb7rEninxR$Q+I6aTV&!;qLCW zW_(%~EwwgXW=ba5xgaSsZvM~9>;^J=H<`Mo_&bDa9FDHZqK}?js(3ds;hVbS zb=ht2ZPtD7r>-x9$u4!%^BPQ>B=2Ll7yR-}^EmXha=TR~X9+8SpRGo4Krmc$souED zyN>aXn&T?{!HbSA2XGW`+6H?A&rUA6g?l|%c`0{iNoLUs8!EN~#996*YI3fPxAc%} z4Cf92EGOGuiL%%5OzD13-sZkmxGAolOloxr05s4Vd6VZQFUvM52t7s<{JQc$jRh)M zw{5iNM<4NM!@4U$%^RyB)&_Dk3KhF(Tx<+d8yd;1Qeqrdb^G;juTBP%JcQHj=R1|{ z#&*l8L)S4|FR3eiE6srUI207_)t>15!vb>fxr=_J*+BQK5k#|*c7URaL$K<8h}jTq zgs^kP)Vt=z+tI;v9U2U)^LUW39Jey?vgIPcBu?!leK`#hZ?A$cYom7>h^RZ|zXobH zs^&PkhK?7~G4ax{#_UdN%))+m8`ye@N7u({*MAUaAnR_8VB>MD?TCfdAvR<5@QoBc z*_g>-TbavH9~E?DD@9lbR?J+V&b^l7FvKOt%O@VkF>w}YpxT+b(jYxfOZDi-a6y%M^Kj?&AK*4@7~(Ce^dWL zfwrNzCnP#8O_7CyNc&58czEZ)&d!d)H1%(#dUX`FduE8Jv$nyYvNQ=7gjt-NxR(|w zkcbEyH{J)29;F?uNOSy~g~U(B27`fDWZpa)3RM55nG+#fYB8&WA~yUv_2wBRPIw`Y&d8|7Otit@h~D&;2H0#qnsl znXux}I5#Ls5Q*w=DNG?e{Jv*zm-$=_J0|6PCydWbmt8}`6eR^DtT}%N)o&}i91kB# zb0=`_P!ZxoeWaen-7{naOM&wzZ?F}GebB%W);yUae^x<+r2h{Lq$0`mKN#I{uAQf0 z$;ccZ7Zyq5-5B8`dQUL&@sWT}Z4!Hm*(c?98T@Il4XVTTwl;Q=+OT!^;ENyLqtVu^ zEtiQhecu@7hJ=J@%FAoBJ$zVLRRuEOS<(4jvERY*!v_Z+pKJoU5;wc0O)w&Qk#!@^ zf@rY+#LqCd8);wNvm~zq0_8&>982!Va`<_6-;S;{X_>)k_PrX@JGt&$morXOC9-rt z+~`MeF;@8@I2qkv^*7Hsev$vvbM8(a^-kE3FW74C3kEel|8783v)bEvgaB@VRtzy> zwrNp+ULIH6_W|BR!;(~vT(A;WB^V{GK1O&VfoGr)pG1*B3CiYiA`E?@h*0epG4pd>8b-cIc5`B;>Wx{MwG%cErc*IZsinF6&%NoC_586BBb@p3T zO?Y@_8@IpirD;fKnh@;{##foZcXmCj@E+2gPVq~R@b7B(7q>L=`;x2X+9YWUB71KAfv}Ou9iG9Qy=io80*)EuW<%!SWL6D*lnRtFBW$6G6>A~{y+k>%E7rD z4LI>ekEf&S7wWxc1+-0!2i{1`(f2h<1V|d6zxX}poew;N%>J@fT6p$AcTAcmN$w^R z6BNx07GIs&W{(oK+~+&%9F+@}CEL84UsLrYoK)Pf5xbPLyK@V;rp7^0`nYzvz7Z&6Ouo z#4QfIXU7y>@CpOm69pT(NNc?0nT!X8qRPm#p`csAGqB){cj}x%b|Y0-g}t4e*}-0k zM+&yeMJs76t1uohNHx-aq|UH+up$FCoXrv*H^TdR*2g=+9lxO7eLQIRMi#f+H^N+= z0&8~4O};-%*nnL8l~s#fve4P*WVE;9SdjsYtHrgiW(mlaZf#7fm?o;zg=MjvFo!_@ zd~BG;)7j#IbgTicEApL_!S;71Yu?}&JygqEJI!Frng29HbX}0C66GR{;P5uUB`*|gRTs8 zet5eiiFbzb>cPOt%ZyI$!DfH_rPG70l|uU;{^%hKyf*)EaxwT|<9IlnzHz&UWi^;* zeKfw=UrO@b+Lsque0{iW-?}Z@Fip4k;mN@k&*W5J^u10S-E1LnUMY0vxFI_C&-Ld#H8Xfa~n`++mGBwS`Lu5pDD&k?$dI4p1fHIeSz7lhvQY?+KG=q>e+xJ8qyTz4gGqM%}&Zd z>*VXzk{NH0`X>DJTCzBr7F6C29v|WdaEWIs`s2?g>#2S{hI&UWGu7Yf=%=tJ3QU-6tG2%PBD=-z+7L zTkNvO6!@K%H9$)_?6x|Nk)%Hxx_Z`x&uEA&V)2{`ZF zs9hZXQZD_NzeK~KsUAnRvP~aau%4CxfIl>tW$_LwTb}@8rWTf_#C73M3Xno{e})H9 zB*o<>;pICN^|m$IVbfCVJ z+^nRc)bu?&y=J*N*+N7SdyH3V(cJX9_v8=qJ20w|`=}c8fPHFe|D^oGF-wu#k+JAT z9fid$0X=Zk>4zXQ+}?JPC&yX$NtJ4tVS1rme->~T%nBH9(&9JJ%~44)r#R30eGa+B z;d~{E_a2P!msd4_!tJqt}|g>Z2u3}|JtyslGuwdI>#%VHOj&UV6 z|COG0t@NPLuwLB7am>ccu9fC^tY&2K?2x;$sSV|vJYaiXEF#7p*|0%wQIE~xNG4HD zeSIeWEz?}%`2171a$?2w?0S28cu@oodLNpAJdRAS+pChaMn{;$ zR_*ZW`yXuqR-g)wIo7kdl>ENOY6&48mZ%M?Q5(bPU$luc2iW%SUU{au1%7iW$+N?r z-{usPx4Zd{Sh1jA-IFM{)hO9>BW|K^H~Uu+#kwgJ7dD1+65KaXeyh@#8Tptaybn#L zQ_dRv1gw+eOIi4g^}tH{ni>_QeH&$EW!e4-adAu^RsYqK4$powDNPE0G&(};S@?hW zX53j4p1MTTBubuqWOxY>Q_c9NNo215yOMiX`rjQqzLnlOBo4SxJMF}GePJ;CD$%dX zBEo)*!Nu4=`UK-7L7zB2m8jLTC*Y03;UD0Y6uOu{%7kzgL7A|#EJzqxi=DQwHV&5J z=OFr~{WKf%-=(v)RS{-X^YQt}NWn>9^?bC8+>jvuVz+V7Kgv+)Z)M0T8B$pIwP#69 zPp^TIf+Vhq$gz`d2#WxD6#sYS1Nlf~G|Y5c{`$H7pnJ`=)4bdX#zVyTao4LDI|i~S z{`26UfmyUO<&c||r4%hI+tpPlFE6k9^p3{ep9!{D&X={hI%pVGy_Pcd@B}-?#n_6=_o9#C`$h=W&9z-x|U7Z+=Dp zFO}r~e$|irFB-D)D)O9|f5FE&RAj{|b}-HAlR41kLf8W;@ZGTFFR0`FLJ=nH_W_au z=kB)~bxq*>MUl~X=(+Aobhq8L;EQU>^Y%;tpO>wju5N1{<`E@A23%)0d*cg4D`cCIKP$&jYdOu|vVLwR9Z(*`vp*q-8EgAIJlCg;6J%=Z@~w;NrFs0A2@+eYrKmND)P7q82{GD4D~ju$b5HO04}9-m@SgWP@AI7J zw*i~sZLEb|m40LdyCijRbO5Tm>^9t@S+*hMKYFq`#U09;rf!^&@-JgfNwX;~ile;> z^;VELCOPXIP_-lOh$Ss0Y+fFbMB(F+Xx$DoHH| zZ3y8r{&7-%RXP)4d2S9tEg&O11m2gL0D(=i|f^X!$T3d*Mr z{SeTnN~?T{u91S{s{Di4qD=$|&=^>>Rzk}mZr$-77O*8=Z|TRYfCN;8Vb^shPq zXWR|VcwV^D=$PLdBU88s{ScPnmz;Gz&p$5``%XaLq9h>{BeDnMEMBQ~0{++ZnQ~M_ z(c#ZAH^G{GLu40p^a=z8sncTtdy% ze34&;BO(i9m5>cH1Yj=C2ns6}Trt*BR8%j+vyT{CZQkYyllR~=i^JT`;j5sB+B}u@ zF_1?v3lYa+O}=E^yyhp}A^SJSClEDCPC;65;GJ!J%<8SHlXNN=gwX#q&Io7MK|i`k zWnYXIJALs-hs4zdM^U|+=N9`td`eZWs7b#;6w&J3x6sJgddeb`xsbB6sPT9<6XTt! z95*ua)ED4xq4MXrx0-F3GgS;AjU7-XgXgv{^7r;P&S};BgW!+3-Nd>>#M9GEqlqjI zesNTnezl(N{+RaF^$-B~g)O|lj%F>Lye~`+#SCq6F@{C9G5+*gp09sG;oydd3AJ8c zICy@AA@O8Ux@M1Uy6SR@$WNp4P2o4%bM1Xj^XrFLXBdkYRIjeU7xGq&%9=bcxtYFXlwHq0DC;cj@#(6=z7vpdBMZQhE(jzlA$7p0qzHoHav%lu~BkR){mn(U70F&)OC6TmFU@WM8LR zfs3PbtNcoR`*`O;cfXPv6#_mkwZo)=q*E=$3V6TJwk}wuh+f9Faa5L8Eb!3jo<69IjtBh2dcN@R_rBV#B zum{XHT`Qk5u7Ya8rrC-hjU zj4DUjl58Q4*{~9NSovPT#s9g+9|ZJJm9AHsJQEEZB}O+MOQIXC7It)CAZEu?d7!O6 z_qBMAdXq@0?)A)9Ct#$Vk%U`f3a8Ut9~EjBTTSwGUb(BFKc7MO5-daOR;E8Ji~I9g z^Y8yYSQd)KK+{H-0fExZikntO)ylJwnjoEOTkL8!w*-aB8}=G)#GjA8?&EeCO03vP zT>=l{vCP}QJ8L&PsJXf1t=%-R^PD_V9`1&fhPBHQFs7mWj;1K(<3_L%Ba1-&6idk% zQ}ET;DmcgS3S0u9^^3aQy`qU93I&L?JoDf%%FY;iRL(VHU9iLR;m{@O;7CwuL_X_;uJ>0rVqD9qPae4Nt#MB#F#>`@sm zx%&!O3^efYJFTvYIt23E@5#BnYo(!RNWd~E6X_Z}HF~;RF!BIs7wzVQjneXdn#jpq zI*A`V0X0B$b~$>l@<@+TX3R$@Hr?KyNaksM@X8!QS^A^lb&YnN5uQE}6xu!j>%zPf zez#8fbu5q+3pz_LGJQZAmwcl2mnvY>`$rZX6e-3V#u_nPk;q9cGm8a>U`huG8<&B{ z0z$ER;2SYEmaXGmqH3U? zSD!#TVK%l=FtN#gBpgK=*%=jHyxTE}Gj@p-adpwt`T4BHHmX{<9K-mAaGt?yJP{Ea zXPIx!h26~cBoW5cx68M-3+ZR^!a)$K5=9jyX^yUrakM>#Lrd`_jxrgir2B?Y$|l&_ z@WLO1CtYG;5~0##s-xkhzVZ4s)AD^qPtNk%6Kx=gX>CAAN4Y>wB(lVDP87gU9U-!ldjKEIh*L zUA}_$t;vs(aV0DMN)X`}QT#T}Ju&oU_L5&VWE~>E7LB!pZ;M-J*9Q{USK91bS z+Y1>9Hr@r*maRo^2Z8SIA`+feus+%Bv<%Vo2HH^pXr4wD8nXN9_>Syl0(>nu?!q-- zY9vTDuda9y>|nPle6i{q8#v}_K|T+S=OzxV;qb^}O56cP#@jT&i(vKaPI_3L=~fF` zs|DA&%hkZ`3hvK9)DjW{NL$imB4s3%6Ab@rH7kDOS92$1`>r4gFlAP&5NE>QVSkZG zXnF7ck?ZT*5?m9N0g9DLmj?oQsZQyXmbbVEF|neaBQq_G6?;J=&o0G-7UYo0`F$%^ zP5S8wO`bg!xi~cU95YptRJ*Tw`R}<=Ib}9g?-eOx0;9UL@2@>18$D@d@;_8v^3CQMCUvjP1&Wd8UO1NHF;+Y@jq-VO7_%j~_u*lL${+r@9=_1ORB wjr`|+Z|c%C|A%PrbEH}S57>f%_0Gk1uYK+?q+>t6;HRIiUh#CMI{k6y-@A1`v;Y7A literal 0 HcmV?d00001 From 83f7deaea09acc87a84e18a342505c521e8a2cef Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Sat, 6 Jun 2026 15:43:14 +0000 Subject: [PATCH 03/28] 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 04/28] 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 2e605d2c249f2cda58d0ac525bb1558406e31cbf Mon Sep 17 00:00:00 2001 From: akpmarcelin Date: Sat, 20 Jun 2026 19:34:29 +0000 Subject: [PATCH 05/28] fix(data,pdf): adapt to Role 3 real model classes --- .../cyberlabmanager/data/DatabaseManager.java | 270 ++++++++---------- .../tg/cyberlabmanager/pdf/PdfExporter.java | 104 +++---- 2 files changed, 162 insertions(+), 212 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java index 56ac366..ee485b0 100644 --- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java +++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java @@ -1,53 +1,58 @@ package tg.cyberlabmanager.data; -import tg.cyberlabmanager.model.*; +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.AuditEntry; +import tg.cyberlabmanager.model.JournalEntry; +import tg.cyberlabmanager.model.Lab; +import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.model.VMStatus; +import tg.cyberlabmanager.model.VirtualMachine; import java.sql.*; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; /** - * Gestionnaire de la base de données SQLite de Cyber Lab Manager. - * Fournit toutes les opérations CRUD pour les labos, VMs, snapshots, - * journal de bord, audit et configuration. + * Service d'accès aux données de l'application. * - * Utilisation normale : new DatabaseManager("jdbc:sqlite:/home/user/cyberlab.db") - * Utilisation en test : new DatabaseManager("jdbc:sqlite::memory:") + * Il masque la persistance SQLite derrière des opérations de haut niveau sur + * les entités métier. */ public class DatabaseManager { private final String jdbcUrl; - - // ========================================================================= + // Constructeur + initialisation - // ========================================================================= /** - * Crée le DatabaseManager et initialise les tables si elles n'existent pas. + * Crée un gestionnaire de base de données utilisant le fichier SQLite + * par défaut situé dans le répertoire utilisateur. + */ + public DatabaseManager() { + this("jdbc:sqlite:" + System.getProperty("user.home") + "/cyberlab.db"); + } + + /** + * Crée un gestionnaire de base de données avec une URL JDBC explicite. + * Utile pour les tests (ex: "jdbc:sqlite::memory:"). * - * @param jdbcUrl URL JDBC SQLite, ex: "jdbc:sqlite:/home/user/cyberlab.db" - * ou "jdbc:sqlite::memory:" pour les tests + * @param jdbcUrl URL JDBC SQLite */ public DatabaseManager(String jdbcUrl) { this.jdbcUrl = jdbcUrl; initDatabase(); } - /** - * Ouvre et retourne une connexion SQLite. - * Toujours utilisée dans un try-with-resources pour garantir la fermeture. - */ private Connection getConnection() throws SQLException { return DriverManager.getConnection(jdbcUrl); } /** * Crée toutes les tables si elles n'existent pas encore. - * Appelé automatiquement au démarrage. */ private void initDatabase() { String[] tables = { - // Table des laboratoires "CREATE TABLE IF NOT EXISTS labo (" + " id_labo INTEGER PRIMARY KEY AUTOINCREMENT," + " titre TEXT NOT NULL," + @@ -55,26 +60,25 @@ private void initDatabase() { " categorie TEXT" + ");", - // Table des machines virtuelles "CREATE TABLE IF NOT EXISTS vm (" + - " id_vm INTEGER PRIMARY KEY AUTOINCREMENT," + - " nom_vm TEXT NOT NULL," + - " uuid TEXT UNIQUE," + - " id_labo INTEGER REFERENCES labo(id_labo) ON DELETE SET NULL," + - " lien_doc TEXT," + - " chemin_vboxmanage TEXT" + + " id_vm INTEGER PRIMARY KEY AUTOINCREMENT," + + " nom_vm TEXT NOT NULL," + + " uuid TEXT UNIQUE," + + " id_labo INTEGER REFERENCES labo(id_labo) ON DELETE SET NULL," + + " lien_doc TEXT," + + " statut TEXT" + ");", - // Table des snapshots "CREATE TABLE IF NOT EXISTS snapshot (" + " id_snap INTEGER PRIMARY KEY AUTOINCREMENT," + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + + " uuid TEXT," + " nom_snap TEXT," + " date_creation TEXT," + - " description TEXT" + + " description TEXT," + + " online INTEGER DEFAULT 0" + ");", - // Table du journal de bord "CREATE TABLE IF NOT EXISTS journal_entry (" + " id_entry INTEGER PRIMARY KEY AUTOINCREMENT," + " id_vm INTEGER NOT NULL REFERENCES vm(id_vm) ON DELETE CASCADE," + @@ -82,16 +86,15 @@ private void initDatabase() { " contenu TEXT NOT NULL" + ");", - // Table d'audit automatique "CREATE TABLE IF NOT EXISTS audit_log (" + " id_log INTEGER PRIMARY KEY AUTOINCREMENT," + " horodatage TEXT NOT NULL," + " id_vm INTEGER REFERENCES vm(id_vm) ON DELETE SET NULL," + " action TEXT NOT NULL," + - " details TEXT" + + " details TEXT," + + " lab_name TEXT" + ");", - // Table de configuration (clé/valeur) "CREATE TABLE IF NOT EXISTS app_config (" + " cle TEXT PRIMARY KEY," + " valeur TEXT" + @@ -108,41 +111,34 @@ private void initDatabase() { } } - // ========================================================================= // LABORATOIRES - // ========================================================================= /** - * Sauvegarde un laboratoire. Fait un INSERT si l'id est 0, un UPDATE sinon. - * Après INSERT, l'id généré est assigné à l'objet lab. + * Enregistre ou met à jour un laboratoire. * - * @param lab le laboratoire à sauvegarder + * @param lab laboratoire à persister */ public void saveLab(Lab lab) { if (lab.getId() == 0) { - // INSERT String sql = "INSERT INTO labo (titre, description, categorie) VALUES (?, ?, ?)"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - ps.setString(1, lab.getTitre()); + ps.setString(1, lab.getTitle()); ps.setString(2, lab.getDescription()); - ps.setString(3, lab.getCategorie()); + ps.setString(3, lab.getCategory()); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); - if (keys.next()) { - lab.setId(keys.getInt(1)); - } + if (keys.next()) lab.setId(keys.getInt(1)); } catch (SQLException e) { throw new RuntimeException("Erreur saveLab (INSERT) : " + e.getMessage(), e); } } else { - // UPDATE String sql = "UPDATE labo SET titre=?, description=?, categorie=? WHERE id_labo=?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setString(1, lab.getTitre()); + ps.setString(1, lab.getTitle()); ps.setString(2, lab.getDescription()); - ps.setString(3, lab.getCategorie()); + ps.setString(3, lab.getCategory()); ps.setInt(4, lab.getId()); ps.executeUpdate(); } catch (SQLException e) { @@ -152,10 +148,10 @@ public void saveLab(Lab lab) { } /** - * Récupère un laboratoire par son identifiant. + * Récupère un laboratoire par son identifiant local. * - * @param id l'identifiant du laboratoire - * @return le Lab correspondant, ou null si introuvable + * @param id identifiant local du laboratoire + * @return laboratoire correspondant, ou null si introuvable */ public Lab getLab(int id) { String sql = "SELECT id_labo, titre, description, categorie FROM labo WHERE id_labo=?"; @@ -173,9 +169,9 @@ public Lab getLab(int id) { } /** - * Retourne tous les laboratoires enregistrés. + * Récupère tous les laboratoires. * - * @return liste de tous les Lab, vide si aucun + * @return liste des laboratoires */ public List getAllLabs() { List labs = new ArrayList<>(); @@ -193,9 +189,9 @@ public List getAllLabs() { } /** - * Supprime un laboratoire. Les VMs associées deviennent orphelines (id_labo → NULL). + * Supprime un laboratoire sans supprimer les VM physiques. * - * @param lab le laboratoire à supprimer + * @param lab laboratoire à supprimer */ public void deleteLab(Lab lab) { String sql = "DELETE FROM labo WHERE id_labo=?"; @@ -208,37 +204,35 @@ public void deleteLab(Lab lab) { } } - /** Construit un objet Lab depuis un ResultSet. */ private Lab mapLab(ResultSet rs) throws SQLException { Lab lab = new Lab(); lab.setId(rs.getInt("id_labo")); - lab.setTitre(rs.getString("titre")); + lab.setTitle(rs.getString("titre")); lab.setDescription(rs.getString("description")); - lab.setCategorie(rs.getString("categorie")); + lab.setCategory(rs.getString("categorie")); return lab; } - // ========================================================================= // MACHINES VIRTUELLES - // ========================================================================= /** - * Sauvegarde une VM. INSERT si id==0, UPDATE sinon. - * Si labId est null, la VM est orpheline (non rattachée à un labo). + * Enregistre ou met à jour une machine virtuelle. * - * @param vm la machine virtuelle à sauvegarder - * @param labId l'identifiant du labo associé, ou null pour une VM orpheline + * @param vm machine virtuelle à persister + * @param labId identifiant du laboratoire rattaché, ou {@code null} */ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { + String statut = vm.getStatus() != null ? vm.getStatus().name() : VMStatus.UNKNOWN.name(); + if (vm.getId() == 0) { - String sql = "INSERT INTO vm (nom_vm, uuid, id_labo, lien_doc, chemin_vboxmanage) VALUES (?,?,?,?,?)"; + String sql = "INSERT INTO vm (nom_vm, uuid, id_labo, lien_doc, statut) VALUES (?,?,?,?,?)"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - ps.setString(1, vm.getNomVm()); + ps.setString(1, vm.getName()); ps.setString(2, vm.getUuid()); if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); - ps.setString(4, vm.getLienDoc()); - ps.setString(5, vm.getCheminVboxmanage()); + ps.setString(4, vm.getDocumentationUrl()); + ps.setString(5, statut); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); if (keys.next()) vm.setId(keys.getInt(1)); @@ -246,14 +240,14 @@ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { throw new RuntimeException("Erreur saveVirtualMachine (INSERT) : " + e.getMessage(), e); } } else { - String sql = "UPDATE vm SET nom_vm=?, uuid=?, id_labo=?, lien_doc=?, chemin_vboxmanage=? WHERE id_vm=?"; + String sql = "UPDATE vm SET nom_vm=?, uuid=?, id_labo=?, lien_doc=?, statut=? WHERE id_vm=?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { - ps.setString(1, vm.getNomVm()); + ps.setString(1, vm.getName()); ps.setString(2, vm.getUuid()); if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); - ps.setString(4, vm.getLienDoc()); - ps.setString(5, vm.getCheminVboxmanage()); + ps.setString(4, vm.getDocumentationUrl()); + ps.setString(5, statut); ps.setInt(6, vm.getId()); ps.executeUpdate(); } catch (SQLException e) { @@ -263,10 +257,10 @@ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { } /** - * Retourne toutes les VMs rattachées à un laboratoire. + * Récupère les machines virtuelles rattachées à un laboratoire. * - * @param labId l'identifiant du laboratoire - * @return liste des VMs du labo + * @param labId identifiant local du laboratoire + * @return liste des machines virtuelles rattachées */ public List getVMsForLab(int labId) { List vms = new ArrayList<>(); @@ -283,9 +277,9 @@ public List getVMsForLab(int labId) { } /** - * Retourne les VMs sans laboratoire associé. + * Récupère les machines virtuelles sans laboratoire. * - * @return liste des VMs orphelines + * @return liste des machines virtuelles orphelines */ public List getOrphanVMs() { List vms = new ArrayList<>(); @@ -301,10 +295,9 @@ public List getOrphanVMs() { } /** - * Détache une VM de son laboratoire (id_labo devient NULL). - * Ne supprime pas la VM de l'application ni de VirtualBox. + * Détache une machine virtuelle de son laboratoire. * - * @param vm la VM à détacher + * @param vm machine virtuelle à détacher */ public void removeVMFromLab(VirtualMachine vm) { String sql = "UPDATE vm SET id_labo=NULL WHERE id_vm=?"; @@ -317,35 +310,39 @@ public void removeVMFromLab(VirtualMachine vm) { } } - /** Construit un objet VirtualMachine depuis un ResultSet. */ private VirtualMachine mapVM(ResultSet rs) throws SQLException { VirtualMachine vm = new VirtualMachine(); vm.setId(rs.getInt("id_vm")); - vm.setNomVm(rs.getString("nom_vm")); + vm.setName(rs.getString("nom_vm")); vm.setUuid(rs.getString("uuid")); - vm.setLienDoc(rs.getString("lien_doc")); - vm.setCheminVboxmanage(rs.getString("chemin_vboxmanage")); + vm.setDocumentationUrl(rs.getString("lien_doc")); + String statut = rs.getString("statut"); + try { + vm.setStatus(statut != null ? VMStatus.valueOf(statut) : VMStatus.UNKNOWN); + } catch (IllegalArgumentException e) { + vm.setStatus(VMStatus.UNKNOWN); + } return vm; } - // ========================================================================= // SNAPSHOTS - // ========================================================================= /** - * Enregistre un snapshot associé à une VM. + * Enregistre un snapshot pour une machine virtuelle. * - * @param snap le snapshot à persister - * @param vmId l'identifiant de la VM concernée + * @param snap snapshot à persister + * @param vmId identifiant local de la machine virtuelle */ public void saveSnapshot(Snapshot snap, int vmId) { - String sql = "INSERT INTO snapshot (id_vm, nom_snap, date_creation, description) VALUES (?,?,?,?)"; + String sql = "INSERT INTO snapshot (id_vm, uuid, nom_snap, date_creation, description, online) VALUES (?,?,?,?,?,?)"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, vmId); - ps.setString(2, snap.getNomSnap()); - ps.setString(3, snap.getDateCreation()); - ps.setString(4, snap.getDescription()); + ps.setString(2, snap.getUuid()); + ps.setString(3, snap.getName()); + ps.setString(4, snap.getCreatedAt() != null ? snap.getCreatedAt().toString() : null); + ps.setString(5, snap.getDescription()); + ps.setInt(6, snap.isOnline() ? 1 : 0); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); if (keys.next()) snap.setId(keys.getInt(1)); @@ -355,10 +352,10 @@ public void saveSnapshot(Snapshot snap, int vmId) { } /** - * Retourne tous les snapshots d'une VM, du plus récent au plus ancien. + * Récupère les snapshots d'une machine virtuelle. * - * @param vmId l'identifiant de la VM - * @return liste des snapshots + * @param vmId identifiant local de la machine virtuelle + * @return liste des snapshots rattachés */ public List getSnapshotsForVM(int vmId) { List snaps = new ArrayList<>(); @@ -374,33 +371,33 @@ public List getSnapshotsForVM(int vmId) { return snaps; } - /** Construit un objet Snapshot depuis un ResultSet. */ private Snapshot mapSnapshot(ResultSet rs) throws SQLException { Snapshot s = new Snapshot(); s.setId(rs.getInt("id_snap")); - s.setNomSnap(rs.getString("nom_snap")); - s.setDateCreation(rs.getString("date_creation")); + s.setUuid(rs.getString("uuid")); + s.setName(rs.getString("nom_snap")); + String dateStr = rs.getString("date_creation"); + s.setCreatedAt(dateStr != null ? LocalDateTime.parse(dateStr) : null); s.setDescription(rs.getString("description")); + s.setOnline(rs.getInt("online") == 1); return s; } - // ========================================================================= // JOURNAL DE BORD - // ========================================================================= /** - * Ajoute une entrée dans le journal de bord d'une VM. + * Ajoute une note d'analyse pour une machine virtuelle. * - * @param entry l'entrée de journal à persister - * @param vmId l'identifiant de la VM concernée + * @param entry note d'analyse à persister + * @param vmId identifiant local de la machine virtuelle */ public void addJournalEntry(JournalEntry entry, int vmId) { String sql = "INSERT INTO journal_entry (id_vm, horodatage, contenu) VALUES (?,?,?)"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, vmId); - ps.setString(2, entry.getHorodatage()); - ps.setString(3, entry.getContenu()); + ps.setString(2, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); + ps.setString(3, entry.getContent()); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); if (keys.next()) entry.setId(keys.getInt(1)); @@ -410,10 +407,10 @@ public void addJournalEntry(JournalEntry entry, int vmId) { } /** - * Retourne toutes les entrées du journal d'une VM, de la plus récente à la plus ancienne. + * Récupère les notes d'analyse d'une machine virtuelle. * - * @param vmId l'identifiant de la VM - * @return liste des entrées de journal + * @param vmId identifiant local de la machine virtuelle + * @return liste des notes d'analyse */ public List getJournalEntriesForVM(int vmId) { List entries = new ArrayList<>(); @@ -429,32 +426,31 @@ public List getJournalEntriesForVM(int vmId) { return entries; } - /** Construit un objet JournalEntry depuis un ResultSet. */ private JournalEntry mapJournalEntry(ResultSet rs) throws SQLException { JournalEntry e = new JournalEntry(); e.setId(rs.getInt("id_entry")); - e.setHorodatage(rs.getString("horodatage")); - e.setContenu(rs.getString("contenu")); + String dateStr = rs.getString("horodatage"); + e.setTimestamp(dateStr != null ? LocalDateTime.parse(dateStr) : null); + e.setContent(rs.getString("contenu")); return e; } - // ========================================================================= // AUDIT - // ========================================================================= /** - * Enregistre une entrée d'audit (appelé automatiquement après chaque action sur une VM). + * Ajoute une entrée d'audit. * - * @param entry l'entrée d'audit à persister + * @param entry entrée d'audit à persister */ public void addAuditEntry(AuditEntry entry) { - String sql = "INSERT INTO audit_log (horodatage, id_vm, action, details) VALUES (?,?,?,?)"; + String sql = "INSERT INTO audit_log (horodatage, id_vm, action, details, lab_name) VALUES (?,?,?,?,?)"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { - ps.setString(1, entry.getHorodatage()); - if (entry.getVmId() > 0) ps.setInt(2, entry.getVmId()); else ps.setNull(2, Types.INTEGER); + ps.setString(1, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); + if (entry.getVmId() != null) ps.setInt(2, entry.getVmId()); else ps.setNull(2, Types.INTEGER); ps.setString(3, entry.getAction()); ps.setString(4, entry.getDetails()); + ps.setString(5, entry.getLabName()); ps.executeUpdate(); ResultSet keys = ps.getGeneratedKeys(); if (keys.next()) entry.setId(keys.getInt(1)); @@ -464,10 +460,10 @@ public void addAuditEntry(AuditEntry entry) { } /** - * Retourne l'historique d'audit d'une VM. + * Récupère les entrées d'audit d'une machine virtuelle. * - * @param vmId l'identifiant de la VM - * @return liste des entrées d'audit, de la plus récente à la plus ancienne + * @param vmId identifiant local de la machine virtuelle + * @return liste des entrées d'audit */ public List getAuditLogsForVM(int vmId) { List logs = new ArrayList<>(); @@ -483,52 +479,44 @@ public List getAuditLogsForVM(int vmId) { return logs; } - /** Construit un objet AuditEntry depuis un ResultSet. */ private AuditEntry mapAuditEntry(ResultSet rs) throws SQLException { AuditEntry a = new AuditEntry(); a.setId(rs.getInt("id_log")); - a.setHorodatage(rs.getString("horodatage")); - a.setVmId(rs.getInt("id_vm")); + String dateStr = rs.getString("horodatage"); + a.setTimestamp(dateStr != null ? LocalDateTime.parse(dateStr) : null); + int vmIdValue = rs.getInt("id_vm"); + a.setVmId(rs.wasNull() ? null : vmIdValue); a.setAction(rs.getString("action")); a.setDetails(rs.getString("details")); + a.setLabName(rs.getString("lab_name")); return a; } - // ========================================================================= // CONFIGURATION - // ========================================================================= /** - * Charge la configuration applicative depuis la base. - * Retourne une configuration avec des valeurs par défaut si rien n'est enregistré. + * Récupère la configuration applicative. * - * @return la configuration courante + * @return configuration applicative */ public AppConfig getConfig() { AppConfig config = new AppConfig(); config.setVboxManagePath(getConfigValue("vboxManagePath", "VBoxManage")); - config.setDefaultExportDir(getConfigValue("defaultExportDir", + config.setPdfExportDirectory(getConfigValue("pdfExportDirectory", System.getProperty("user.home"))); return config; } /** - * Sauvegarde la configuration applicative. + * Enregistre la configuration applicative. * - * @param config la configuration à persister + * @param config configuration à persister */ public void saveConfig(AppConfig config) { upsertConfig("vboxManagePath", config.getVboxManagePath()); - upsertConfig("defaultExportDir", config.getDefaultExportDir()); + upsertConfig("pdfExportDirectory", config.getPdfExportDirectory()); } - /** - * Lit une valeur de configuration par sa clé. - * - * @param key la clé de configuration - * @param defaultValue valeur retournée si la clé est absente - * @return la valeur stockée ou la valeur par défaut - */ private String getConfigValue(String key, String defaultValue) { String sql = "SELECT valeur FROM app_config WHERE cle=?"; try (Connection conn = getConnection(); @@ -542,12 +530,6 @@ private String getConfigValue(String key, String defaultValue) { return defaultValue; } - /** - * Insère ou met à jour une valeur de configuration (UPSERT). - * - * @param key la clé - * @param value la valeur - */ private void upsertConfig(String key, String value) { String sql = "INSERT OR REPLACE INTO app_config (cle, valeur) VALUES (?,?)"; try (Connection conn = getConnection(); diff --git a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java index 18490ef..7a6679f 100644 --- a/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java +++ b/src/main/java/tg/cyberlabmanager/pdf/PdfExporter.java @@ -1,6 +1,9 @@ package tg.cyberlabmanager.pdf; -import tg.cyberlabmanager.model.*; +import tg.cyberlabmanager.model.JournalEntry; +import tg.cyberlabmanager.model.Lab; +import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.model.VirtualMachine; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; @@ -22,31 +25,25 @@ */ public class PdfExporter { - // ── Mise en page ────────────────────────────────────────────────────────── private static final float MARGIN = 50f; - private static final float PAGE_HEIGHT = PDRectangle.A4.getHeight(); // ~842 - private static final float PAGE_WIDTH = PDRectangle.A4.getWidth(); // ~595 + private static final float PAGE_HEIGHT = PDRectangle.A4.getHeight(); + private static final float PAGE_WIDTH = PDRectangle.A4.getWidth(); private static final float CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN; - // ── Tailles de police ───────────────────────────────────────────────────── private static final float FONT_TITLE = 18f; private static final float FONT_HEADING = 13f; private static final float FONT_SUBHEAD = 11f; private static final float FONT_BODY = 10f; - // ── Polices PDFBox 3.x ──────────────────────────────────────────────────── private static final PDType1Font FONT_BOLD = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); private static final PDType1Font FONT_REGULAR = new PDType1Font(Standard14Fonts.FontName.HELVETICA); private static final PDType1Font FONT_OBLIQUE = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); - // ── État interne de pagination ───────────────────────────────────────────── + private static final DateTimeFormatter DISPLAY_FORMAT = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"); + private PDDocument document; private PDPageContentStream content; - private float cursorY; // position verticale courante (descend vers 0) - - // ========================================================================= - // Méthode publique principale - // ========================================================================= + private float cursorY; /** * Génère et sauvegarde le rapport PDF d'un laboratoire. @@ -65,12 +62,12 @@ public void exportLabToPdf(Lab lab, String filePath) throws IOException { document = new PDDocument(); - newPage(); // crée la première page et initialise cursorY + newPage(); - // ── En-tête du rapport ──────────────────────────────────────────────── - writeLine("Rapport – " + lab.getTitre(), FONT_BOLD, FONT_TITLE, true); - writeLine("Catégorie : " + nvl(lab.getCategorie(), "Non définie"), FONT_REGULAR, FONT_BODY, false); - writeLine("Généré le : " + now(), FONT_REGULAR, FONT_BODY, false); + // En-tête + writeLine("Rapport – " + nvl(lab.getTitle(), "Sans titre"), FONT_BOLD, FONT_TITLE, true); + writeLine("Catégorie : " + nvl(lab.getCategory(), "Non définie"), FONT_REGULAR, FONT_BODY, false); + writeLine("Généré le : " + now(), FONT_REGULAR, FONT_BODY, false); if (lab.getDescription() != null && !lab.getDescription().isBlank()) { writeLine("Description : " + lab.getDescription(), FONT_OBLIQUE, FONT_BODY, false); } @@ -78,7 +75,7 @@ public void exportLabToPdf(Lab lab, separator(); spacer(8f); - // ── Section 1 : Machines virtuelles ─────────────────────────────────── + // Section 1 : Machines virtuelles writeLine("1. Machines virtuelles (" + vms.size() + ")", FONT_BOLD, FONT_HEADING, false); spacer(6f); @@ -87,11 +84,12 @@ public void exportLabToPdf(Lab lab, } else { for (VirtualMachine vm : vms) { checkPageBreak(60f); - writeLine(" ▸ " + vm.getNomVm(), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" ▸ " + nvl(vm.getName(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); writeLine(" UUID : " + nvl(vm.getUuid(), "—"), FONT_REGULAR, FONT_BODY, false); - writeLine(" Statut : " + nvl(vm.getStatusLabel(), "Inconnu"), FONT_REGULAR, FONT_BODY, false); - if (vm.getLienDoc() != null && !vm.getLienDoc().isBlank()) { - writeLine(" Doc : " + vm.getLienDoc(), FONT_REGULAR, FONT_BODY, false); + writeLine(" Statut : " + (vm.getStatus() != null ? vm.getStatus().name() : "UNKNOWN"), + FONT_REGULAR, FONT_BODY, false); + if (vm.getDocumentationUrl() != null && !vm.getDocumentationUrl().isBlank()) { + writeLine(" Doc : " + vm.getDocumentationUrl(), FONT_REGULAR, FONT_BODY, false); } spacer(5f); } @@ -101,7 +99,7 @@ public void exportLabToPdf(Lab lab, separator(); spacer(8f); - // ── Section 2 : Snapshots ───────────────────────────────────────────── + // Section 2 : Snapshots writeLine("2. Snapshots (" + snapshots.size() + ")", FONT_BOLD, FONT_HEADING, false); spacer(6f); @@ -110,8 +108,10 @@ public void exportLabToPdf(Lab lab, } else { for (Snapshot snap : snapshots) { checkPageBreak(40f); - writeLine(" ▸ " + nvl(snap.getNomSnap(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); - writeLine(" Date : " + nvl(snap.getDateCreation(), "—"), FONT_REGULAR, FONT_BODY, false); + writeLine(" ▸ " + nvl(snap.getName(), "Sans nom"), FONT_BOLD, FONT_SUBHEAD, false); + writeLine(" Date : " + formatDate(snap.getCreatedAt()), FONT_REGULAR, FONT_BODY, false); + writeLine(" Type : " + (snap.isOnline() ? "À chaud (VM allumée)" : "À froid (VM éteinte)"), + FONT_REGULAR, FONT_BODY, false); if (snap.getDescription() != null && !snap.getDescription().isBlank()) { writeLine(" Note : " + snap.getDescription(), FONT_OBLIQUE, FONT_BODY, false); } @@ -123,7 +123,7 @@ public void exportLabToPdf(Lab lab, separator(); spacer(8f); - // ── Section 3 : Journal de bord ─────────────────────────────────────── + // Section 3 : Journal de bord writeLine("3. Journal de bord (" + journals.size() + " entrée(s))", FONT_BOLD, FONT_HEADING, false); spacer(6f); @@ -132,14 +132,12 @@ public void exportLabToPdf(Lab lab, } else { for (JournalEntry entry : journals) { checkPageBreak(40f); - writeLine(" [" + nvl(entry.getHorodatage(), "—") + "]", FONT_BOLD, FONT_BODY, false); - // Le contenu peut être long : on le découpe en lignes - writeWrappedText(" " + nvl(entry.getContenu(), ""), FONT_REGULAR, FONT_BODY); + writeLine(" [" + formatDate(entry.getTimestamp()) + "]", FONT_BOLD, FONT_BODY, false); + writeWrappedText(" " + nvl(entry.getContent(), ""), FONT_REGULAR, FONT_BODY); spacer(5f); } } - // ── Pied de page de la dernière page ────────────────────────────────── writeFooter(); content.close(); @@ -147,13 +145,10 @@ public void exportLabToPdf(Lab lab, document.close(); } - // ========================================================================= + // Gestion des pages - // ========================================================================= - /** - * Crée une nouvelle page A4 et réinitialise le curseur en haut. - */ + private void newPage() throws IOException { if (content != null) { content.close(); @@ -164,30 +159,14 @@ private void newPage() throws IOException { cursorY = PAGE_HEIGHT - MARGIN; } - /** - * Si l'espace restant est insuffisant pour écrire {@code needed} points, - * crée une nouvelle page. - * - * @param needed espace vertical nécessaire en points - */ private void checkPageBreak(float needed) throws IOException { if (cursorY - needed < MARGIN + 20f) { newPage(); } } - // ========================================================================= // Écriture de contenu - // ========================================================================= - /** - * Écrit une ligne de texte simple et fait descendre le curseur. - * - * @param text le texte à écrire - * @param font la police PDFBox - * @param size la taille en points - * @param underline true pour ajouter une ligne sous le texte (simulée par un trait) - */ private void writeLine(String text, PDType1Font font, float size, boolean underline) throws IOException { checkPageBreak(size + 6f); @@ -208,17 +187,8 @@ private void writeLine(String text, PDType1Font font, float size, boolean underl } } - /** - * Écrit un texte long en le découpant sur plusieurs lignes si nécessaire. - * - * @param text le texte (potentiellement long) - * @param font la police - * @param size la taille - */ private void writeWrappedText(String text, PDType1Font font, float size) throws IOException { - // Largeur max en points, convertie en "unités police" float maxWidth = CONTENT_WIDTH; - String[] words = sanitize(text).split(" "); StringBuilder line = new StringBuilder(); @@ -237,12 +207,10 @@ private void writeWrappedText(String text, PDType1Font font, float size) throws } } - /** Ajoute un espace vertical. */ private void spacer(float points) { cursorY -= points; } - /** Dessine un trait de séparation horizontal. */ private void separator() throws IOException { content.setLineWidth(0.5f); content.moveTo(MARGIN, cursorY); @@ -251,7 +219,6 @@ private void separator() throws IOException { cursorY -= 2f; } - /** Écrit un pied de page discret en bas de la page courante. */ private void writeFooter() throws IOException { float footerY = MARGIN - 10f; content.beginText(); @@ -261,18 +228,19 @@ private void writeFooter() throws IOException { content.endText(); } - // ========================================================================= + // Utilitaires - // ========================================================================= - /** Retourne la valeur ou le fallback si null/blank. */ private String nvl(String value, String fallback) { return (value != null && !value.isBlank()) ? value : fallback; } - /** Date/heure courante formatée pour affichage. */ private String now() { - return LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")); + return LocalDateTime.now().format(DISPLAY_FORMAT); + } + + private String formatDate(LocalDateTime dt) { + return dt != null ? dt.format(DISPLAY_FORMAT) : "—"; } /** From eb159f90378c0ae7439fa8d4e9db2754e18cca81 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Thu, 25 Jun 2026 11:49:30 +0000 Subject: [PATCH 06/28] 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 07/28] 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 08/28] 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 09/28] 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 10/28] 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 11/28] 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 12/28] 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 13/28] 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 14/28] 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 15/28] 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 16/28] 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 17/28] 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 18/28] 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) ); From 12d8a64639f0a5973a8166813f673ac003e0f6ca Mon Sep 17 00:00:00 2001 From: akpmarcelin Date: Sat, 4 Jul 2026 07:21:50 +0000 Subject: [PATCH 19/28] feat(data): implement DatabaseManager with SQLite - 30 tests passing --- .../cyberlabmanager/data/DatabaseManager.java | 117 ++-- .../data/DatabaseManagerTest.java | 510 ++++++++++++++++++ 2 files changed, 571 insertions(+), 56 deletions(-) create mode 100644 src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java diff --git a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java index ee485b0..ae19aed 100644 --- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java +++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java @@ -16,42 +16,58 @@ /** * Service d'accès aux données de l'application. * - * Il masque la persistance SQLite derrière des opérations de haut niveau sur - * les entités métier. + *

Maintient une connexion SQLite unique ouverte pendant toute la durée de + * vie de l'instance. Compatible avec SQLite en mémoire (:memory:) pour les + * tests.

*/ public class DatabaseManager { - private final String jdbcUrl; - - // Constructeur + initialisation + private final Connection connection; + + // ========================================================================= + // Constructeurs + // ========================================================================= /** - * Crée un gestionnaire de base de données utilisant le fichier SQLite - * par défaut situé dans le répertoire utilisateur. + * Crée un gestionnaire utilisant le fichier cyberlab.db dans le répertoire + * utilisateur. */ public DatabaseManager() { this("jdbc:sqlite:" + System.getProperty("user.home") + "/cyberlab.db"); } /** - * Crée un gestionnaire de base de données avec une URL JDBC explicite. - * Utile pour les tests (ex: "jdbc:sqlite::memory:"). + * Crée un gestionnaire avec une URL JDBC explicite. + * Utiliser "jdbc:sqlite::memory:" pour les tests unitaires. * * @param jdbcUrl URL JDBC SQLite */ public DatabaseManager(String jdbcUrl) { - this.jdbcUrl = jdbcUrl; + try { + Class.forName("org.sqlite.JDBC"); + this.connection = DriverManager.getConnection(jdbcUrl); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Driver SQLite introuvable", e); + } catch (SQLException e) { + throw new RuntimeException("Impossible d'ouvrir la base de données : " + e.getMessage(), e); + } initDatabase(); } - private Connection getConnection() throws SQLException { - return DriverManager.getConnection(jdbcUrl); + private Connection getConnection() { + return this.connection; } /** * Crée toutes les tables si elles n'existent pas encore. */ private void initDatabase() { + // Activer les clés étrangères SQLite (désactivées par défaut) + try (Statement stmt = getConnection().createStatement()) { + stmt.execute("PRAGMA foreign_keys = ON"); + } catch (SQLException e) { + throw new RuntimeException("Impossible d'activer les clés étrangères : " + e.getMessage(), e); + } String[] tables = { "CREATE TABLE IF NOT EXISTS labo (" + " id_labo INTEGER PRIMARY KEY AUTOINCREMENT," + @@ -101,8 +117,7 @@ private void initDatabase() { ");" }; - try (Connection conn = getConnection(); - Statement stmt = conn.createStatement()) { + try (Statement stmt = getConnection().createStatement()) { for (String sql : tables) { stmt.execute(sql); } @@ -111,7 +126,9 @@ private void initDatabase() { } } + // ========================================================================= // LABORATOIRES + // ========================================================================= /** * Enregistre ou met à jour un laboratoire. @@ -121,8 +138,7 @@ private void initDatabase() { public void saveLab(Lab lab) { if (lab.getId() == 0) { String sql = "INSERT INTO labo (titre, description, categorie) VALUES (?, ?, ?)"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, lab.getTitle()); ps.setString(2, lab.getDescription()); ps.setString(3, lab.getCategory()); @@ -134,8 +150,7 @@ public void saveLab(Lab lab) { } } else { String sql = "UPDATE labo SET titre=?, description=?, categorie=? WHERE id_labo=?"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setString(1, lab.getTitle()); ps.setString(2, lab.getDescription()); ps.setString(3, lab.getCategory()); @@ -155,13 +170,10 @@ public void saveLab(Lab lab) { */ public Lab getLab(int id) { String sql = "SELECT id_labo, titre, description, categorie FROM labo WHERE id_labo=?"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setInt(1, id); ResultSet rs = ps.executeQuery(); - if (rs.next()) { - return mapLab(rs); - } + if (rs.next()) return mapLab(rs); } catch (SQLException e) { throw new RuntimeException("Erreur getLab : " + e.getMessage(), e); } @@ -176,12 +188,9 @@ public Lab getLab(int id) { public List getAllLabs() { List labs = new ArrayList<>(); String sql = "SELECT id_labo, titre, description, categorie FROM labo ORDER BY titre"; - try (Connection conn = getConnection(); - Statement stmt = conn.createStatement(); + try (Statement stmt = getConnection().createStatement(); ResultSet rs = stmt.executeQuery(sql)) { - while (rs.next()) { - labs.add(mapLab(rs)); - } + while (rs.next()) labs.add(mapLab(rs)); } catch (SQLException e) { throw new RuntimeException("Erreur getAllLabs : " + e.getMessage(), e); } @@ -195,8 +204,7 @@ public List getAllLabs() { */ public void deleteLab(Lab lab) { String sql = "DELETE FROM labo WHERE id_labo=?"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setInt(1, lab.getId()); ps.executeUpdate(); } catch (SQLException e) { @@ -213,7 +221,9 @@ private Lab mapLab(ResultSet rs) throws SQLException { return lab; } + // ========================================================================= // MACHINES VIRTUELLES + // ========================================================================= /** * Enregistre ou met à jour une machine virtuelle. @@ -226,8 +236,7 @@ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { if (vm.getId() == 0) { String sql = "INSERT INTO vm (nom_vm, uuid, id_labo, lien_doc, statut) VALUES (?,?,?,?,?)"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, vm.getName()); ps.setString(2, vm.getUuid()); if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); @@ -241,8 +250,7 @@ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { } } else { String sql = "UPDATE vm SET nom_vm=?, uuid=?, id_labo=?, lien_doc=?, statut=? WHERE id_vm=?"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setString(1, vm.getName()); ps.setString(2, vm.getUuid()); if (labId != null) ps.setInt(3, labId); else ps.setNull(3, Types.INTEGER); @@ -265,8 +273,7 @@ public void saveVirtualMachine(VirtualMachine vm, Integer labId) { public List getVMsForLab(int labId) { List vms = new ArrayList<>(); String sql = "SELECT * FROM vm WHERE id_labo=?"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setInt(1, labId); ResultSet rs = ps.executeQuery(); while (rs.next()) vms.add(mapVM(rs)); @@ -284,8 +291,7 @@ public List getVMsForLab(int labId) { public List getOrphanVMs() { List vms = new ArrayList<>(); String sql = "SELECT * FROM vm WHERE id_labo IS NULL"; - try (Connection conn = getConnection(); - Statement stmt = conn.createStatement(); + try (Statement stmt = getConnection().createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) vms.add(mapVM(rs)); } catch (SQLException e) { @@ -301,8 +307,7 @@ public List getOrphanVMs() { */ public void removeVMFromLab(VirtualMachine vm) { String sql = "UPDATE vm SET id_labo=NULL WHERE id_vm=?"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setInt(1, vm.getId()); ps.executeUpdate(); } catch (SQLException e) { @@ -325,7 +330,9 @@ private VirtualMachine mapVM(ResultSet rs) throws SQLException { return vm; } + // ========================================================================= // SNAPSHOTS + // ========================================================================= /** * Enregistre un snapshot pour une machine virtuelle. @@ -335,8 +342,7 @@ private VirtualMachine mapVM(ResultSet rs) throws SQLException { */ public void saveSnapshot(Snapshot snap, int vmId) { String sql = "INSERT INTO snapshot (id_vm, uuid, nom_snap, date_creation, description, online) VALUES (?,?,?,?,?,?)"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, vmId); ps.setString(2, snap.getUuid()); ps.setString(3, snap.getName()); @@ -360,8 +366,7 @@ public void saveSnapshot(Snapshot snap, int vmId) { public List getSnapshotsForVM(int vmId) { List snaps = new ArrayList<>(); String sql = "SELECT * FROM snapshot WHERE id_vm=? ORDER BY date_creation DESC"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setInt(1, vmId); ResultSet rs = ps.executeQuery(); while (rs.next()) snaps.add(mapSnapshot(rs)); @@ -383,7 +388,9 @@ private Snapshot mapSnapshot(ResultSet rs) throws SQLException { return s; } + // ========================================================================= // JOURNAL DE BORD + // ========================================================================= /** * Ajoute une note d'analyse pour une machine virtuelle. @@ -393,8 +400,7 @@ private Snapshot mapSnapshot(ResultSet rs) throws SQLException { */ public void addJournalEntry(JournalEntry entry, int vmId) { String sql = "INSERT INTO journal_entry (id_vm, horodatage, contenu) VALUES (?,?,?)"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, vmId); ps.setString(2, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); ps.setString(3, entry.getContent()); @@ -415,8 +421,7 @@ public void addJournalEntry(JournalEntry entry, int vmId) { public List getJournalEntriesForVM(int vmId) { List entries = new ArrayList<>(); String sql = "SELECT * FROM journal_entry WHERE id_vm=? ORDER BY horodatage DESC"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setInt(1, vmId); ResultSet rs = ps.executeQuery(); while (rs.next()) entries.add(mapJournalEntry(rs)); @@ -435,7 +440,9 @@ private JournalEntry mapJournalEntry(ResultSet rs) throws SQLException { return e; } + // ========================================================================= // AUDIT + // ========================================================================= /** * Ajoute une entrée d'audit. @@ -444,8 +451,7 @@ private JournalEntry mapJournalEntry(ResultSet rs) throws SQLException { */ public void addAuditEntry(AuditEntry entry) { String sql = "INSERT INTO audit_log (horodatage, id_vm, action, details, lab_name) VALUES (?,?,?,?,?)"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, entry.getTimestamp() != null ? entry.getTimestamp().toString() : LocalDateTime.now().toString()); if (entry.getVmId() != null) ps.setInt(2, entry.getVmId()); else ps.setNull(2, Types.INTEGER); ps.setString(3, entry.getAction()); @@ -468,8 +474,7 @@ public void addAuditEntry(AuditEntry entry) { public List getAuditLogsForVM(int vmId) { List logs = new ArrayList<>(); String sql = "SELECT * FROM audit_log WHERE id_vm=? ORDER BY horodatage DESC"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setInt(1, vmId); ResultSet rs = ps.executeQuery(); while (rs.next()) logs.add(mapAuditEntry(rs)); @@ -492,7 +497,9 @@ private AuditEntry mapAuditEntry(ResultSet rs) throws SQLException { return a; } + // ========================================================================= // CONFIGURATION + // ========================================================================= /** * Récupère la configuration applicative. @@ -519,8 +526,7 @@ public void saveConfig(AppConfig config) { private String getConfigValue(String key, String defaultValue) { String sql = "SELECT valeur FROM app_config WHERE cle=?"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setString(1, key); ResultSet rs = ps.executeQuery(); if (rs.next()) return rs.getString("valeur"); @@ -532,8 +538,7 @@ private String getConfigValue(String key, String defaultValue) { private void upsertConfig(String key, String value) { String sql = "INSERT OR REPLACE INTO app_config (cle, valeur) VALUES (?,?)"; - try (Connection conn = getConnection(); - PreparedStatement ps = conn.prepareStatement(sql)) { + try (PreparedStatement ps = getConnection().prepareStatement(sql)) { ps.setString(1, key); ps.setString(2, value); ps.executeUpdate(); diff --git a/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java b/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java new file mode 100644 index 0000000..9c34f1d --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/data/DatabaseManagerTest.java @@ -0,0 +1,510 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package tg.cyberlabmanager.data; + +/** + * + * @author agokoli + */ +import tg.cyberlabmanager.model.*; + +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * Tests unitaires de DatabaseManager. + * Utilise SQLite en mémoire (:memory:) — aucun fichier créé sur le disque. + * Chaque test repart d'une base vide grâce au @BeforeEach. + */ +class DatabaseManagerTest { + + private DatabaseManager db; + + @BeforeEach + void setUp() { + // Base en mémoire : rapide, isolée, détruite après chaque test + db = new DatabaseManager("jdbc:sqlite::memory:"); + } + + // ========================================================================= + // LABORATOIRES + // ========================================================================= + + @Test + @DisplayName("saveLab : un nouveau labo reçoit un id généré") + void testSaveLabInsert() { + Lab lab = new Lab(); + lab.setTitle("Labo Malware"); + lab.setDescription("Analyse de malware"); + lab.setCategory("malware"); + + db.saveLab(lab); + + assertNotEquals(0, lab.getId(), "L'id doit être assigné après l'INSERT"); + } + + @Test + @DisplayName("getLab : retrouve un labo par son id") + void testGetLab() { + Lab lab = new Lab(); + lab.setTitle("Labo Pentest"); + lab.setCategory("pentest"); + db.saveLab(lab); + + Lab retrieved = db.getLab(lab.getId()); + + assertNotNull(retrieved); + assertEquals("Labo Pentest", retrieved.getTitle()); + assertEquals("pentest", retrieved.getCategory()); + } + + @Test + @DisplayName("getLab : retourne null si l'id n'existe pas") + void testGetLabNotFound() { + Lab result = db.getLab(9999); + assertNull(result); + } + + @Test + @DisplayName("saveLab : met à jour un labo existant") + void testSaveLabUpdate() { + Lab lab = new Lab(); + lab.setTitle("Ancien titre"); + lab.setCategory("reseau"); + db.saveLab(lab); + + lab.setTitle("Nouveau titre"); + db.saveLab(lab); // doit faire un UPDATE + + Lab updated = db.getLab(lab.getId()); + assertEquals("Nouveau titre", updated.getTitle()); + } + + @Test + @DisplayName("getAllLabs : retourne tous les labos") + void testGetAllLabs() { + Lab lab1 = new Lab(); lab1.setTitle("Labo A"); lab1.setCategory("malware"); + Lab lab2 = new Lab(); lab2.setTitle("Labo B"); lab2.setCategory("pentest"); + Lab lab3 = new Lab(); lab3.setTitle("Labo C"); lab3.setCategory("reseau"); + + db.saveLab(lab1); + db.saveLab(lab2); + db.saveLab(lab3); + + List labs = db.getAllLabs(); + assertEquals(3, labs.size()); + } + + @Test + @DisplayName("getAllLabs : retourne une liste vide si aucun labo") + void testGetAllLabsEmpty() { + List labs = db.getAllLabs(); + assertNotNull(labs); + assertTrue(labs.isEmpty()); + } + + @Test + @DisplayName("deleteLab : supprime un labo existant") + void testDeleteLab() { + Lab lab = new Lab(); + lab.setTitle("Labo à supprimer"); + db.saveLab(lab); + + db.deleteLab(lab); + + assertNull(db.getLab(lab.getId())); + assertEquals(0, db.getAllLabs().size()); + } + + @Test + @DisplayName("deleteLab : les VMs rattachées deviennent orphelines") + void testDeleteLabOrphansVMs() { + Lab lab = new Lab(); + lab.setTitle("Labo avec VMs"); + db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); + vm.setUuid("uuid-kali"); + db.saveVirtualMachine(vm, lab.getId()); + + db.deleteLab(lab); // les VMs doivent passer en orphelines + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("Kali", orphans.get(0).getName()); + } + + // ========================================================================= + // MACHINES VIRTUELLES + // ========================================================================= + + @Test + @DisplayName("saveVirtualMachine : une nouvelle VM reçoit un id généré") + void testSaveVMInsert() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Ubuntu-22"); + vm.setUuid("uuid-ubuntu"); + vm.setStatus(VMStatus.POWERED_OFF); + + db.saveVirtualMachine(vm, lab.getId()); + + assertNotEquals(0, vm.getId(), "L'id doit être assigné après l'INSERT"); + } + + @Test + @DisplayName("saveVirtualMachine : labId null crée une VM orpheline") + void testSaveVMOrpheline() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM-Orpheline"); + vm.setUuid("uuid-orphan"); + + db.saveVirtualMachine(vm, null); + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("VM-Orpheline", orphans.get(0).getName()); + } + + @Test + @DisplayName("getVMsForLab : retourne les VMs d'un labo") + void testGetVMsForLab() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm1 = new VirtualMachine(); vm1.setName("VM1"); vm1.setUuid("uuid-1"); + VirtualMachine vm2 = new VirtualMachine(); vm2.setName("VM2"); vm2.setUuid("uuid-2"); + + db.saveVirtualMachine(vm1, lab.getId()); + db.saveVirtualMachine(vm2, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals(2, vms.size()); + } + + @Test + @DisplayName("getVMsForLab : ne retourne pas les VMs d'un autre labo") + void testGetVMsForLabIsolation() { + Lab lab1 = new Lab(); lab1.setTitle("Labo 1"); db.saveLab(lab1); + Lab lab2 = new Lab(); lab2.setTitle("Labo 2"); db.saveLab(lab2); + + VirtualMachine vm = new VirtualMachine(); vm.setName("VM du labo 1"); vm.setUuid("uuid-x"); + db.saveVirtualMachine(vm, lab1.getId()); + + List vmsLab2 = db.getVMsForLab(lab2.getId()); + assertTrue(vmsLab2.isEmpty()); + } + + @Test + @DisplayName("getOrphanVMs : retourne uniquement les VMs sans labo") + void testGetOrphanVMs() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vmRattachee = new VirtualMachine(); + vmRattachee.setName("VM rattachée"); vmRattachee.setUuid("uuid-r"); + db.saveVirtualMachine(vmRattachee, lab.getId()); + + VirtualMachine vmOrpheline = new VirtualMachine(); + vmOrpheline.setName("VM orpheline"); vmOrpheline.setUuid("uuid-o"); + db.saveVirtualMachine(vmOrpheline, null); + + List orphans = db.getOrphanVMs(); + assertEquals(1, orphans.size()); + assertEquals("VM orpheline", orphans.get(0).getName()); + } + + @Test + @DisplayName("removeVMFromLab : détache une VM sans la supprimer") + void testRemoveVMFromLab() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("Kali"); vm.setUuid("uuid-kali"); + db.saveVirtualMachine(vm, lab.getId()); + + db.removeVMFromLab(vm); + + // La VM ne doit plus être dans le labo + assertTrue(db.getVMsForLab(lab.getId()).isEmpty()); + // Mais elle doit être orpheline + assertEquals(1, db.getOrphanVMs().size()); + } + + @Test + @DisplayName("saveVirtualMachine : le statut est bien persisté") + void testSaveVMStatus() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM Running"); vm.setUuid("uuid-run"); + vm.setStatus(VMStatus.RUNNING); + db.saveVirtualMachine(vm, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals(VMStatus.RUNNING, vms.get(0).getStatus()); + } + + @Test + @DisplayName("saveVirtualMachine : documentationUrl est bien persistée") + void testSaveVMDocUrl() { + Lab lab = new Lab(); lab.setTitle("Labo"); db.saveLab(lab); + + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM Doc"); vm.setUuid("uuid-doc"); + vm.setDocumentationUrl("https://doc.exemple.com"); + db.saveVirtualMachine(vm, lab.getId()); + + List vms = db.getVMsForLab(lab.getId()); + assertEquals("https://doc.exemple.com", vms.get(0).getDocumentationUrl()); + } + + // ========================================================================= + // SNAPSHOTS + // ========================================================================= + + @Test + @DisplayName("saveSnapshot : un nouveau snapshot reçoit un id généré") + void testSaveSnapshot() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-snap"); + db.saveVirtualMachine(vm, null); + + Snapshot snap = new Snapshot(); + snap.setName("Snapshot initial"); + snap.setDescription("Etat propre"); + snap.setCreatedAt(LocalDateTime.of(2026, 6, 1, 10, 0)); + snap.setOnline(false); + + db.saveSnapshot(snap, vm.getId()); + + assertNotEquals(0, snap.getId()); + } + + @Test + @DisplayName("getSnapshotsForVM : retourne les snapshots dans l'ordre décroissant") + void testGetSnapshotsForVM() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-snaps"); + db.saveVirtualMachine(vm, null); + + Snapshot s1 = new Snapshot(); s1.setName("Snap 1"); + s1.setCreatedAt(LocalDateTime.of(2026, 1, 1, 0, 0)); + Snapshot s2 = new Snapshot(); s2.setName("Snap 2"); + s2.setCreatedAt(LocalDateTime.of(2026, 6, 1, 0, 0)); + + db.saveSnapshot(s1, vm.getId()); + db.saveSnapshot(s2, vm.getId()); + + List snaps = db.getSnapshotsForVM(vm.getId()); + assertEquals(2, snaps.size()); + // Le plus récent en premier + assertEquals("Snap 2", snaps.get(0).getName()); + } + + @Test + @DisplayName("getSnapshotsForVM : retourne liste vide si aucun snapshot") + void testGetSnapshotsEmpty() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-no-snap"); + db.saveVirtualMachine(vm, null); + + assertTrue(db.getSnapshotsForVM(vm.getId()).isEmpty()); + } + + @Test + @DisplayName("saveSnapshot : le champ online est bien persisté") + void testSnapshotOnline() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-online"); + db.saveVirtualMachine(vm, null); + + Snapshot snap = new Snapshot(); + snap.setName("Snap chaud"); + snap.setCreatedAt(LocalDateTime.now()); + snap.setOnline(true); + db.saveSnapshot(snap, vm.getId()); + + List snaps = db.getSnapshotsForVM(vm.getId()); + assertTrue(snaps.get(0).isOnline()); + } + + // ========================================================================= + // JOURNAL DE BORD + // ========================================================================= + + @Test + @DisplayName("addJournalEntry : une nouvelle entrée reçoit un id généré") + void testAddJournalEntry() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-journal"); + db.saveVirtualMachine(vm, null); + + JournalEntry entry = new JournalEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setContent("Analyse en cours — comportement suspect détecté"); + + db.addJournalEntry(entry, vm.getId()); + + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("getJournalEntriesForVM : retourne les entrées de la plus récente à la plus ancienne") + void testGetJournalEntries() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-j2"); + db.saveVirtualMachine(vm, null); + + JournalEntry e1 = new JournalEntry(); + e1.setTimestamp(LocalDateTime.of(2026, 1, 1, 8, 0)); + e1.setContent("Première note"); + + JournalEntry e2 = new JournalEntry(); + e2.setTimestamp(LocalDateTime.of(2026, 6, 1, 9, 0)); + e2.setContent("Note récente"); + + db.addJournalEntry(e1, vm.getId()); + db.addJournalEntry(e2, vm.getId()); + + List entries = db.getJournalEntriesForVM(vm.getId()); + assertEquals(2, entries.size()); + assertEquals("Note récente", entries.get(0).getContent()); + } + + @Test + @DisplayName("getJournalEntriesForVM : retourne liste vide si aucune note") + void testGetJournalEntriesEmpty() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-j-empty"); + db.saveVirtualMachine(vm, null); + + assertTrue(db.getJournalEntriesForVM(vm.getId()).isEmpty()); + } + + // ========================================================================= + // AUDIT + // ========================================================================= + + @Test + @DisplayName("addAuditEntry : une entrée d'audit reçoit un id généré") + void testAddAuditEntry() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-audit"); + db.saveVirtualMachine(vm, null); + + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vm.getId()); + entry.setAction("START"); + entry.setDetails("Démarrage manuel"); + + db.addAuditEntry(entry); + + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("getAuditLogsForVM : retourne les logs d'une VM") + void testGetAuditLogs() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-audit2"); + db.saveVirtualMachine(vm, null); + + AuditEntry e1 = new AuditEntry(); + e1.setTimestamp(LocalDateTime.now()); + e1.setVmId(vm.getId()); + e1.setAction("START"); + + AuditEntry e2 = new AuditEntry(); + e2.setTimestamp(LocalDateTime.now()); + e2.setVmId(vm.getId()); + e2.setAction("STOP"); + + db.addAuditEntry(e1); + db.addAuditEntry(e2); + + List logs = db.getAuditLogsForVM(vm.getId()); + assertEquals(2, logs.size()); + } + + @Test + @DisplayName("addAuditEntry : vmId null est accepté") + void testAddAuditEntryNullVmId() { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(null); // pas de VM liée + entry.setAction("APP_START"); + entry.setDetails("Démarrage de l'application"); + + assertDoesNotThrow(() -> db.addAuditEntry(entry)); + assertNotEquals(0, entry.getId()); + } + + @Test + @DisplayName("addAuditEntry : labName est bien persisté") + void testAuditEntryLabName() { + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM"); vm.setUuid("uuid-labname"); + db.saveVirtualMachine(vm, null); + + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vm.getId()); + entry.setAction("SNAPSHOT_TAKEN"); + entry.setLabName("Labo Malware"); + + db.addAuditEntry(entry); + + List logs = db.getAuditLogsForVM(vm.getId()); + assertEquals("Labo Malware", logs.get(0).getLabName()); + } + + // ========================================================================= + // CONFIGURATION + // ========================================================================= + + @Test + @DisplayName("getConfig : retourne des valeurs par défaut si rien n'est sauvegardé") + void testGetConfigDefaults() { + AppConfig config = db.getConfig(); + + assertNotNull(config); + assertEquals("VBoxManage", config.getVboxManagePath()); + assertNotNull(config.getPdfExportDirectory()); + } + + @Test + @DisplayName("saveConfig et getConfig : les valeurs sont bien persistées") + void testSaveAndGetConfig() { + AppConfig config = new AppConfig(); + config.setVboxManagePath("/usr/bin/VBoxManage"); + config.setPdfExportDirectory("/home/agokoli/rapports"); + + db.saveConfig(config); + + AppConfig loaded = db.getConfig(); + assertEquals("/usr/bin/VBoxManage", loaded.getVboxManagePath()); + assertEquals("/home/agokoli/rapports", loaded.getPdfExportDirectory()); + } + + @Test + @DisplayName("saveConfig : une deuxième sauvegarde écrase la première") + void testSaveConfigOverwrite() { + AppConfig config1 = new AppConfig(); + config1.setVboxManagePath("/chemin/ancien"); + db.saveConfig(config1); + + AppConfig config2 = new AppConfig(); + config2.setVboxManagePath("/chemin/nouveau"); + db.saveConfig(config2); + + AppConfig loaded = db.getConfig(); + assertEquals("/chemin/nouveau", loaded.getVboxManagePath()); + } +} \ No newline at end of file From 33a5c34f565e9a80b41d7b1b72208be3c2dfc9e9 Mon Sep 17 00:00:00 2001 From: akpmarcelin Date: Mon, 13 Jul 2026 08:28:18 +0000 Subject: [PATCH 20/28] feat(pdf): implement PdfExporter with PDFBox - 15 tests passing --- .../cyberlabmanager/pdf/PdfExporterTest.java | 385 ++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 src/test/java/tg/cyberlabmanager/pdf/PdfExporterTest.java diff --git a/src/test/java/tg/cyberlabmanager/pdf/PdfExporterTest.java b/src/test/java/tg/cyberlabmanager/pdf/PdfExporterTest.java new file mode 100644 index 0000000..ed01e75 --- /dev/null +++ b/src/test/java/tg/cyberlabmanager/pdf/PdfExporterTest.java @@ -0,0 +1,385 @@ +package tg.cyberlabmanager.pdf; + +import tg.cyberlabmanager.model.*; + +import org.junit.jupiter.api.*; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Tests unitaires de PdfExporter. + * Chaque test génère un vrai fichier PDF temporaire puis le supprime. + * On ne teste pas le contenu visuel mais les comportements : création, + * taille, gestion des cas limites, robustesse. + */ +class PdfExporterTest { + + private PdfExporter exporter; + private String tempDir; + + @BeforeEach + void setUp() { + exporter = new PdfExporter(); + tempDir = System.getProperty("java.io.tmpdir"); + } + + // Utilitaires + + /** Crée un labo de test avec les champs remplis. */ + private Lab makeLab(String title, String category, String description) { + Lab lab = new Lab(); + lab.setTitle(title); + lab.setCategory(category); + lab.setDescription(description); + return lab; + } + + /** Crée une VM de test. */ + private VirtualMachine makeVM(String name, String uuid, VMStatus status) { + VirtualMachine vm = new VirtualMachine(); + vm.setName(name); + vm.setUuid(uuid); + vm.setStatus(status); + return vm; + } + + /** Crée un snapshot de test. */ + private Snapshot makeSnapshot(String name, boolean online) { + Snapshot s = new Snapshot(); + s.setName(name); + s.setCreatedAt(LocalDateTime.of(2026, 6, 1, 10, 0)); + s.setDescription("Description du snapshot"); + s.setOnline(online); + return s; + } + + /** Crée une entrée de journal de test. */ + private JournalEntry makeJournalEntry(String content) { + JournalEntry e = new JournalEntry(); + e.setTimestamp(LocalDateTime.of(2026, 6, 1, 14, 30)); + e.setContent(content); + return e; + } + + /** Retourne un chemin de fichier PDF temporaire unique. */ + private String tempPdf(String name) { + return tempDir + File.separator + "cyberlab_test_" + name + "_" + + System.currentTimeMillis() + ".pdf"; + } + + /** Supprime un fichier si il existe. */ + private void cleanup(String path) { + File f = new File(path); + if (f.exists()) f.delete(); + } + + // Tests de création de fichier + + @Test + @DisplayName("exportLabToPdf : crée bien un fichier PDF sur le disque") + void testFileIsCreated() throws IOException { + String path = tempPdf("creation"); + try { + Lab lab = makeLab("Labo Malware", "malware", "Test de création"); + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path); + + assertTrue(new File(path).exists(), "Le fichier PDF doit exister"); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : le fichier PDF n'est pas vide") + void testFileIsNotEmpty() throws IOException { + String path = tempPdf("notempty"); + try { + Lab lab = makeLab("Labo Test", "pentest", null); + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path); + + long size = new File(path).length(); + assertTrue(size > 0, "Le fichier PDF ne doit pas être vide, taille : " + size); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : le fichier commence par la signature PDF (%PDF)") + void testFileIsPdf() throws IOException { + String path = tempPdf("signature"); + try { + Lab lab = makeLab("Labo Réseau", "réseau", null); + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path); + + byte[] header = Files.readAllBytes(new File(path).toPath()); + // Un PDF valide commence toujours par "%PDF" + String start = new String(header, 0, Math.min(4, header.length)); + assertEquals("%PDF", start, "Le fichier doit commencer par %PDF"); + } finally { + cleanup(path); + } + } + + // Tests avec données réelles + + @Test + @DisplayName("exportLabToPdf : fonctionne avec une VM, un snapshot et une note") + void testWithFullData() throws IOException { + String path = tempPdf("fulldata"); + try { + Lab lab = makeLab("Labo Pentest", "pentest", "Tests d'intrusion réseau"); + + VirtualMachine vm = makeVM("Kali-Linux", "uuid-kali-001", VMStatus.RUNNING); + vm.setDocumentationUrl("https://kali.org"); + + Snapshot snap = makeSnapshot("Etat initial", false); + JournalEntry entry = makeJournalEntry("Scan réseau effectué — ports 22, 80, 443 ouverts"); + + exporter.exportLabToPdf( + lab, + List.of(vm), + List.of(snap), + List.of(entry), + path + ); + + assertTrue(new File(path).exists()); + assertTrue(new File(path).length() > 1000, + "Le PDF avec données doit être plus grand que 1Ko"); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : fonctionne avec plusieurs VMs") + void testWithMultipleVMs() throws IOException { + String path = tempPdf("multivms"); + try { + Lab lab = makeLab("Labo Multi", "malware", null); + + List vms = List.of( + makeVM("Windows-10", "uuid-win-001", VMStatus.POWERED_OFF), + makeVM("Ubuntu-22", "uuid-ubuntu-001", VMStatus.RUNNING), + makeVM("Kali-2024", "uuid-kali-002", VMStatus.SAVED) + ); + + exporter.exportLabToPdf(lab, vms, List.of(), List.of(), path); + + assertTrue(new File(path).exists()); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : fonctionne avec plusieurs snapshots") + void testWithMultipleSnapshots() throws IOException { + String path = tempPdf("multisnaps"); + try { + Lab lab = makeLab("Labo Snap", "réseau", null); + VirtualMachine vm = makeVM("VM-Test", "uuid-test", VMStatus.POWERED_OFF); + + List snaps = List.of( + makeSnapshot("Snap initial", false), + makeSnapshot("Snap après config", false), + makeSnapshot("Snap VM allumée", true) + ); + + exporter.exportLabToPdf(lab, List.of(vm), snaps, List.of(), path); + + assertTrue(new File(path).exists()); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : fonctionne avec plusieurs entrées de journal") + void testWithMultipleJournalEntries() throws IOException { + String path = tempPdf("multijournal"); + try { + Lab lab = makeLab("Labo Journal", "malware", null); + + List entries = List.of( + makeJournalEntry("Première observation : comportement suspect détecté"), + makeJournalEntry("Deuxième observation : connexions réseau anormales vers 192.168.1.100"), + makeJournalEntry("Troisième observation : fichiers chiffrés dans le répertoire Documents") + ); + + exporter.exportLabToPdf(lab, List.of(), List.of(), entries, path); + + assertTrue(new File(path).exists()); + } finally { + cleanup(path); + } + } + + // Tests des cas limites + + @Test + @DisplayName("exportLabToPdf : labo sans description ne plante pas") + void testLabWithoutDescription() throws IOException { + String path = tempPdf("nodesc"); + try { + Lab lab = makeLab("Labo Sans Description", "pentest", null); + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : VM sans statut ne plante pas") + void testVMWithoutStatus() throws IOException { + String path = tempPdf("nostatus"); + try { + Lab lab = makeLab("Labo", "malware", null); + VirtualMachine vm = new VirtualMachine(); + vm.setName("VM sans statut"); + vm.setUuid("uuid-nostatus"); + // status non défini — reste UNKNOWN par défaut + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(vm), List.of(), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : VM sans documentationUrl ne plante pas") + void testVMWithoutDocUrl() throws IOException { + String path = tempPdf("nodocurl"); + try { + Lab lab = makeLab("Labo", "réseau", null); + VirtualMachine vm = makeVM("VM-Test", "uuid-nodoc", VMStatus.RUNNING); + // documentationUrl non défini + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(vm), List.of(), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : snapshot sans date ne plante pas") + void testSnapshotWithoutDate() throws IOException { + String path = tempPdf("nodate"); + try { + Lab lab = makeLab("Labo", "malware", null); + Snapshot snap = new Snapshot(); + snap.setName("Snap sans date"); + // createdAt null + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(snap), List.of(), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : journal entry sans timestamp ne plante pas") + void testJournalEntryWithoutTimestamp() throws IOException { + String path = tempPdf("notimestamp"); + try { + Lab lab = makeLab("Labo", "pentest", null); + JournalEntry entry = new JournalEntry(); + entry.setContent("Note sans horodatage"); + // timestamp null + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(entry), path) + ); + } finally { + cleanup(path); + } + } + + @Test + @DisplayName("exportLabToPdf : note longue est gérée sans planter") + void testLongJournalEntry() throws IOException { + String path = tempPdf("longnote"); + try { + Lab lab = makeLab("Labo", "malware", null); + // Texte très long qui devrait déclencher le wrapping + String longContent = "Analyse détaillée : ".repeat(20) + + "comportement suspect détecté avec connexions réseau anormales " + + "vers plusieurs adresses IP externes non reconnues dans la liste blanche."; + + JournalEntry entry = makeJournalEntry(longContent); + + assertDoesNotThrow(() -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(entry), path) + ); + } finally { + cleanup(path); + } + } + + // Test de saut de page + + @Test + @DisplayName("exportLabToPdf : beaucoup de données génèrent un PDF multi-pages") + void testMultiplePages() throws IOException { + String path = tempPdf("multipages"); + try { + Lab lab = makeLab("Grand Labo", "pentest", "Labo avec beaucoup de contenu"); + + // 10 VMs + List vms = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + vms.add(makeVM("VM-" + i, "uuid-" + i, VMStatus.POWERED_OFF)); + } + + // 10 snapshots + List snaps = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + snaps.add(makeSnapshot("Snapshot-" + i, i % 2 == 0)); + } + + // 10 notes de journal + List entries = new ArrayList<>(); + for (int i = 1; i <= 10; i++) { + entries.add(makeJournalEntry( + "Note d'analyse numéro " + i + " : observations détaillées sur le comportement de la VM." + )); + } + + exporter.exportLabToPdf(lab, vms, snaps, entries, path); + + // Un PDF multi-pages est plus grand qu'un PDF d'une page + assertTrue(new File(path).length() > 1000, + "Un PDF avec beaucoup de données doit être suffisamment grand"); + } finally { + cleanup(path); + } + } + + // Test d'erreur + + @Test + @DisplayName("exportLabToPdf : lève IOException si le chemin est invalide") + void testInvalidPathThrowsIOException() { + Lab lab = makeLab("Labo", "malware", null); + String invalidPath = "/chemin/qui/nexiste/pas/rapport.pdf"; + + assertThrows(IOException.class, () -> + exporter.exportLabToPdf(lab, List.of(), List.of(), List.of(), invalidPath) + ); + } +} \ No newline at end of file From d4dd5fed67830ef7aca289261135997232db90a1 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Mon, 20 Jul 2026 15:55:21 +0000 Subject: [PATCH 21/28] feat(integration): brancher databaseManager.getVMsForLab (Role 2) - suppression du cache mock --- .../tg/cyberlabmanager/controller/LabController.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 6074c08..441c5fd 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -92,6 +92,10 @@ public class LabController { 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(); + + // Initialiser le DatabaseManager + this.databaseManager = new DatabaseManager(); + mockLabs(); initMockVMs(); refreshLabList(); @@ -117,12 +121,12 @@ private void initMockVMs() { // ── Affichage de la liste des labos ────────────────────────────────── private void refreshLabList() { - labListContainer.getChildren().clear(); + labListContainer.getChildren().clear(); - // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() - List labs = mockLabs(); + // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() + List labs = databaseManager.getAllLabs(); - for (Lab lab : labs) { + for (Lab lab : labs) { boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId(); String badgeStyle = switch (lab.getCategory().toLowerCase()) { From c90a717d599cf34c994bdb849458782816829e33 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Mon, 20 Jul 2026 15:56:13 +0000 Subject: [PATCH 22/28] feat(integration): brancher databaseManager.getVMsForLab (Role 2) - suppression du cache mock --- .../controller/LabController.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 441c5fd..fc60729 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -39,7 +39,6 @@ public class LabController { private boolean auditExpanded = false; private final ExecutorService executor = Executors.newCachedThreadPool(); - private final java.util.Map> vmsByLab = new java.util.HashMap<>(); @FXML private Label statusLabel; @@ -95,9 +94,8 @@ private void initialize() { // Initialiser le DatabaseManager this.databaseManager = new DatabaseManager(); - + mockLabs(); - initMockVMs(); refreshLabList(); } @@ -113,11 +111,7 @@ 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() { @@ -178,9 +172,16 @@ public void initView() { } private List getVMsForLab(int labId) { - // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) - return vmsByLab.getOrDefault(labId, new ArrayList<>()); - } + // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) + List vms = databaseManager.getVMsForLab(labId); + + // Fallback vers les mocks si la base est vide + if (vms.isEmpty()) { + vms = vmsByLab.getOrDefault(labId, new ArrayList<>()); + } + + return vms; +} public List getAllLabs() { throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); From 5eec63b58ab78800cfb5de8f356da36503c05a87 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Mon, 20 Jul 2026 16:06:06 +0000 Subject: [PATCH 23/28] fix: correction des references a vmsByLab supprime --- .../controller/LabController.java | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index fc60729..cd2f557 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -14,6 +14,7 @@ import tg.cyberlabmanager.model.VMStatus; import tg.cyberlabmanager.ui.LabListView; import tg.cyberlabmanager.ui.VmTableView; +import tg.cyberlabmanager.model.JournalEntry; import java.util.List; import java.util.concurrent.ExecutorService; @@ -177,8 +178,8 @@ private List getVMsForLab(int labId) { // Fallback vers les mocks si la base est vide if (vms.isEmpty()) { - vms = vmsByLab.getOrDefault(labId, new ArrayList<>()); - } + vms = mockVMsForLab(labId); + } return vms; } @@ -514,20 +515,36 @@ private void handleRestoreSnapshot() { }); } - @FXML - - private void handleAddJournalEntry() { - String text = journalInputField.getText(); - if (text == null || text.isBlank()) { - return; + @FXML +private void handleAddJournalEntry() { + String text = journalInputField.getText(); + if (text == null || text.isBlank()) { + return; + } + + appendJournalLine(text); + journalInputField.clear(); + + // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) + if (selectedVM != null) { + try { + // Créer une entrée de journal + tg.cyberlabmanager.model.JournalEntry entry = new tg.cyberlabmanager.model.JournalEntry(); + entry.setContent(text); + entry.setTimestamp(java.time.LocalDateTime.now()); + + // Sauvegarder en base + databaseManager.addJournalEntry(entry, selectedVM.getId()); + + appendJournalLine("✅ Note sauvegardée en base"); + } catch (Exception e) { + appendJournalLine("❌ Erreur sauvegarde: " + e.getMessage()); + showError("Erreur journal", e.getMessage()); } - - appendJournalLine(text); - journalInputField.clear(); - - // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) + } else { + appendJournalLine("⚠️ Aucune VM sélectionnée - note non sauvegardée"); } - +} @FXML private void handleToggleAuditLogs() { auditExpanded = !auditExpanded; From 29ef2bc429bf5ffa9f226edc77c28e5ed23e0f4f Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Mon, 20 Jul 2026 16:13:19 +0000 Subject: [PATCH 24/28] feat(integration): brancher pdfExporter.exportLabToPdf (Role 2) --- .../controller/LabController.java | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index cd2f557..d343847 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -10,6 +10,7 @@ import tg.cyberlabmanager.model.AppConfig; import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.Snapshot; import tg.cyberlabmanager.pdf.PdfExporter; import tg.cyberlabmanager.model.VMStatus; import tg.cyberlabmanager.ui.LabListView; @@ -96,6 +97,9 @@ private void initialize() { // Initialiser le DatabaseManager this.databaseManager = new DatabaseManager(); + // Initialiser le PdfExporter + this.pdfExporter = new PdfExporter(); + mockLabs(); refreshLabList(); @@ -302,7 +306,42 @@ public void onImportVMsClicked(Consumer> onSuccess, } public void onExportLabClicked(Lab lab, String filePath) { - } + // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() + executor.submit(() -> { + try { + // Récupérer les données depuis la base + List vms = databaseManager.getVMsForLab(lab.getId()); + + // Récupérer les snapshots pour chaque VM + List allSnapshots = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId())); + } + + // Récupérer les journaux pour chaque VM + List allJournals = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId())); + } + + // Exporter le PDF + pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath); + + // Succès - mise à jour de l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("✅ Export PDF terminé : " + filePath); + appendAuditLine("Export PDF du labo \"" + lab.getTitle() + "\""); + }); + + } catch (Exception e) { + // Erreur - affichage dans l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("❌ Erreur export PDF : " + e.getMessage()); + showError("Erreur export PDF", e.getMessage()); + }); + } + }); +} public void onExportOrphansClicked(String filePath) { } @@ -374,9 +413,6 @@ private void handleExportPDF() { // 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 7fbd322dc67b44cd5c3caa992b778cb90cdcdccd Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Mon, 20 Jul 2026 17:25:46 +0000 Subject: [PATCH 25/28] =?UTF-8?q?fix(ui):=20suppression=20des=20commentair?= =?UTF-8?q?es=20TODO=20r=C3=B4le2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/tg/cyberlabmanager/controller/LabController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index d343847..30ec49d 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -122,7 +122,6 @@ private List mockLabs() { private void refreshLabList() { labListContainer.getChildren().clear(); - // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() List labs = databaseManager.getAllLabs(); for (Lab lab : labs) { @@ -177,7 +176,6 @@ public void initView() { } private List getVMsForLab(int labId) { - // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) List vms = databaseManager.getVMsForLab(labId); // Fallback vers les mocks si la base est vide @@ -306,7 +304,6 @@ public void onImportVMsClicked(Consumer> onSuccess, } public void onExportLabClicked(Lab lab, String filePath) { - // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() executor.submit(() -> { try { // Récupérer les données depuis la base @@ -409,7 +406,6 @@ private void handleExportPDF() { 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()); @@ -561,7 +557,6 @@ private void handleAddJournalEntry() { appendJournalLine(text); journalInputField.clear(); - // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) if (selectedVM != null) { try { // Créer une entrée de journal @@ -710,7 +705,6 @@ private void onLabCardClicked(Lab lab) { // Rafraîchir la liste et le tableau refreshLabList(); - // TODO (Rôle 2) : remplacer mockVMsForLab par databaseManager.getVMsForLab refreshVmTable(getVMsForLab(lab.getId())); } From 88d4bfeaa6996f0bf203eff806eb4f9396299d51 Mon Sep 17 00:00:00 2001 From: M4dnolyn Date: Tue, 21 Jul 2026 09:50:40 +0000 Subject: [PATCH 26/28] feat(ui): audit automatique + accordeon system audit logs --- "docs/Images/Image coll\303\251e.png" | Bin 0 -> 70132 bytes nbactions.xml | 19 + .../controller/LabController.java | 390 +++++---- .../controller/LabController.java.bak | 765 ++++++++++++++++++ .../tg/cyberlabmanager/ui/LabListView.java | 1 + .../tg/cyberlabmanager/ui/main-view.fxml | 8 + 6 files changed, 1040 insertions(+), 143 deletions(-) create mode 100644 "docs/Images/Image coll\303\251e.png" create mode 100644 nbactions.xml create mode 100644 src/main/java/tg/cyberlabmanager/controller/LabController.java.bak diff --git "a/docs/Images/Image coll\303\251e.png" "b/docs/Images/Image coll\303\251e.png" new file mode 100644 index 0000000000000000000000000000000000000000..735249135e41140d87a3f8a601d3ccbd106f1ccb GIT binary patch literal 70132 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∨hsc(Wl2`aWFVtqv@|fVtkolmF|dcl{VCeY z%ko*B#ojcV@90pNyeyD085$`dT*#UlS(v z-B|cWv6>s*axK+&+{BZr*bJ*_UY~F%w;9SjJUBR1AT%B20?Ve#^wauyQ90xQlHDd; zu+oJP-=7>k?Gyv)w9<8C9|Gwy9An(zFFNL2vRK}I7)Kn+uRha9_4l6hJ&3yj{)dZ& z`$O^kzG_GB7`^_T^>oRW;%xf3&$t>aqNvI@L_bpj;2dbSX|h0-3}KT_2m`F1)C2wV z3g@9cM;8ND2A8Sl5PBM`M+Sa+@{(`=tBXfN^KZB(2At|f?J=qgW1{DWG6drn9CU1Z z5Xu}-wA5lxd$_Eix;gSM7R>u41lMEYW>`l04Nr!etx~>iDPKrkIRYT?W~S-|N`y6h zik8g0(67xkq+G$cr_D*q2@KarneQWiOCMjiDPyOxi1T}(Y;O^j0#3geSCwZSLtv{8 znuCg|X>DN&A0!Sr98oyu_@i04Z=dJ!MC-zR;7-2VeL`!hJT#-dF(foR!}970$L!Zv zsqgN&OXM;4`;NWv->?ey-kLe!s^R?PcHN}PryMMM{XxbsfzUxq;6 z^s`CX%zFF!nkFubAd8R(>$G^w!imRg!~wG;5Cb%7mivm*ttcSjnd zM8UQP!N~5)&n(7CpV3DY(fX3P?!)*c966z;z(s4~sQ3#rT5U)`DM`UOYJ^q3-*<(2 zj^$kNW@i$W)SD;(mEGer_Z_nr4;S#mrvS;qYohvLK~9AA#>^K1b+irFHlnO3Q-T-e zKJvOr``B>)VX_#3AR-1kx}ZIO9$n6#fL@XT2!1^XBA0yMfCK{2So?TI)-FWY)Xm-Z z^LuZZ!Pn#Vm#qHiI|ZG6uz?G8b3%L#OJW}yaj$A+{Vhfmls|8x5tt2%)E4@8ulEbI zqweh$#!kJ~a`Q#%Q*^JuZ={5(kE@8j_G4Zj&NOInjuYQoB+%E(H~ zB#3A-DlqsBIYaTi??lTFGGRuwcz28_xMXiBpVu~};5v>(4dzr`E@pMz^baPjw5P9c zkWU7ugs8&_W6I?Kw6NB!2HKa0>b!a8_=xijQj_|fx@&rbBq-Wte4OyN;tg*0hJZhG zXX}tU?RsY1%JCs21f}Bp)mS)PJ$^S?j3EIx%5U42N+DO!NT(mnoY&Oj4mS#?OYe87 zA0Z@t74t?povCAW>C;_>p?DAH_1U9if>vTc2j|k-;kmrDA}$@MU6ojamNaGN@L?hI z{eIolkd5?o^VFBEs5M@dQW%kW= z++SMyCNL2alB&LB)wqv;fhFf!n}|y0wT!$LJK2Gxu&|RU+PJy?Jr+^4Vz!-gO=8aF zhd0;OYz&@RKJms9{wDQ8=igi3UlUV#Y}T}1svFX2k-TVcjCHECP#{yI)M@y_dzooR z`axtrhHkKvt?d!3yaOs%_t~3_O?D$dit~dry){y3;#ir;vZ@=ciE}j%Ve+{!ypZBO z5E?kE04DMfy6s$3$hc{ZOsj@`{Zp)Za7x*vZ{+5MEsKee2{y2tLPba-L@nk900a%$ z4#T#k{Aw=H`Dy`y-S)ZBMi7X+)UE`m%5xvd6#^V?vF!cTeommO)uRv`S4ZQPm!CWW zN=WaP_d=8UFNzNR7>gkJ93Q$Go!{0&+-xSF+cs`!w36xpyLaOY?V#lTvb8vX>Y<|F zlQIPea`EPaEvhPrHj6#R0S+B^twL%G(4c6mVP{l$j^Z2Kh<7lK-q7nNnm0s*J;&Lw zI-yCmM)bTyGi7gDHDBH|S)?6jb0;C)7vLLx9tPpuN5n+qFAUv02;TQI=o0vWJ z+(aPbY)Hd?EtSX>?S`it`?9w!!k5DKgQv!#+#qxAiNX+eF0WqW + + + run + + jar + + + javafx:run + + + + ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs} + + ${packageClassName} + java + + + diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java index 8d79802..2fcc30c 100644 --- a/src/main/java/tg/cyberlabmanager/controller/LabController.java +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java @@ -8,16 +8,20 @@ import tg.cyberlabmanager.data.DatabaseManager; import tg.cyberlabmanager.hypervisor.HypervisorException; import tg.cyberlabmanager.hypervisor.IHypervisor; +import tg.cyberlabmanager.hypervisor.VBoxOrchestrator; import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.AuditEntry; import tg.cyberlabmanager.model.Lab; import tg.cyberlabmanager.model.VirtualMachine; import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.model.VMDescriptor; import tg.cyberlabmanager.pdf.PdfExporter; import tg.cyberlabmanager.model.VMStatus; import tg.cyberlabmanager.ui.LabListView; import tg.cyberlabmanager.ui.VmTableView; import tg.cyberlabmanager.model.JournalEntry; +import java.time.LocalDateTime; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -29,8 +33,10 @@ import static tg.cyberlabmanager.model.VMStatus.SAVED; /** - * 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). + * + *

Coordonne l'UI, la persistance ({@link DatabaseManager}, Rôle 2) et le + * pilotage de l'hyperviseur VirtualBox ({@link VBoxOrchestrator}, Rôle 3).

*/ public class LabController { @@ -40,6 +46,7 @@ public class LabController { private Lab selectedLab; private VirtualMachine selectedVM; private boolean auditExpanded = false; + private boolean viewingOrphans = false; private final ExecutorService executor = Executors.newCachedThreadPool(); @@ -95,8 +102,7 @@ private void initialize() { this.databaseManager = new DatabaseManager(); AppConfig config = databaseManager.getConfig(); - tg.cyberlabmanager.hypervisor.VBoxOrchestrator orchestrator - = new tg.cyberlabmanager.hypervisor.VBoxOrchestrator(config); + VBoxOrchestrator orchestrator = new VBoxOrchestrator(config); this.hypervisor = orchestrator; if (!orchestrator.isReady()) { @@ -107,32 +113,15 @@ private void initialize() { this.pdfExporter = new PdfExporter(); - mockLabs(); refreshLabList(); } - // ── 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(); List labs = databaseManager.getAllLabs(); - // Fallback vers les mocks si la base est vide - if (labs.isEmpty()) { - labs = mockLabs(); - } - for (Lab lab : labs) { boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId(); @@ -153,7 +142,6 @@ private void refreshLabList() { lab.getTitle(), lab.getCategory(), badgeStyle, isSelected ); - // Listener de clic — Jour 2 card.setOnMouseClicked(e -> onLabCardClicked(lab)); labListContainer.getChildren().add(card); @@ -181,26 +169,18 @@ public LabController(IHypervisor hypervisor, DatabaseManager databaseManager, Pd } public void initView() { - } private List getVMsForLab(int labId) { - List vms = databaseManager.getVMsForLab(labId); - - // Fallback vers les mocks si la base est vide - if (vms.isEmpty()) { - vms = mockVMsForLab(labId); - } - - return vms; + return databaseManager.getVMsForLab(labId); } public List getAllLabs() { - throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + return databaseManager.getAllLabs(); } public List getOrphanVMs() { - throw new UnsupportedOperationException("À implémenter après merge Rôle 2"); + return databaseManager.getOrphanVMs(); } public void onStartVMClicked(VirtualMachine vm, @@ -286,61 +266,176 @@ public void onRestoreSnapshotClicked(VirtualMachine vm, } public void onAddJournalEntry(VirtualMachine vm, String text) { - + if (vm == null || text == null || text.isBlank()) { + return; + } + JournalEntry entry = new JournalEntry(); + entry.setContent(text); + entry.setTimestamp(LocalDateTime.now()); + databaseManager.addJournalEntry(entry, vm.getId()); } - public void onImportVMsClicked(Consumer> onSuccess, - Consumer onError) { + // ── Import de VM déjà présentes dans VirtualBox ────────────────────── + /** + * Déclenche le listage des machines virtuelles disponibles dans + * l'hyperviseur, en vue de leur import (rattachement en base). + */ + public void onImportVMsClicked() { 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")); + List descriptors = hypervisor.listVMs(); + javafx.application.Platform.runLater(() -> showImportSelectionDialog(descriptors)); + } catch (HypervisorException e) { + javafx.application.Platform.runLater(() -> + showError("Erreur listage VM", e.getMessage())); + } + }); + } + + private void showImportSelectionDialog(List descriptors) { + if (descriptors.isEmpty()) { + showError("Aucune VM trouvée", "Aucune machine virtuelle détectée dans VirtualBox."); + return; + } + + // Exclure les VM dont l'UUID est déjà connu en base (tous labos + orphelines confondus) + List knownUuids = new ArrayList<>(); + for (Lab lab : databaseManager.getAllLabs()) { + for (VirtualMachine vm : databaseManager.getVMsForLab(lab.getId())) { + knownUuids.add(vm.getUuid()); + } + } + for (VirtualMachine vm : databaseManager.getOrphanVMs()) { + knownUuids.add(vm.getUuid()); + } + + List importable = descriptors.stream() + .filter(d -> !knownUuids.contains(d.uuid())) + .toList(); + + if (importable.isEmpty()) { + showError("Aucune VM à importer", + "Toutes les VM VirtualBox détectées sont déjà rattachées dans l'application."); + return; + } + + javafx.scene.control.ListView listView = new javafx.scene.control.ListView<>(); + listView.getItems().addAll(importable); + listView.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + listView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() { + @Override + protected void updateItem(VMDescriptor item, boolean empty) { + super.updateItem(item, empty); + setText(empty || item == null ? null : item.name() + " (" + item.uuid() + ")"); } }); + + javafx.scene.control.Dialog> dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle("Importer des VM"); + dialog.setHeaderText("Sélectionnez les VM à rattacher" + + (selectedLab != null ? " au labo \"" + selectedLab.getTitle() + "\"" : " (sans labo)")); + dialog.getDialogPane().setContent(listView); + dialog.getDialogPane().getButtonTypes().addAll( + javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL); + + dialog.setResultConverter(button -> + button == javafx.scene.control.ButtonType.OK + ? new ArrayList<>(listView.getSelectionModel().getSelectedItems()) + : null + ); + + dialog.showAndWait().ifPresent(selectedDescriptors -> { + if (selectedDescriptors.isEmpty()) { + return; + } + + List selectedVMs = new ArrayList<>(); + for (VMDescriptor d : selectedDescriptors) { + VirtualMachine vm = new VirtualMachine(); + vm.setName(d.name()); + vm.setUuid(d.uuid()); + vm.setStatus(VMStatus.UNKNOWN); + selectedVMs.add(vm); + } + + Integer labId = (selectedLab != null) ? selectedLab.getId() : null; + onImportConfirm(selectedVMs, labId); + }); + } + + /** + * Persiste en base les machines virtuelles sélectionnées pour import, + * rattachées (ou non) au laboratoire indiqué. + * + * @param selectedVMs machines virtuelles sélectionnées + * @param labId identifiant du laboratoire cible, ou {@code null} + */ + public void onImportConfirm(List selectedVMs, Integer labId) { + executor.submit(() -> { + List alreadyImported = new ArrayList<>(); + List importedOk = new ArrayList<>(); + + for (VirtualMachine vm : selectedVMs) { + try { + databaseManager.saveVirtualMachine(vm, labId); + importedOk.add(vm); + } catch (RuntimeException e) { + // Filet de sécurité : la VM a pu être importée entre le listage et la confirmation + if (e.getMessage() != null && e.getMessage().contains("UNIQUE constraint")) { + alreadyImported.add(vm.getName()); + } else { + javafx.application.Platform.runLater(() -> + showError("Erreur import VM", e.getMessage())); + return; + } + } + } + + javafx.application.Platform.runLater(() -> { + if (labId != null && selectedLab != null && labId.equals(selectedLab.getId())) { + refreshVmTable(getVMsForLab(selectedLab.getId())); + } + if (!importedOk.isEmpty()) { + appendJournalLine(importedOk.size() + " VM importée(s)"); + persistAuditEntry(null, "Import VM", + importedOk.size() + " VM importée(s)" + + (labId == null ? " (orphelines)" : ""), + selectedLab != null ? selectedLab.getTitle() : null); + } + if (!alreadyImported.isEmpty()) { + showError("VM déjà importées", + "Ces VM étaient déjà connues et n'ont pas été réimportées : " + + String.join(", ", alreadyImported)); + } + }); + }); } public void onExportLabClicked(Lab lab, String filePath) { executor.submit(() -> { try { - // Récupérer les données depuis la base List vms = databaseManager.getVMsForLab(lab.getId()); - // Récupérer les snapshots pour chaque VM List allSnapshots = new ArrayList<>(); for (VirtualMachine vm : vms) { allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId())); } - // Récupérer les journaux pour chaque VM List allJournals = new ArrayList<>(); for (VirtualMachine vm : vms) { allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId())); } - // Exporter le PDF pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath); - // Succès - mise à jour de l'UI javafx.application.Platform.runLater(() -> { appendJournalLine("✅ Export PDF terminé : " + filePath); - appendAuditLine("Export PDF du labo \"" + lab.getTitle() + "\""); + persistAuditEntry(null, "Export PDF", + "Export PDF du labo \"" + lab.getTitle() + "\"", lab.getTitle()); }); } catch (Exception e) { - // Erreur - affichage dans l'UI javafx.application.Platform.runLater(() -> { appendJournalLine("❌ Erreur export PDF : " + e.getMessage()); showError("Erreur export PDF", e.getMessage()); @@ -353,7 +448,8 @@ public void onExportOrphansClicked(String filePath) { } /** - * Persiste la configuration mise à jour dans la base et dans l'appel. + * Persiste la configuration mise à jour dans la base et l'applique à + * l'hyperviseur déjà instancié. * * @param newConfig nouvelle configuration applicative */ @@ -368,40 +464,69 @@ 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") - ); + private void handleNewLab() { + javafx.scene.control.Dialog dialog = new javafx.scene.control.Dialog<>(); + dialog.setTitle("Nouveau laboratoire"); + dialog.setHeaderText("Créer un nouveau laboratoire"); - File selectedFile = fileChooser.showOpenDialog( - labListContainer.getScene().getWindow() - ); + javafx.scene.control.ButtonType createButtonType = + new javafx.scene.control.ButtonType("Créer", javafx.scene.control.ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().addAll( + createButtonType, javafx.scene.control.ButtonType.CANCEL); - if (selectedFile == null) { - return; - } + javafx.scene.control.TextField titleField = new javafx.scene.control.TextField(); + titleField.setPromptText("Titre du labo"); - // 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()); - }, - errorMsg -> showError("Erreur d'import", errorMsg) - ); + javafx.scene.control.TextArea descriptionField = new javafx.scene.control.TextArea(); + descriptionField.setPromptText("Description"); + descriptionField.setPrefRowCount(3); + + javafx.scene.control.ComboBox categoryCombo = new javafx.scene.control.ComboBox<>(); + categoryCombo.getItems().addAll("Malware", "Pentest", "Network", "Forensics"); + categoryCombo.setPromptText("Catégorie"); + categoryCombo.setMaxWidth(Double.MAX_VALUE); + + javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(10, + new javafx.scene.control.Label("Titre :"), titleField, + new javafx.scene.control.Label("Description :"), descriptionField, + new javafx.scene.control.Label("Catégorie :"), categoryCombo); + content.setPadding(new javafx.geometry.Insets(10)); + dialog.getDialogPane().setContent(content); + + javafx.scene.Node createButton = dialog.getDialogPane().lookupButton(createButtonType); + createButton.setDisable(true); + titleField.textProperty().addListener((obs, oldVal, newVal) -> + createButton.setDisable(newVal == null || newVal.isBlank())); + + dialog.setResultConverter(button -> { + if (button != createButtonType) { + return null; + } + Lab lab = new Lab(); + lab.setTitle(titleField.getText().trim()); + lab.setDescription(descriptionField.getText()); + lab.setCategory(categoryCombo.getValue() != null ? categoryCombo.getValue() : "Malware"); + return lab; + }); + + dialog.showAndWait().ifPresent(lab -> { + try { + databaseManager.saveLab(lab); + refreshLabList(); + appendJournalLine("Labo \"" + lab.getTitle() + "\" créé"); + } catch (Exception e) { + showError("Erreur création labo", e.getMessage()); + } + }); + } + + @FXML + private void handleImportVM() { + onImportVMsClicked(); } @FXML private void handleExportPDF() { - // 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; } @@ -411,7 +536,6 @@ private void handleExportPDF() { 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( @@ -419,10 +543,9 @@ private void handleExportPDF() { ); if (selectedFile == null) { - return; // L'utilisateur a annulé + return; } - // Pour l'instant, on appelle le stub métier (déjà présent dans LabController) onExportLabClicked(selectedLab, selectedFile.getAbsolutePath()); } @@ -440,7 +563,7 @@ private void handleStartVM() { updateControlButtons(VMStatus.RUNNING); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM started successfully"); - appendAuditLine("VM démarrée"); + persistAuditEntry(selectedVM.getId(), "Start VM", "VM démarrée", selectedLab.getTitle()); }, errorMsg -> { btnStart.setDisable(false); @@ -463,7 +586,7 @@ private void handleStopVM() { updateControlButtons(VMStatus.POWERED_OFF); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM stopped"); - appendAuditLine("VM arrêtée"); + persistAuditEntry(selectedVM.getId(), "Stop VM", "VM arrêtée", selectedLab.getTitle()); }, errorMsg -> { btnStop.setDisable(false); @@ -486,7 +609,7 @@ private void handleSaveState() { updateControlButtons(VMStatus.SAVED); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("VM state saved"); - appendAuditLine("État sauvegardé"); + persistAuditEntry(selectedVM.getId(), "Save State", "État sauvegardé", selectedLab.getTitle()); }, errorMsg -> { btnSaveState.setDisable(false); @@ -516,14 +639,17 @@ private void handleTakeSnapshot() { Snapshot snapshot = new Snapshot(); snapshot.setName(name); snapshot.setDescription(""); - snapshot.setCreatedAt(java.time.LocalDateTime.now()); + snapshot.setCreatedAt(LocalDateTime.now()); snapshot.setOnline(selectedVM.getStatus() == VMStatus.RUNNING); selectedVM.addSnapshot(snapshot); + databaseManager.saveSnapshot(snapshot, selectedVM.getId()); + snapshotCombo.getItems().add(name); refreshVmTable(getVMsForLab(selectedLab.getId())); appendJournalLine("Snapshot \"" + name + "\" created"); - appendAuditLine("Snapshot \"" + name + "\" créé"); + persistAuditEntry(selectedVM.getId(), "Take Snapshot", + "Snapshot \"" + name + "\" créé", selectedLab.getTitle()); }, errorMsg -> showError("Erreur snapshot", errorMsg) ); @@ -555,7 +681,8 @@ private void handleRestoreSnapshot() { onRestoreSnapshotClicked(selectedVM, snapshotName, () -> { appendJournalLine("Snapshot \"" + snapshotName + "\" restored"); - appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); + persistAuditEntry(selectedVM.getId(), "Restore Snapshot", + "Snapshot \"" + snapshotName + "\" restauré", selectedLab.getTitle()); }, errorMsg -> showError("Erreur restauration snapshot", errorMsg) ); @@ -574,12 +701,10 @@ private void handleAddJournalEntry() { if (selectedVM != null) { try { - // Créer une entrée de journal JournalEntry entry = new JournalEntry(); entry.setContent(text); - entry.setTimestamp(java.time.LocalDateTime.now()); + entry.setTimestamp(LocalDateTime.now()); - // Sauvegarder en base databaseManager.addJournalEntry(entry, selectedVM.getId()); appendJournalLine("✅ Note sauvegardée en base"); @@ -604,38 +729,6 @@ 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(); @@ -684,7 +777,6 @@ private void refreshVmTable(List vms) { vm.getName(), shortUuid, statusText, "" ); - // Listener clic VM → Jour 3 row.setOnMouseClicked(e -> onVmRowClicked(vm)); vmListContainer.getChildren().add(row); } @@ -694,10 +786,8 @@ private void onLabCardClicked(Lab lab) { 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); @@ -705,7 +795,6 @@ private void onLabCardClicked(Lab lab) { vmTableHeader.setVisible(true); vmTableHeader.setManaged(true); - // Remettre le panneau droit en état vide selectedVmNameLabel.setText(""); controlsPlaceholder.setVisible(true); controlsPlaceholder.setManaged(true); @@ -718,9 +807,7 @@ private void onLabCardClicked(Lab lab) { btnTakeSnapshot.setVisible(false); btnTakeSnapshot.setManaged(false); - // Rafraîchir la liste et le tableau refreshLabList(); - refreshVmTable(getVMsForLab(lab.getId())); } @@ -729,11 +816,9 @@ private void onVmRowClicked(VirtualMachine 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()); } @@ -766,10 +851,29 @@ private void showError(String title, String 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"); + /** + * Trace une action dans la zone d'audit visuelle ET la persiste en base + * via {@link DatabaseManager#addAuditEntry(AuditEntry)}. + * + * @param vmId identifiant local de la VM concernée, ou {@code null} + * @param action action auditée (ex: "Start VM") + * @param details détails complémentaires de l'action + * @param labName nom du laboratoire concerné, ou {@code null} + */ + private void persistAuditEntry(Integer vmId, String action, String details, String labName) { + AuditEntry entry = new AuditEntry(); + entry.setTimestamp(LocalDateTime.now()); + entry.setVmId(vmId); + entry.setAction(action); + entry.setDetails(details); + entry.setLabName(labName); + + auditArea.appendText(entry.getLog() + "\n"); + + try { + databaseManager.addAuditEntry(entry); + } catch (Exception e) { + appendJournalLine("⚠️ Erreur persistance audit : " + e.getMessage()); + } } -} +} \ No newline at end of file diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak b/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak new file mode 100644 index 0000000..d343847 --- /dev/null +++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java.bak @@ -0,0 +1,765 @@ +package tg.cyberlabmanager.controller; + +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; +import tg.cyberlabmanager.model.AppConfig; +import tg.cyberlabmanager.model.Lab; +import tg.cyberlabmanager.model.VirtualMachine; +import tg.cyberlabmanager.model.Snapshot; +import tg.cyberlabmanager.pdf.PdfExporter; +import tg.cyberlabmanager.model.VMStatus; +import tg.cyberlabmanager.ui.LabListView; +import tg.cyberlabmanager.ui.VmTableView; +import tg.cyberlabmanager.model.JournalEntry; + +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 + * 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 VirtualMachine selectedVM; + private boolean auditExpanded = false; + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @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 javafx.scene.control.TextField journalInputField; + @FXML + private javafx.scene.control.TextArea auditArea; + @FXML + private Label auditArrow; + + @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(); + + // Initialiser le DatabaseManager + this.databaseManager = new DatabaseManager(); + + // Initialiser le PdfExporter + this.pdfExporter = new PdfExporter(); + + mockLabs(); + refreshLabList(); + + } + + // ── 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(); + + // TODO (Rôle 2) : remplacer mockLabs() par databaseManager.getAllLabs() + List labs = databaseManager.getAllLabs(); + + 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); + } + } + + /** + * Constructeur par défaut initialisant le contrôleur du laboratoire. + */ + 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) { + this.hypervisor = hypervisor; + this.databaseManager = databaseManager; + this.pdfExporter = pdfExporter; + } + + public void initView() { + + } + + private List getVMsForLab(int labId) { + // TODO (Rôle 2) : remplacer par databaseManager.getVMsForLab(labId) + List vms = databaseManager.getVMsForLab(labId); + + // Fallback vers les mocks si la base est vide + if (vms.isEmpty()) { + vms = mockVMsForLab(labId); + } + + return vms; +} + + 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) { + + 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, + 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, + String name, + 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) { + + } + + 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) { + // TODO (Rôle 2) : remplacer par un appel réel à pdfExporter.exportLabReport() + executor.submit(() -> { + try { + // Récupérer les données depuis la base + List vms = databaseManager.getVMsForLab(lab.getId()); + + // Récupérer les snapshots pour chaque VM + List allSnapshots = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId())); + } + + // Récupérer les journaux pour chaque VM + List allJournals = new ArrayList<>(); + for (VirtualMachine vm : vms) { + allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId())); + } + + // Exporter le PDF + pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath); + + // Succès - mise à jour de l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("✅ Export PDF terminé : " + filePath); + appendAuditLine("Export PDF du labo \"" + lab.getTitle() + "\""); + }); + + } catch (Exception e) { + // Erreur - affichage dans l'UI + javafx.application.Platform.runLater(() -> { + appendJournalLine("❌ Erreur export PDF : " + e.getMessage()); + showError("Erreur export PDF", e.getMessage()); + }); + } + }); +} + + public void onExportOrphansClicked(String filePath) { + } + + /** + * Persiste la configuration mise a jour. + * + * @param newConfig nouvelle configuration applicative + */ + 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 -> { + if (selectedLab != null) { + getVMsForLab(selectedLab.getId()).addAll(vms); + refreshVmTable(getVMsForLab(selectedLab.getId())); + } + appendJournalLine("Import terminé : " + selectedFile.getName()); + appendAuditLine("VM importée depuis " + selectedFile.getName()); + }, + errorMsg -> showError("Erreur d'import", errorMsg) + ); + } + + @FXML + private void handleExportPDF() { + // 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()); + + } + + @FXML + private void handleStartVM() { + if (selectedVM == null) { + return; + } + + btnStart.setDisable(true); + + onStartVMClicked(selectedVM, + () -> { + btnStart.setDisable(false); + updateControlButtons(VMStatus.RUNNING); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM started successfully"); + appendAuditLine("VM démarrée"); + }, + errorMsg -> { + btnStart.setDisable(false); + showError("Erreur démarrage VM", errorMsg); + } + ); + } + + @FXML + private void handleStopVM() { + if (selectedVM == null) { + return; + } + + btnStop.setDisable(true); + + onStopVMClicked(selectedVM, + () -> { + btnStop.setDisable(false); + updateControlButtons(VMStatus.POWERED_OFF); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM stopped"); + appendAuditLine("VM arrêtée"); + }, + errorMsg -> { + btnStop.setDisable(false); + showError("Erreur arrêt VM", errorMsg); + } + ); + } + + @FXML + private void handleSaveState() { + if (selectedVM == null) { + return; + } + + btnSaveState.setDisable(true); + + onSaveStateClicked(selectedVM, + () -> { + btnSaveState.setDisable(false); + updateControlButtons(VMStatus.SAVED); + refreshVmTable(getVMsForLab(selectedLab.getId())); + appendJournalLine("VM state saved"); + appendAuditLine("État sauvegardé"); + }, + errorMsg -> { + btnSaveState.setDisable(false); + showError("Erreur sauvegarde état", errorMsg); + } + ); + } + + @FXML + private void handleTakeSnapshot() { + 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"); + appendAuditLine("Snapshot \"" + name + "\" créé"); + }, + errorMsg -> showError("Erreur snapshot", errorMsg) + ); + }); + } + + @FXML + private void handleRestoreSnapshot() { + 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"); + appendAuditLine("Snapshot \"" + snapshotName + "\" restauré"); + }, + errorMsg -> showError("Erreur restauration snapshot", errorMsg) + ); + }); + } + + @FXML +private void handleAddJournalEntry() { + String text = journalInputField.getText(); + if (text == null || text.isBlank()) { + return; + } + + appendJournalLine(text); + journalInputField.clear(); + + // TODO (Rôle 2) : databaseManager.addJournalEntry(selectedLab, selectedVM, text) + if (selectedVM != null) { + try { + // Créer une entrée de journal + tg.cyberlabmanager.model.JournalEntry entry = new tg.cyberlabmanager.model.JournalEntry(); + entry.setContent(text); + entry.setTimestamp(java.time.LocalDateTime.now()); + + // Sauvegarder en base + databaseManager.addJournalEntry(entry, selectedVM.getId()); + + appendJournalLine("✅ Note sauvegardée en base"); + } catch (Exception e) { + appendJournalLine("❌ Erreur sauvegarde: " + e.getMessage()); + showError("Erreur journal", e.getMessage()); + } + } else { + appendJournalLine("⚠️ Aucune VM sélectionnée - note non sauvegardée"); + } +} + @FXML + private void handleToggleAuditLogs() { + auditExpanded = !auditExpanded; + auditArea.setVisible(auditExpanded); + auditArea.setManaged(auditExpanded); + auditArrow.setText(auditExpanded ? "⌄" : "›"); + } + + 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; + + // 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(getVMsForLab(lab.getId())); + } + + private void onVmRowClicked(VirtualMachine 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); + } + + 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(); + } + + 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"); + } +} diff --git a/src/main/java/tg/cyberlabmanager/ui/LabListView.java b/src/main/java/tg/cyberlabmanager/ui/LabListView.java index 930ea8f..0d0133a 100644 --- a/src/main/java/tg/cyberlabmanager/ui/LabListView.java +++ b/src/main/java/tg/cyberlabmanager/ui/LabListView.java @@ -3,6 +3,7 @@ import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.layout.VBox; +import tg.cyberlabmanager.ui.LabListView; /** * Vue chargee d'afficher la liste des laboratoires et leurs cartes. diff --git a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml index 2f307af..e8fa6a5 100644 --- a/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml +++ b/src/main/resources/tg/cyberlabmanager/ui/main-view.fxml @@ -49,8 +49,16 @@ +