diff --git "a/docs/Images/Image coll\303\251e.png" "b/docs/Images/Image coll\303\251e.png"
new file mode 100644
index 0000000..7352491
Binary files /dev/null and "b/docs/Images/Image coll\303\251e.png" differ
diff --git a/docs/Images/mockup-final.png b/docs/Images/mockup-final.png
new file mode 100644
index 0000000..f57ca06
Binary files /dev/null and b/docs/Images/mockup-final.png differ
diff --git a/nbactions.xml b/nbactions.xml
new file mode 100644
index 0000000..186cf9c
--- /dev/null
+++ b/nbactions.xml
@@ -0,0 +1,19 @@
+
+
+
+ run
+
+ jar
+
+
+ javafx:run
+
+
+
+ ${exec.vmArgs} -classpath %classpath ${exec.mainClass} ${exec.appArgs}
+
+ ${packageClassName}
+ java
+
+
+
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 873c65e..505ba0e 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;
@@ -34,4 +34,4 @@
exports tg.cyberlabmanager.hypervisor;
exports tg.cyberlabmanager.pdf;
exports tg.cyberlabmanager.ui;
-}
+}
\ No newline at end of file
diff --git a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java
index d8770e1..0f55392 100644
--- a/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java
+++ b/src/main/java/tg/cyberlabmanager/app/CyberLabApp.java
@@ -4,13 +4,15 @@
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
-
-import java.io.IOException;
+import tg.cyberlabmanager.controller.LabController;
/**
* Application JavaFX principale de Cyber Lab Manager.
*/
public class CyberLabApp extends Application {
+
+ private LabController controller;
+
/**
* Cree l'application JavaFX.
*/
@@ -24,11 +26,27 @@ public CyberLabApp() {
* @throws IOException si le chargement du fichier FXML echoue
*/
@Override
- public void start(Stage stage) throws IOException {
- FXMLLoader loader = new FXMLLoader(CyberLabApp.class.getResource("/tg/cyberlabmanager/ui/main-view.fxml"));
- 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);
+ }
}
diff --git a/src/main/java/tg/cyberlabmanager/controller/LabController.java b/src/main/java/tg/cyberlabmanager/controller/LabController.java
index bc81b5a..bdc61aa 100644
--- a/src/main/java/tg/cyberlabmanager/controller/LabController.java
+++ b/src/main/java/tg/cyberlabmanager/controller/LabController.java
@@ -1,174 +1,1143 @@
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.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 javafx.fxml.FXML;
-import javafx.scene.control.Label;
+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;
+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 coordonnant l'interface, la persistance et l'hyperviseur.
+ * 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 {
+
private IHypervisor hypervisor;
private DatabaseManager databaseManager;
private PdfExporter pdfExporter;
+ private Lab selectedLab;
+ private VirtualMachine selectedVM;
+ private boolean auditExpanded = false;
+ private boolean viewingOrphans = 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 javafx.scene.control.Button btnAssignLab;
+ @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() {
+ this.databaseManager = new DatabaseManager();
+ AppConfig config = databaseManager.getConfig();
+
+ VBoxOrchestrator orchestrator = new VBoxOrchestrator(config);
+ this.hypervisor = orchestrator;
+
+ if (!orchestrator.isReady()) {
+ showError("VirtualBox introuvable",
+ "VBoxManage n'a pas été détecté automatiquement. "
+ + "Vous pourrez configurer son chemin dans les paramètres.");
+ }
+
+ this.pdfExporter = new PdfExporter();
+
+ refreshLabList();
+ }
+
+ // ── Affichage de la liste des labos ──────────────────────────────────
+ private void refreshLabList() {
+ labListContainer.getChildren().clear();
+
+ List labs = databaseManager.getAllLabs();
+
+ for (Lab lab : labs) {
+ boolean isSelected = selectedLab != null && selectedLab.getId() == lab.getId() && !viewingOrphans;
+
+ 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);
+
+ card.setOnMouseClicked(e -> onLabCardClicked(lab));
+
+ labListContainer.getChildren().add(card);
+ }
+
+ // ── Carte « VMs sans labo » ──────────────────────────────────────
+ List orphans = databaseManager.getOrphanVMs();
+ if (!orphans.isEmpty()) {
+ javafx.scene.layout.VBox orphanCard = LabListView.createOrphanCard(
+ orphans.size(), viewingOrphans);
+ orphanCard.setOnMouseClicked(e -> onOrphanCardClicked(orphans));
+ labListContainer.getChildren().add(orphanCard);
+ }
+ }
/**
- * Cree un contrôleur vide utilise par JavaFX lors du chargement FXML.
+ * Constructeur par défaut initialisant le contrôleur du laboratoire.
*/
public LabController() {
}
- /**
- * Cree un contrôleur avec ses dépendances injectées.
- *
- * @param hypervisor service de pilotage de l'hyperviseur
- * @param databaseManager service de persistance
- * @param pdfExporter service d'export PDF
- */
+ 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;
}
- @FXML
- private void initialize() {
- if (statusLabel != null) {
- statusLabel.setText("Cyber Lab Manager pret");
- }
+ public void initView() {
}
- /**
- * Déclenche le démarrage d'une machine virtuelle.
- *
- * @param vm machine virtuelle cible
- */
- public void onStartVMClicked(VirtualMachine vm) {
- throw new UnsupportedOperationException("Not implemented yet");
+ private List getVMsForLab(int labId) {
+ return databaseManager.getVMsForLab(labId);
}
- /**
- * Déclenche l'arrêt d'une machine virtuelle.
- *
- * @param vm machine virtuelle cible
- */
- public void onStopVMClicked(VirtualMachine vm) {
- throw new UnsupportedOperationException("Not implemented yet");
+ public List getAllLabs() {
+ return databaseManager.getAllLabs();
}
- /**
- * Déclenche la sauvegarde d'etat d'une machine virtuelle.
- *
- * @param vm machine virtuelle cible
- */
- public void onSaveStateClicked(VirtualMachine vm) {
- throw new UnsupportedOperationException("Not implemented yet");
+ public List getOrphanVMs() {
+ return databaseManager.getOrphanVMs();
}
- /**
- * Déclenche 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");
+ 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) {
+ try {
+ VMStatus liveStatus = hypervisor.getStatus(vm.getUuid());
+ if (liveStatus == VMStatus.RUNNING) {
+ javafx.application.Platform.runLater(() -> {
+ vm.setStatus(VMStatus.RUNNING);
+ onSuccess.run();
+ });
+ return;
+ }
+ } catch (Exception ignored) {
+ }
+ javafx.application.Platform.runLater(() -> onError.accept(e.getMessage()));
+ }
+ });
}
- /**
- * Déclenche 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");
+ 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()));
+ }
+ });
}
- /**
- * 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");
+ if (vm == null || text == null || text.isBlank()) {
+ return;
+ }
+ JournalEntry entry = new JournalEntry();
+ entry.setContent(text);
+ entry.setTimestamp(LocalDateTime.now());
+ databaseManager.addJournalEntry(entry, vm.getId());
}
+ // ── Import de VM déjà présentes dans VirtualBox ──────────────────────
+
/**
- * Declenche l'import des machines virtuelles disponibles.
+ * Déclenche le listage des machines virtuelles disponibles dans
+ * l'hyperviseur, en vue de leur import (rattachement en base).
*/
public void onImportVMsClicked() {
- throw new UnsupportedOperationException("Not implemented yet");
+ executor.submit(() -> {
+ try {
+ 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);
+ });
}
/**
- * Confirme l'import de machines virtuelles selectionnees.
+ * Persiste en base les machines virtuelles sélectionnées pour import,
+ * rattachées (ou non) au laboratoire indiqué.
*
- * @param selectedVMs machines virtuelles selectionnees
- * @param labId identifiant du laboratoire cible, ou {@code null}
+ * @param selectedVMs machines virtuelles sélectionnées
+ * @param labId identifiant du laboratoire cible, ou {@code null}
*/
public void onImportConfirm(List selectedVMs, Integer labId) {
- throw new UnsupportedOperationException("Not implemented yet");
+ 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 {
+ List vms = databaseManager.getVMsForLab(lab.getId());
+
+ List allSnapshots = new ArrayList<>();
+ for (VirtualMachine vm : vms) {
+ allSnapshots.addAll(databaseManager.getSnapshotsForVM(vm.getId()));
+ }
+
+ List allJournals = new ArrayList<>();
+ for (VirtualMachine vm : vms) {
+ allJournals.addAll(databaseManager.getJournalEntriesForVM(vm.getId()));
+ }
+
+ pdfExporter.exportLabToPdf(lab, vms, allSnapshots, allJournals, filePath);
+
+ javafx.application.Platform.runLater(() -> {
+ appendJournalLine("✅ Export PDF terminé : " + filePath);
+ persistAuditEntry(null, "Export PDF",
+ "Export PDF du labo \"" + lab.getTitle() + "\"", lab.getTitle());
+ });
+
+ } catch (Exception e) {
+ javafx.application.Platform.runLater(() -> {
+ appendJournalLine("❌ Erreur export PDF : " + e.getMessage());
+ showError("Erreur export PDF", e.getMessage());
+ });
+ }
+ });
+ }
+
+ public void onExportOrphansClicked(String filePath) {
}
/**
- * Declenche l'export PDF d'un laboratoire.
+ * Persiste la configuration mise à jour dans la base et l'applique à
+ * l'hyperviseur déjà instancié.
*
- * @param lab laboratoire a exporter
+ * @param newConfig nouvelle configuration applicative
*/
- public void onExportLabClicked(Lab lab) {
- throw new UnsupportedOperationException("Not implemented yet");
+ public void onConfigUpdated(AppConfig newConfig) {
+ try {
+ hypervisor.applyConfig(newConfig);
+ databaseManager.saveConfig(newConfig);
+ appendJournalLine("Configuration mise à jour");
+ } catch (HypervisorException e) {
+ showError("Erreur configuration hyperviseur", e.getMessage());
+ }
}
- /**
- * Declenche l'export des machines virtuelles sans laboratoire.
- */
- public void onExportOrphansClicked() {
- throw new UnsupportedOperationException("Not implemented yet");
+ @FXML
+ private void handleNewLab() {
+ javafx.scene.control.Dialog dialog = new javafx.scene.control.Dialog<>();
+ dialog.setTitle("Nouveau laboratoire");
+ dialog.setHeaderText("Créer un nouveau laboratoire");
+
+ 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);
+
+ javafx.scene.control.TextField titleField = new javafx.scene.control.TextField();
+ titleField.setPromptText("Titre du labo");
+
+ 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);
+ selectedLab = lab;
+ viewingOrphans = false;
+ refreshLabList();
+ appendJournalLine("Labo \"" + lab.getTitle() + "\" créé");
+
+ // Proposition d'importer des VM immédiatement (cf. diagramme séquence «Créer
+ // labo»)
+ javafx.scene.control.Alert prompt = new javafx.scene.control.Alert(
+ javafx.scene.control.Alert.AlertType.CONFIRMATION);
+ prompt.setTitle("Labo créé");
+ prompt.setHeaderText("Labo \"" + lab.getTitle() + "\" créé avec succès !");
+ prompt.setContentText("Voulez-vous importer des VM dans ce labo maintenant ?");
+
+ javafx.scene.control.ButtonType btnOui = new javafx.scene.control.ButtonType("Oui, importer des VM",
+ javafx.scene.control.ButtonBar.ButtonData.YES);
+ javafx.scene.control.ButtonType btnNon = new javafx.scene.control.ButtonType("Non, plus tard",
+ javafx.scene.control.ButtonBar.ButtonData.NO);
+ prompt.getButtonTypes().setAll(btnOui, btnNon);
+
+ prompt.showAndWait().ifPresent(response -> {
+ if (response == btnOui) {
+ onImportVMsClicked();
+ }
+ });
+
+ } catch (Exception e) {
+ showError("Erreur création labo", e.getMessage());
+ }
+ });
+ }
+
+ @FXML
+ private void handleImportVM() {
+ onImportVMsClicked();
+ }
+
+ @FXML
+ private void handleExportPDF() {
+ if (selectedLab == null) {
+ return;
+ }
+
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Exporter le rapport PDF");
+ fileChooser.getExtensionFilters().addAll(
+ new FileChooser.ExtensionFilter("PDF Document", "*.pdf"));
+ fileChooser.setInitialFileName(selectedLab.getTitle().replace(" ", "_") + ".pdf");
+
+ File selectedFile = fileChooser.showSaveDialog(
+ labListContainer.getScene().getWindow());
+
+ if (selectedFile == null) {
+ return;
+ }
+
+ onExportLabClicked(selectedLab, selectedFile.getAbsolutePath());
+ }
+
+ @FXML
+ private void handleStartVM() {
+ if (selectedVM == null) {
+ return;
+ }
+
+ btnStart.setDisable(true);
+
+ onStartVMClicked(selectedVM,
+ () -> {
+ btnStart.setDisable(false);
+ selectedVM.setStatus(VMStatus.RUNNING);
+ persisterStatutVM(selectedVM);
+ updateControlButtons(VMStatus.RUNNING);
+ refreshVmTable(getVMsForLab(selectedLab.getId()));
+ appendJournalLine("VM démarrée avec succès");
+ persistAuditEntry(selectedVM.getId(), "Start VM", "VM démarrée", selectedLab.getTitle());
+ },
+ 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);
+ selectedVM.setStatus(VMStatus.POWERED_OFF);
+ persisterStatutVM(selectedVM);
+ updateControlButtons(VMStatus.POWERED_OFF);
+ refreshVmTable(getVMsForLab(selectedLab.getId()));
+ appendJournalLine("VM arrêtée");
+ persistAuditEntry(selectedVM.getId(), "Stop VM", "VM arrêtée", selectedLab.getTitle());
+ },
+ 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);
+ selectedVM.setStatus(VMStatus.SAVED);
+ persisterStatutVM(selectedVM);
+ updateControlButtons(VMStatus.SAVED);
+ refreshVmTable(getVMsForLab(selectedLab.getId()));
+ appendJournalLine("État de la VM sauvegardé");
+ persistAuditEntry(selectedVM.getId(), "Save State", "État sauvegardé", selectedLab.getTitle());
+ },
+ errorMsg -> {
+ btnSaveState.setDisable(false);
+ showError("Erreur sauvegarde état", errorMsg);
+ });
}
/**
- * Retourne tous les laboratoires connus.
- *
- * @return liste des laboratoires
+ * Persiste le statut courant d'une VM en base sans modifier son labo de
+ * rattachement.
*/
- public List getAllLabs() {
- return databaseManager.getAllLabs();
+ private void persisterStatutVM(VirtualMachine vm) {
+ try {
+ Integer labId = (selectedLab != null) ? selectedLab.getId() : null;
+ databaseManager.saveVirtualMachine(vm, labId);
+ } catch (Exception e) {
+ appendJournalLine("⚠️ Impossible de sauvegarder le statut en base : " + e.getMessage());
+ }
+ }
+
+ @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, "",
+ () -> {
+ Snapshot snapshot = new Snapshot();
+ snapshot.setName(name);
+ snapshot.setDescription("");
+ 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");
+ persistAuditEntry(selectedVM.getId(), "Take Snapshot",
+ "Snapshot \"" + name + "\" créé", selectedLab.getTitle());
+ },
+ 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;
+ }
+
+ final VirtualMachine vmToRestore = selectedVM;
+ final String labTitle = selectedLab != null ? selectedLab.getTitle() : "";
+
+ onRestoreSnapshotClicked(vmToRestore, snapshotName,
+ () -> {
+ // Après restauration, la VM est éteinte
+ vmToRestore.setStatus(VMStatus.POWERED_OFF);
+ persisterStatutVM(vmToRestore);
+ updateControlButtons(VMStatus.POWERED_OFF);
+ refreshVmTable(getVMsForLab(selectedLab.getId()));
+ appendJournalLine("Snapshot \"" + snapshotName + "\" restauré");
+ persistAuditEntry(vmToRestore.getId(), "Restore Snapshot",
+ "Snapshot \"" + snapshotName + "\" restauré", labTitle);
+ },
+ errorMsg -> {
+ // Fallback: la restauration peut échouer si la session VBox est encore
+ // verrouillée mais la VM est en fait dans l'état attendu
+ executor.submit(() -> {
+ try {
+ VMStatus live = hypervisor.getStatus(vmToRestore.getUuid());
+ if (live == VMStatus.POWERED_OFF || live == VMStatus.SAVED) {
+ javafx.application.Platform.runLater(() -> {
+ vmToRestore.setStatus(live);
+ persisterStatutVM(vmToRestore);
+ updateControlButtons(live);
+ refreshVmTable(getVMsForLab(selectedLab.getId()));
+ appendJournalLine("Snapshot restauré (état: " + live.name() + ")");
+ });
+ return;
+ }
+ } catch (Exception ignored) {
+ }
+ javafx.application.Platform.runLater(
+ () -> showError("Erreur restauration", errorMsg));
+ });
+ });
+ });
+ }
+
+ @FXML
+ private void handleAddJournalEntry() {
+ String text = journalInputField.getText();
+ if (text == null || text.isBlank()) {
+ return;
+ }
+
+ appendJournalLine(text);
+ journalInputField.clear();
+
+ if (selectedVM != null) {
+ try {
+ JournalEntry entry = new JournalEntry();
+ entry.setContent(text);
+ entry.setTimestamp(LocalDateTime.now());
+
+ 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 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);
+
+ // Charger les snapshots depuis la DB en premier pour que les stats soient
+ // correctes
+ for (VirtualMachine vm : vms) {
+ if (vm.getSnapshots().isEmpty() && vm.getId() != 0) {
+ databaseManager.getSnapshotsForVM(vm.getId()).forEach(vm::addSnapshot);
+ }
+ }
+
+ 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, "");
+
+ row.setOnMouseClicked(e -> onVmRowClicked(vm));
+ vmListContainer.getChildren().add(row);
+ }
+ }
+
+ private void onLabCardClicked(Lab lab) {
+ // Double-clic sur le labo déjà sélectionné → désélection (retour à l'état
+ // neutre)
+ if (!viewingOrphans && selectedLab != null && selectedLab.getId() == lab.getId()) {
+ selectedLab = null;
+ selectedVM = null;
+ viewingOrphans = false;
+ labTitleLabel.setText("Sélectionnez un laboratoire");
+ btnExportPdf.setVisible(false);
+ btnExportPdf.setManaged(false);
+ statsBar.setVisible(false);
+ statsBar.setManaged(false);
+ vmTableHeader.setVisible(false);
+ vmTableHeader.setManaged(false);
+ vmListContainer.getChildren().clear();
+ 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);
+ btnAssignLab.setVisible(false);
+ btnAssignLab.setManaged(false);
+ refreshLabList();
+ return;
+ }
+
+ this.selectedLab = lab;
+ this.selectedVM = null;
+ this.viewingOrphans = false;
+
+ labTitleLabel.setText(lab.getTitle());
+
+ btnExportPdf.setVisible(true);
+ btnExportPdf.setManaged(true);
+ statsBar.setVisible(true);
+ statsBar.setManaged(true);
+ vmTableHeader.setVisible(true);
+ vmTableHeader.setManaged(true);
+
+ 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);
+ btnAssignLab.setVisible(false);
+ btnAssignLab.setManaged(false);
+
+ refreshLabList();
+ refreshVmTable(getVMsForLab(lab.getId()));
+ }
+
+ private void onOrphanCardClicked(List orphans) {
+ this.selectedLab = null;
+ this.selectedVM = null;
+ this.viewingOrphans = true;
+
+ labTitleLabel.setText("VMs sans labo");
+
+ btnExportPdf.setVisible(false);
+ btnExportPdf.setManaged(false);
+ statsBar.setVisible(true);
+ statsBar.setManaged(true);
+ vmTableHeader.setVisible(true);
+ vmTableHeader.setManaged(true);
+
+ 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);
+ btnAssignLab.setVisible(false);
+ btnAssignLab.setManaged(false);
+
+ refreshLabList();
+ refreshVmTable(orphans);
+ }
+
+ private void onVmRowClicked(VirtualMachine vm) {
+ this.selectedVM = vm;
+
+ selectedVmNameLabel.setText(vm.getName());
+
+ controlsPlaceholder.setVisible(false);
+ controlsPlaceholder.setManaged(false);
+
+ updateControlButtons(vm.getStatus());
+
+ // Interroger l'hyperviseur en arrière-plan pour synchroniser l'état réel
+ executor.submit(() -> {
+ try {
+ VMStatus liveStatus = hypervisor.getStatus(vm.getUuid());
+ if (liveStatus != null && liveStatus != VMStatus.UNKNOWN) {
+ javafx.application.Platform.runLater(() -> {
+ if (selectedVM == vm) {
+ vm.setStatus(liveStatus);
+ updateControlButtons(liveStatus);
+ }
+ });
+ }
+ } catch (Exception ignored) {
+ }
+ });
+ }
+
+ private void updateControlButtons(VMStatus status) {
+ boolean isRunning = status == VMStatus.RUNNING;
+ boolean isStopped = status == VMStatus.POWERED_OFF || status == VMStatus.SAVED;
+ boolean isUnknown = status == VMStatus.UNKNOWN || status == null;
+
+ btnStart.setVisible(isStopped || isUnknown);
+ btnStart.setManaged(isStopped || isUnknown);
+ btnStop.setVisible(isRunning || isUnknown);
+ btnStop.setManaged(isRunning || isUnknown);
+ btnSaveState.setVisible(isRunning);
+ btnSaveState.setManaged(isRunning);
+ btnTakeSnapshot.setVisible(true);
+ btnTakeSnapshot.setManaged(true);
+
+ // Le bouton d'assignation change de texte selon le contexte
+ if (viewingOrphans) {
+ btnAssignLab.setText("🔗 Assigner au labo");
+ } else {
+ btnAssignLab.setText("🔗 Changer de labo / Détacher");
+ }
+ btnAssignLab.setVisible(true);
+ btnAssignLab.setManaged(true);
}
/**
- * Retourne les machines virtuelles non rattachees a un laboratoire.
- *
- * @return liste des machines virtuelles orphelines
+ * Ouvre un dialog de sélection de laboratoire pour rattacher la VM
+ * sélectionnée à un labo existant, ou la détacher (orpheline).
*/
- public List getOrphanVMs() {
- return databaseManager.getOrphanVMs();
+ @FXML
+ private void handleAssignLabToVM() {
+ if (selectedVM == null) {
+ return;
+ }
+
+ List labs = databaseManager.getAllLabs();
+ if (labs.isEmpty()) {
+ showError("Aucun labo disponible",
+ "Créez d'abord un laboratoire avant d'assigner une VM.");
+ return;
+ }
+
+ // Options : tous les labs + option « Détacher (orpheline) »
+ javafx.scene.control.ChoiceDialog dialog = new javafx.scene.control.ChoiceDialog<>();
+ dialog.setTitle("Assigner au labo");
+ dialog.setHeaderText("VM : " + selectedVM.getName());
+ dialog.setContentText("Choisissez le laboratoire de destination :");
+
+ for (Lab lab : labs) {
+ dialog.getItems().add(lab.getId() + " — " + lab.getTitle());
+ }
+ dialog.getItems().add("— Détacher (orpheline)");
+ dialog.setSelectedItem(dialog.getItems().get(0));
+
+ dialog.showAndWait().ifPresent(choice -> {
+ if (choice == null) {
+ return;
+ }
+
+ Integer targetLabId = null;
+ String targetLabName = null;
+
+ if (!choice.startsWith("—")) {
+ // Extraire l'ID depuis le format "ID — Titre"
+ try {
+ int sepIdx = choice.indexOf(" — ");
+ int labId = Integer.parseInt(choice.substring(0, sepIdx).trim());
+ for (Lab lab : labs) {
+ if (lab.getId() == labId) {
+ targetLabId = labId;
+ targetLabName = lab.getTitle();
+ break;
+ }
+ }
+ } catch (NumberFormatException ignored) {
+ return;
+ }
+ }
+
+ final Integer finalLabId = targetLabId;
+ final String finalLabName = targetLabName;
+
+ try {
+ databaseManager.saveVirtualMachine(selectedVM, finalLabId);
+
+ // Rafraîchir l'affichage selon le contexte actif
+ if (viewingOrphans) {
+ refreshVmTable(databaseManager.getOrphanVMs());
+ } else if (selectedLab != null) {
+ refreshVmTable(databaseManager.getVMsForLab(selectedLab.getId()));
+ }
+ refreshLabList();
+
+ String msg = finalLabName != null
+ ? "VM \"" + selectedVM.getName() + "\" assignée au labo \"" + finalLabName + "\""
+ : "VM \"" + selectedVM.getName() + "\" détachée (orpheline)";
+ appendJournalLine(msg);
+ persistAuditEntry(selectedVM.getId(), "Assign Lab", msg,
+ finalLabName != null ? finalLabName : null);
+
+ } catch (Exception e) {
+ showError("Erreur assignation", e.getMessage());
+ }
+ });
+ }
+
+ 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();
}
/**
- * Persiste la configuration mise à jour dans la base et dans l'appel.
+ * Trace une action dans la zone d'audit visuelle ET la persiste en base
+ * via {@link DatabaseManager#addAuditEntry(AuditEntry)}.
*
- * @param newConfig nouvelle configuration applicative
+ * @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}
*/
- public void onConfigUpdated(AppConfig newConfig) throws HypervisorException {
+ 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 {
- hypervisor.applyConfig(newConfig);
- } catch (HypervisorException e) {
- throw new RuntimeException(e);
+ databaseManager.addAuditEntry(entry);
+ } catch (Exception e) {
+ appendJournalLine("⚠️ Erreur persistance audit : " + e.getMessage());
}
- databaseManager.saveConfig(newConfig);
}
-}
+}
\ 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/data/DatabaseManager.java b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java
index 833413f..ae19aed 100644
--- a/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java
+++ b/src/main/java/tg/cyberlabmanager/data/DatabaseManager.java
@@ -5,172 +5,545 @@
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;
/**
- * Service d'acces aux donnees de l'application.
+ * Service d'accès aux données de l'application.
*
- *
Il masque la persistance SQLite derriere des operations de haut niveau sur
- * les entites metier.
+ *
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.