diff --git a/src/components/ErrorDialog.vue b/src/components/ErrorDialog.vue
index 469d7f36b..380d62157 100644
--- a/src/components/ErrorDialog.vue
+++ b/src/components/ErrorDialog.vue
@@ -56,7 +56,7 @@
Sur NixOS : remplacez nom_du_binaire par
- target/debug/sonar ou le chemin du binaire compilé.
+ target/debug/netscan-ai ou le chemin du binaire compilé.
diff --git a/src/components/NavBar/TopBar.vue b/src/components/NavBar/TopBar.vue
index cc7e10268..cf8988b14 100644
--- a/src/components/NavBar/TopBar.vue
+++ b/src/components/NavBar/TopBar.vue
@@ -7,6 +7,7 @@
+
@@ -32,14 +33,23 @@
-
-
-
-
-
-
-
-
+
+
@@ -75,6 +85,24 @@
+
+
+
@@ -90,7 +118,7 @@
- SONAR
+ NetScan-AI
@@ -125,6 +153,7 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { exit } from '@tauri-apps/plugin-process';
import { info, error } from '@tauri-apps/plugin-log';
import { save } from '@tauri-apps/plugin-dialog';
+import { downloadDir } from '@tauri-apps/api/path';
import { register, unregister } from '@tauri-apps/plugin-global-shortcut';
// when using `"withGlobalTauri": true`, you may use
// const { register } = window.__TAURI__.globalShortcut;
@@ -163,10 +192,17 @@ export default {
showMatrice: true,
shortcuts: [] as string[],
isMaximized: false,
+ isRecording: false,
+ showSaveMenu: false,
+ showExportMenu: false,
+ _closeMenu: null as null | (() => void),
};
},
async mounted() {
appWindow = getCurrentWebviewWindow();
+ const closeMenu = () => { this.showExportMenu = false; this.showSaveMenu = false; };
+ document.addEventListener('click', closeMenu, true);
+ this._closeMenu = closeMenu;
this.isMaximized = await appWindow.isMaximized();
await appWindow.onResized(async () => {
this.isMaximized = await appWindow!.isMaximized();
@@ -204,8 +240,8 @@ export default {
},
async beforeUnmount() {
- // recommandé en dev/hot reload
await this.unbindAllShortcuts();
+ if (this._closeMenu) document.removeEventListener('click', this._closeMenu, true);
},
methods: {
bindShortcut(shortcut: string, handler: () => void) {
@@ -289,9 +325,16 @@ export default {
}
},
async triggerSave() {
- info("trigger save")
+ this.showSaveMenu = false;
this.SaveAsCsv();
-
+ },
+ triggerSavePng() {
+ this.showSaveMenu = false;
+ document.dispatchEvent(new CustomEvent('export-png'));
+ },
+ triggerSaveSvg() {
+ this.showSaveMenu = false;
+ document.dispatchEvent(new CustomEvent('export-svg'));
},
async reset() {
info("reset")
@@ -316,13 +359,52 @@ export default {
info("[TopBar] Bouton IA cliqué");
this.$emit('toggle-ai');
},
+
+ async startPcapRecord() {
+ this.showSaveMenu = false;
+ const path = await save({
+ filters: [{ name: 'PCAP', extensions: ['pcap'] }],
+ title: 'Enregistrer en PCAP',
+ defaultPath: getCurrentDate() + '_capture.pcap',
+ });
+ if (!path) return;
+ await invoke('start_pcap_record', { path }).catch((e: any) => error('start_pcap_record:', e));
+ this.isRecording = true;
+ },
+
+ async stopPcapRecord() {
+ this.showSaveMenu = false;
+ const path = await invoke('stop_pcap_record').catch((e: any) => { error('stop_pcap_record:', e); return null });
+ this.isRecording = false;
+ if (path) info('PCAP enregistré : ' + path);
+ },
+
+ async exportRules(type: 'snort' | 'suricata' | 'iptables') {
+ this.showExportMenu = false;
+ const ext = type === 'iptables' ? 'sh' : 'rules';
+ const path = await save({
+ filters: [{ name: type, extensions: [ext] }],
+ title: `Exporter règles ${type}`,
+ defaultPath: `${getCurrentDate()}_${type}.${ext}`,
+ });
+ if (!path) return;
+ const cmd = type === 'snort' ? 'export_snort_rules'
+ : type === 'suricata' ? 'export_suricata_rules'
+ : 'export_iptables';
+ await invoke(cmd, { path }).catch((e: any) => error(`${cmd}:`, e));
+ info(`Règles ${type} exportées : ${path}`);
+ },
async start() {
- if (this.captureStore.isRunning) {
- return;
- }
- const onEvent = new Channel();
- this.captureStore.setChannel(onEvent); // 🟢 rendre le Channel accessible
+ if (this.captureStore.isRunning) return;
+
+ const dir = await downloadDir().catch(() => '.');
+ const pcapPath = `${dir}/${getCurrentDate()}_capture.pcap`;
+ await invoke('start_pcap_record', { path: pcapPath })
+ .then(() => { this.isRecording = true; info('Enregistrement PCAP : ' + pcapPath); })
+ .catch((e: any) => error('start_pcap_record:', e));
+ const onEvent = new Channel();
+ this.captureStore.setChannel(onEvent);
await invoke('start_capture', { onEvent })
.then((status) => {
const typedStatus = status as { is_running: boolean };
@@ -332,17 +414,22 @@ export default {
.catch(displayCaptureError);
},
async stop() {
- if (!this.captureStore.isRunning) {
- return;
- }
+ if (!this.captureStore.isRunning) return;
+
const onEvent = this.captureStore.getChannel();
- await invoke('stop_capture',{ onEvent })
+ await invoke('stop_capture', { onEvent })
.then((status) => {
const typedStatus = status as { is_running: boolean };
this.captureStore.updateStatus(typedStatus);
info('Capture arrêtée : ' + this.captureStore.isRunning);
})
.catch(displayCaptureError);
+
+ if (this.isRecording) {
+ await invoke('stop_pcap_record')
+ .then((path) => { this.isRecording = false; info('PCAP enregistré : ' + path); })
+ .catch((e: any) => error('stop_pcap_record:', e));
+ }
},
toggleView() {
info('Vue basculée');
@@ -494,6 +581,7 @@ export default {
.logo-btn {
padding: 3px;
margin-right: 4px;
+ position: relative;
}
.logo-img {
@@ -528,4 +616,72 @@ export default {
color: #a0a0e8;
background-color: #35355a;
}
+
+.rec-dot {
+ position: absolute;
+ bottom: 3px;
+ right: 3px;
+ width: 5px;
+ height: 5px;
+ border-radius: 50%;
+ background: #e05555;
+ animation: rec-pulse 1.2s ease-in-out infinite;
+}
+
+@keyframes rec-pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.3; }
+}
+
+/* Export rules dropdown */
+.export-wrap {
+ position: relative;
+}
+
+.export-menu {
+ position: absolute;
+ top: calc(100% + 4px);
+ left: 0;
+ background: #2c2c3a;
+ border: 1px solid #3c3c50;
+ border-radius: 6px;
+ padding: 4px 0;
+ min-width: 120px;
+ z-index: 10000;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
+}
+
+.export-menu button {
+ display: block;
+ width: 100%;
+ padding: 6px 14px;
+ background: transparent;
+ border: none;
+ color: #a0a0b8;
+ font-size: 12px;
+ text-align: left;
+ cursor: pointer;
+ transition: background 0.12s ease, color 0.12s ease;
+}
+
+.export-menu button:hover {
+ background: #383850;
+ color: #d4d4d8;
+}
+
+.export-menu button.record-item {
+ color: #e05555;
+}
+.export-menu button.record-item:hover {
+ background: #3a2020;
+ color: #f07070;
+}
+
+.menu-fade-enter-active, .menu-fade-leave-active {
+ transition: opacity 0.12s ease, transform 0.12s cubic-bezier(0.22, 1, 0.36, 1);
+}
+.menu-fade-enter-from, .menu-fade-leave-to {
+ opacity: 0;
+ transform: translateY(-4px);
+}
\ No newline at end of file
diff --git a/src/components/QuitDialog.vue b/src/components/QuitDialog.vue
index 2d59b368b..22f432fcb 100644
--- a/src/components/QuitDialog.vue
+++ b/src/components/QuitDialog.vue
@@ -12,7 +12,7 @@
-
Quitter Sonar ?
+
Quitter NetScan-AI ?
Les données non sauvegardées seront perdues.
diff --git a/src/components/homeVue/Capture.vue b/src/components/homeVue/Capture.vue
index 2b93830b8..c102f909c 100644
--- a/src/components/homeVue/Capture.vue
+++ b/src/components/homeVue/Capture.vue
@@ -1,6 +1,6 @@
-
+
diff --git a/src/components/homeVue/FromPcap.vue b/src/components/homeVue/FromPcap.vue
index 90cc32e68..a8b776420 100644
--- a/src/components/homeVue/FromPcap.vue
+++ b/src/components/homeVue/FromPcap.vue
@@ -1,7 +1,7 @@
-
+