fisrt
This commit is contained in:
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IPWatch - Scanner Réseau</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "ipwatch-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.15",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.3",
|
||||
"vite": "^5.0.11",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"postcss": "^8.4.33"
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
48
frontend/src/App.vue
Normal file
48
frontend/src/App.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="h-screen flex flex-col bg-monokai-bg">
|
||||
<!-- Header -->
|
||||
<AppHeader />
|
||||
|
||||
<!-- Layout 3 colonnes selon consigne-design_webui.md -->
|
||||
<div class="flex-1 flex overflow-hidden">
|
||||
<!-- Colonne gauche: Détails IP -->
|
||||
<div class="w-80 flex-shrink-0">
|
||||
<IPDetails />
|
||||
</div>
|
||||
|
||||
<!-- Colonne centrale: Grille d'IP organisée en arbre -->
|
||||
<div class="flex-1">
|
||||
<IPGridTree />
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite: Nouvelles détections -->
|
||||
<div class="w-80 flex-shrink-0">
|
||||
<NewDetections />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
import AppHeader from '@/components/AppHeader.vue'
|
||||
import IPDetails from '@/components/IPDetails.vue'
|
||||
import IPGridTree from '@/components/IPGridTree.vue'
|
||||
import NewDetections from '@/components/NewDetections.vue'
|
||||
|
||||
const ipStore = useIPStore()
|
||||
|
||||
onMounted(async () => {
|
||||
// Charger les données initiales
|
||||
await ipStore.fetchIPs()
|
||||
|
||||
// Connecter WebSocket
|
||||
ipStore.connectWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// Déconnecter WebSocket
|
||||
ipStore.disconnectWebSocket()
|
||||
})
|
||||
</script>
|
||||
147
frontend/src/assets/main.css
Normal file
147
frontend/src/assets/main.css
Normal file
@@ -0,0 +1,147 @@
|
||||
/* Styles principaux IPWatch - Thème Monokai */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Variables CSS Monokai */
|
||||
:root {
|
||||
--monokai-bg: #272822;
|
||||
--monokai-text: #F8F8F2;
|
||||
--monokai-comment: #75715E;
|
||||
--monokai-green: #A6E22E;
|
||||
--monokai-pink: #F92672;
|
||||
--monokai-cyan: #66D9EF;
|
||||
--monokai-purple: #AE81FF;
|
||||
--monokai-yellow: #E6DB74;
|
||||
--monokai-orange: #FD971F;
|
||||
}
|
||||
|
||||
/* Base */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--monokai-bg);
|
||||
color: var(--monokai-text);
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
/* Animation halo ping */
|
||||
@keyframes ping-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 217, 239, 0.7);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 20px 10px rgba(102, 217, 239, 0.3);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(102, 217, 239, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.ping-animation {
|
||||
animation: ping-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Cases IP compactes - Version minimale */
|
||||
.ip-cell-compact {
|
||||
@apply rounded cursor-pointer transition-all duration-200 relative;
|
||||
border: 2px solid;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Cases IP - États selon guidelines-css.md */
|
||||
.ip-cell {
|
||||
@apply rounded-lg p-3 cursor-pointer transition-all duration-200;
|
||||
border: 2px solid;
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
/* IP libre */
|
||||
.ip-cell.free,
|
||||
.ip-cell-compact.free {
|
||||
background-color: rgba(117, 113, 94, 0.2);
|
||||
border-color: var(--monokai-comment);
|
||||
color: var(--monokai-comment);
|
||||
}
|
||||
|
||||
/* IP en ligne + connue (vert) */
|
||||
.ip-cell.online-known,
|
||||
.ip-cell-compact.online-known {
|
||||
background-color: rgba(166, 226, 46, 0.15);
|
||||
border-color: var(--monokai-green);
|
||||
border-style: solid;
|
||||
color: var(--monokai-text);
|
||||
}
|
||||
|
||||
.ip-cell.online-known:hover,
|
||||
.ip-cell-compact.online-known:hover {
|
||||
background-color: rgba(166, 226, 46, 0.25);
|
||||
}
|
||||
|
||||
/* IP en ligne + inconnue (cyan) */
|
||||
.ip-cell.online-unknown,
|
||||
.ip-cell-compact.online-unknown {
|
||||
background-color: rgba(102, 217, 239, 0.15);
|
||||
border-color: var(--monokai-cyan);
|
||||
border-style: solid;
|
||||
color: var(--monokai-text);
|
||||
}
|
||||
|
||||
.ip-cell.online-unknown:hover,
|
||||
.ip-cell-compact.online-unknown:hover {
|
||||
background-color: rgba(102, 217, 239, 0.25);
|
||||
}
|
||||
|
||||
/* IP hors ligne + connue (rose) */
|
||||
.ip-cell.offline-known,
|
||||
.ip-cell-compact.offline-known {
|
||||
background-color: rgba(249, 38, 114, 0.1);
|
||||
border-color: var(--monokai-pink);
|
||||
border-style: dashed;
|
||||
color: var(--monokai-text);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* IP hors ligne + inconnue (violet) */
|
||||
.ip-cell.offline-unknown,
|
||||
.ip-cell-compact.offline-unknown {
|
||||
background-color: rgba(174, 129, 255, 0.1);
|
||||
border-color: var(--monokai-purple);
|
||||
border-style: dashed;
|
||||
color: var(--monokai-text);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Sélection */
|
||||
.ip-cell.selected {
|
||||
box-shadow: 0 0 20px rgba(230, 219, 116, 0.5);
|
||||
border-color: var(--monokai-yellow);
|
||||
}
|
||||
|
||||
/* Scrollbar custom Monokai */
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #1e1f1c;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--monokai-comment);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--monokai-cyan);
|
||||
}
|
||||
68
frontend/src/components/AppHeader.vue
Normal file
68
frontend/src/components/AppHeader.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<header class="bg-monokai-bg border-b-2 border-monokai-comment p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo et titre -->
|
||||
<div class="flex items-center gap-4">
|
||||
<h1 class="text-3xl font-bold text-monokai-green">IPWatch</h1>
|
||||
<span class="text-monokai-comment">Scanner Réseau</span>
|
||||
</div>
|
||||
|
||||
<!-- Stats et contrôles -->
|
||||
<div class="flex items-center gap-6">
|
||||
<!-- Statistiques -->
|
||||
<div class="flex gap-4 text-sm">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-monokai-comment">Total:</span>
|
||||
<span class="text-monokai-text font-bold">{{ stats.total }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-monokai-green"></span>
|
||||
<span class="text-monokai-text">{{ stats.online }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full bg-monokai-pink opacity-50"></span>
|
||||
<span class="text-monokai-text">{{ stats.offline }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton scan -->
|
||||
<button
|
||||
@click="triggerScan"
|
||||
:disabled="loading"
|
||||
class="px-4 py-2 rounded bg-monokai-cyan text-monokai-bg font-bold hover:bg-monokai-green transition-colors disabled:opacity-50"
|
||||
>
|
||||
{{ loading ? 'Scan en cours...' : 'Lancer Scan' }}
|
||||
</button>
|
||||
|
||||
<!-- Indicateur WebSocket -->
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
:class="[
|
||||
'w-2 h-2 rounded-full',
|
||||
wsConnected ? 'bg-monokai-green' : 'bg-monokai-pink'
|
||||
]"
|
||||
></div>
|
||||
<span class="text-sm text-monokai-comment">
|
||||
{{ wsConnected ? 'Connecté' : 'Déconnecté' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
|
||||
const ipStore = useIPStore()
|
||||
const { stats, loading, wsConnected } = storeToRefs(ipStore)
|
||||
|
||||
async function triggerScan() {
|
||||
try {
|
||||
await ipStore.startScan()
|
||||
} catch (err) {
|
||||
console.error('Erreur lancement scan:', err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
87
frontend/src/components/IPCell.vue
Normal file
87
frontend/src/components/IPCell.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div
|
||||
:class="[
|
||||
'ip-cell-compact',
|
||||
cellClass,
|
||||
{ 'selected': isSelected },
|
||||
{ 'ping-animation': isPinging }
|
||||
]"
|
||||
@click="selectThisIP"
|
||||
:title="getTooltip"
|
||||
>
|
||||
<!-- Afficher seulement le dernier octet -->
|
||||
<div class="font-mono font-bold text-2xl">
|
||||
{{ lastOctet }}
|
||||
</div>
|
||||
|
||||
<!-- Nom très court si connu -->
|
||||
<div v-if="ip.name" class="text-xs opacity-75 truncate mt-1">
|
||||
{{ ip.name }}
|
||||
</div>
|
||||
|
||||
<!-- Indicateur ports ouverts (petit badge) -->
|
||||
<div v-if="ip.open_ports && ip.open_ports.length > 0"
|
||||
class="absolute top-1 right-1 w-2 h-2 rounded-full bg-monokai-cyan">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
|
||||
const props = defineProps({
|
||||
ip: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isPinging: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const ipStore = useIPStore()
|
||||
const { selectedIP } = storeToRefs(ipStore)
|
||||
|
||||
const isSelected = computed(() => {
|
||||
return selectedIP.value?.ip === props.ip.ip
|
||||
})
|
||||
|
||||
// Extraire le dernier octet de l'IP
|
||||
const lastOctet = computed(() => {
|
||||
const parts = props.ip.ip.split('.')
|
||||
return parts[parts.length - 1]
|
||||
})
|
||||
|
||||
// Tooltip avec infos complètes
|
||||
const getTooltip = computed(() => {
|
||||
let tooltip = `${props.ip.ip}`
|
||||
if (props.ip.name) tooltip += ` - ${props.ip.name}`
|
||||
if (props.ip.hostname) tooltip += `\nHostname: ${props.ip.hostname}`
|
||||
if (props.ip.mac) tooltip += `\nMAC: ${props.ip.mac}`
|
||||
if (props.ip.vendor) tooltip += ` (${props.ip.vendor})`
|
||||
if (props.ip.open_ports && props.ip.open_ports.length > 0) {
|
||||
tooltip += `\nPorts: ${props.ip.open_ports.join(', ')}`
|
||||
}
|
||||
return tooltip
|
||||
})
|
||||
|
||||
const cellClass = computed(() => {
|
||||
// Déterminer la classe selon l'état (guidelines-css.md)
|
||||
if (!props.ip.last_status) {
|
||||
return 'free'
|
||||
}
|
||||
|
||||
if (props.ip.last_status === 'online') {
|
||||
return props.ip.known ? 'online-known' : 'online-unknown'
|
||||
} else {
|
||||
return props.ip.known ? 'offline-known' : 'offline-unknown'
|
||||
}
|
||||
})
|
||||
|
||||
function selectThisIP() {
|
||||
ipStore.selectIP(props.ip)
|
||||
}
|
||||
</script>
|
||||
207
frontend/src/components/IPDetails.vue
Normal file
207
frontend/src/components/IPDetails.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-monokai-bg border-r border-monokai-comment">
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-monokai-comment">
|
||||
<h2 class="text-xl font-bold text-monokai-cyan">Détails IP</h2>
|
||||
</div>
|
||||
|
||||
<!-- Contenu -->
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div v-if="selectedIP" class="space-y-4">
|
||||
<!-- Adresse IP -->
|
||||
<div>
|
||||
<div class="text-sm text-monokai-comment mb-1">Adresse IP</div>
|
||||
<div class="text-2xl font-mono font-bold text-monokai-green">
|
||||
{{ selectedIP.ip }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- État -->
|
||||
<div>
|
||||
<div class="text-sm text-monokai-comment mb-1">État</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
:class="[
|
||||
'w-3 h-3 rounded-full',
|
||||
selectedIP.last_status === 'online' ? 'bg-monokai-green' : 'bg-monokai-pink'
|
||||
]"
|
||||
></div>
|
||||
<span class="text-monokai-text capitalize">
|
||||
{{ selectedIP.last_status || 'Inconnu' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire d'édition -->
|
||||
<div class="space-y-3 pt-4 border-t border-monokai-comment">
|
||||
<!-- Nom -->
|
||||
<div>
|
||||
<label class="text-sm text-monokai-comment mb-1 block">Nom</label>
|
||||
<input
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
||||
placeholder="Ex: Serveur Principal"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Connue -->
|
||||
<div>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
v-model="formData.known"
|
||||
type="checkbox"
|
||||
class="form-checkbox"
|
||||
/>
|
||||
<span class="text-sm text-monokai-text">IP connue</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Localisation -->
|
||||
<div>
|
||||
<label class="text-sm text-monokai-comment mb-1 block">Localisation</label>
|
||||
<input
|
||||
v-model="formData.location"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
||||
placeholder="Ex: Bureau"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Type d'hôte -->
|
||||
<div>
|
||||
<label class="text-sm text-monokai-comment mb-1 block">Type d'hôte</label>
|
||||
<input
|
||||
v-model="formData.host"
|
||||
type="text"
|
||||
class="w-full px-3 py-2 bg-monokai-bg border border-monokai-comment rounded text-monokai-text"
|
||||
placeholder="Ex: PC, Serveur, Imprimante"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Boutons -->
|
||||
<div class="flex gap-2 pt-2">
|
||||
<button
|
||||
@click="saveChanges"
|
||||
class="px-4 py-2 bg-monokai-green text-monokai-bg rounded font-bold hover:bg-monokai-cyan transition-colors"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
<button
|
||||
@click="resetForm"
|
||||
class="px-4 py-2 bg-monokai-comment text-monokai-bg rounded hover:bg-monokai-text transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Informations réseau -->
|
||||
<div class="pt-4 border-t border-monokai-comment space-y-2">
|
||||
<h3 class="text-monokai-cyan font-bold mb-2">Informations réseau</h3>
|
||||
|
||||
<div v-if="selectedIP.mac">
|
||||
<div class="text-sm text-monokai-comment">MAC</div>
|
||||
<div class="text-monokai-text font-mono">{{ selectedIP.mac }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedIP.vendor">
|
||||
<div class="text-sm text-monokai-comment">Fabricant</div>
|
||||
<div class="text-monokai-text">{{ selectedIP.vendor }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedIP.hostname">
|
||||
<div class="text-sm text-monokai-comment">Hostname</div>
|
||||
<div class="text-monokai-text font-mono">{{ selectedIP.hostname }}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedIP.open_ports && selectedIP.open_ports.length > 0">
|
||||
<div class="text-sm text-monokai-comment">Ports ouverts</div>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<span
|
||||
v-for="port in selectedIP.open_ports"
|
||||
:key="port"
|
||||
class="px-2 py-1 bg-monokai-cyan/20 text-monokai-cyan rounded text-xs font-mono"
|
||||
>
|
||||
{{ port }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timestamps -->
|
||||
<div class="pt-4 border-t border-monokai-comment space-y-2 text-sm">
|
||||
<div v-if="selectedIP.first_seen">
|
||||
<span class="text-monokai-comment">Première vue:</span>
|
||||
<span class="text-monokai-text ml-2">{{ formatDate(selectedIP.first_seen) }}</span>
|
||||
</div>
|
||||
<div v-if="selectedIP.last_seen">
|
||||
<span class="text-monokai-comment">Dernière vue:</span>
|
||||
<span class="text-monokai-text ml-2">{{ formatDate(selectedIP.last_seen) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder si rien sélectionné -->
|
||||
<div v-else class="text-center text-monokai-comment mt-10">
|
||||
<p>Sélectionnez une IP pour voir les détails</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
|
||||
const ipStore = useIPStore()
|
||||
const { selectedIP } = storeToRefs(ipStore)
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
known: false,
|
||||
location: '',
|
||||
host: ''
|
||||
})
|
||||
|
||||
// Mettre à jour le formulaire quand l'IP change
|
||||
watch(selectedIP, (newIP) => {
|
||||
if (newIP) {
|
||||
formData.value = {
|
||||
name: newIP.name || '',
|
||||
known: newIP.known || false,
|
||||
location: newIP.location || '',
|
||||
host: newIP.host || ''
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function resetForm() {
|
||||
if (selectedIP.value) {
|
||||
formData.value = {
|
||||
name: selectedIP.value.name || '',
|
||||
known: selectedIP.value.known || false,
|
||||
location: selectedIP.value.location || '',
|
||||
host: selectedIP.value.host || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
if (!selectedIP.value) return
|
||||
|
||||
try {
|
||||
await ipStore.updateIP(selectedIP.value.ip, formData.value)
|
||||
console.log('IP mise à jour')
|
||||
} catch (err) {
|
||||
console.error('Erreur mise à jour IP:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'N/A'
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('fr-FR')
|
||||
}
|
||||
</script>
|
||||
79
frontend/src/components/IPGrid.vue
Normal file
79
frontend/src/components/IPGrid.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Filtres -->
|
||||
<div class="bg-monokai-bg border-b border-monokai-comment p-3 flex gap-4 flex-wrap">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showOnline" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">En ligne</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showOffline" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Hors ligne</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showKnown" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Connues</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showUnknown" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Inconnues</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showFree" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Libres</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Grille d'IPs -->
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<IPCell
|
||||
v-for="ip in filteredIPs"
|
||||
:key="ip.ip"
|
||||
:ip="ip"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Message si vide -->
|
||||
<div v-if="filteredIPs.length === 0" class="text-center text-monokai-comment mt-10">
|
||||
<p>Aucune IP à afficher</p>
|
||||
<p class="text-sm mt-2">Ajustez les filtres ou lancez un scan</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Légende -->
|
||||
<div class="bg-monokai-bg border-t border-monokai-comment p-3">
|
||||
<div class="flex gap-6 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-monokai-green bg-monokai-green/15"></div>
|
||||
<span class="text-monokai-text">En ligne (connue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-monokai-cyan bg-monokai-cyan/15"></div>
|
||||
<span class="text-monokai-text">En ligne (inconnue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-dashed border-monokai-pink bg-monokai-pink/10 opacity-50"></div>
|
||||
<span class="text-monokai-text">Hors ligne (connue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-dashed border-monokai-purple bg-monokai-purple/10 opacity-50"></div>
|
||||
<span class="text-monokai-text">Hors ligne (inconnue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-monokai-comment bg-monokai-comment/20"></div>
|
||||
<span class="text-monokai-text">Libre</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
import IPCell from './IPCell.vue'
|
||||
|
||||
const ipStore = useIPStore()
|
||||
const { filteredIPs, filters } = storeToRefs(ipStore)
|
||||
</script>
|
||||
129
frontend/src/components/IPGridTree.vue
Normal file
129
frontend/src/components/IPGridTree.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<!-- Filtres -->
|
||||
<div class="bg-monokai-bg border-b border-monokai-comment p-3 flex gap-4 flex-wrap">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showOnline" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">En ligne</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showOffline" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Hors ligne</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showKnown" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Connues</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showUnknown" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Inconnues</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" v-model="filters.showFree" class="form-checkbox" />
|
||||
<span class="text-sm text-monokai-text">Libres</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Organisation en arbre par sous-réseaux -->
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div v-for="subnet in organizedSubnets" :key="subnet.name" class="mb-6">
|
||||
<!-- Header du sous-réseau (style tree) -->
|
||||
<div class="flex items-center gap-2 mb-3 text-monokai-cyan border-l-4 border-monokai-cyan pl-3">
|
||||
<span class="font-bold text-lg">{{ subnet.name }}</span>
|
||||
<span class="text-sm text-monokai-comment">{{ subnet.cidr }}</span>
|
||||
<span class="text-sm text-monokai-comment ml-auto">
|
||||
{{ subnet.ips.length }} IPs
|
||||
({{ subnet.stats.online }} en ligne)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Grille compacte des IPs du sous-réseau -->
|
||||
<div class="flex flex-wrap gap-2 pl-8">
|
||||
<IPCell
|
||||
v-for="ip in subnet.ips"
|
||||
:key="ip.ip"
|
||||
:ip="ip"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message si vide -->
|
||||
<div v-if="organizedSubnets.length === 0" class="text-center text-monokai-comment mt-10">
|
||||
<p>Aucune IP à afficher</p>
|
||||
<p class="text-sm mt-2">Ajustez les filtres ou lancez un scan</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Légende -->
|
||||
<div class="bg-monokai-bg border-t border-monokai-comment p-3">
|
||||
<div class="flex gap-6 text-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-monokai-green bg-monokai-green/15"></div>
|
||||
<span class="text-monokai-text">En ligne (connue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-monokai-cyan bg-monokai-cyan/15"></div>
|
||||
<span class="text-monokai-text">En ligne (inconnue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-dashed border-monokai-pink bg-monokai-pink/10 opacity-50"></div>
|
||||
<span class="text-monokai-text">Hors ligne (connue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-dashed border-monokai-purple bg-monokai-purple/10 opacity-50"></div>
|
||||
<span class="text-monokai-text">Hors ligne (inconnue)</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-4 h-4 rounded border-2 border-monokai-comment bg-monokai-comment/20"></div>
|
||||
<span class="text-monokai-text">Libre</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
import IPCell from './IPCell.vue'
|
||||
|
||||
const ipStore = useIPStore()
|
||||
const { filteredIPs, filters } = storeToRefs(ipStore)
|
||||
|
||||
// Définition des sous-réseaux (devrait venir de la config mais en dur pour l'instant)
|
||||
const subnets = [
|
||||
{ name: 'static_vm', cidr: '10.0.0.0/24', description: 'Machines virtuelles statiques' },
|
||||
{ name: 'dhcp', cidr: '10.0.1.0/24', description: 'DHCP' },
|
||||
{ name: 'iot', cidr: '10.0.2.0/24', description: 'IoT' }
|
||||
]
|
||||
|
||||
// Organiser les IPs par sous-réseau
|
||||
const organizedSubnets = computed(() => {
|
||||
return subnets.map(subnet => {
|
||||
// Extraire le préfixe du sous-réseau (ex: "10.0.0" pour 10.0.0.0/24)
|
||||
const [oct1, oct2, oct3] = subnet.cidr.split('/')[0].split('.')
|
||||
const prefix = `${oct1}.${oct2}.${oct3}`
|
||||
|
||||
// Filtrer les IPs qui appartiennent à ce sous-réseau
|
||||
const subnetIPs = filteredIPs.value.filter(ip => {
|
||||
return ip.ip.startsWith(prefix + '.')
|
||||
})
|
||||
|
||||
// Calculer les stats
|
||||
const stats = {
|
||||
total: subnetIPs.length,
|
||||
online: subnetIPs.filter(ip => ip.last_status === 'online').length,
|
||||
offline: subnetIPs.filter(ip => ip.last_status === 'offline').length
|
||||
}
|
||||
|
||||
return {
|
||||
name: subnet.name,
|
||||
cidr: subnet.cidr,
|
||||
description: subnet.description,
|
||||
ips: subnetIPs,
|
||||
stats
|
||||
}
|
||||
}).filter(subnet => subnet.ips.length > 0) // Ne montrer que les sous-réseaux avec des IPs
|
||||
})
|
||||
</script>
|
||||
119
frontend/src/components/NewDetections.vue
Normal file
119
frontend/src/components/NewDetections.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-monokai-bg border-l border-monokai-comment">
|
||||
<!-- Header -->
|
||||
<div class="p-4 border-b border-monokai-comment">
|
||||
<h2 class="text-xl font-bold text-monokai-pink">Nouvelles Détections</h2>
|
||||
</div>
|
||||
|
||||
<!-- Liste -->
|
||||
<div class="flex-1 overflow-auto p-4">
|
||||
<div v-if="newIPs.length > 0" class="space-y-3">
|
||||
<div
|
||||
v-for="ip in newIPs"
|
||||
:key="ip.ip"
|
||||
@click="selectIP(ip)"
|
||||
class="p-3 rounded border-2 border-monokai-pink bg-monokai-pink/10 cursor-pointer hover:bg-monokai-pink/20 transition-colors"
|
||||
>
|
||||
<!-- IP -->
|
||||
<div class="font-mono font-bold text-monokai-text">
|
||||
{{ ip.ip }}
|
||||
</div>
|
||||
|
||||
<!-- État -->
|
||||
<div class="text-sm mt-1">
|
||||
<span
|
||||
:class="[
|
||||
'px-2 py-1 rounded text-xs',
|
||||
ip.last_status === 'online'
|
||||
? 'bg-monokai-green/20 text-monokai-green'
|
||||
: 'bg-monokai-comment/20 text-monokai-comment'
|
||||
]"
|
||||
>
|
||||
{{ ip.last_status || 'Inconnu' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="!ip.known"
|
||||
class="ml-2 px-2 py-1 rounded text-xs bg-monokai-purple/20 text-monokai-purple"
|
||||
>
|
||||
Inconnue
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- MAC et Vendor -->
|
||||
<div v-if="ip.mac" class="text-xs text-monokai-comment mt-2 font-mono">
|
||||
{{ ip.mac }}
|
||||
<span v-if="ip.vendor" class="ml-1">({{ ip.vendor }})</span>
|
||||
</div>
|
||||
|
||||
<!-- Timestamp -->
|
||||
<div class="text-xs text-monokai-comment mt-2">
|
||||
{{ formatTime(ip.first_seen) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Placeholder -->
|
||||
<div v-else class="text-center text-monokai-comment mt-10">
|
||||
<p>Aucune nouvelle IP détectée</p>
|
||||
<p class="text-sm mt-2">Les nouvelles IPs apparaîtront ici</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useIPStore } from '@/stores/ipStore'
|
||||
|
||||
const ipStore = useIPStore()
|
||||
const { ips } = storeToRefs(ipStore)
|
||||
|
||||
// IPs nouvellement détectées (dans les dernières 24h)
|
||||
const newIPs = computed(() => {
|
||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||
|
||||
return ips.value
|
||||
.filter(ip => {
|
||||
if (!ip.first_seen) return false
|
||||
const firstSeen = new Date(ip.first_seen)
|
||||
return firstSeen > oneDayAgo
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.first_seen)
|
||||
const dateB = new Date(b.first_seen)
|
||||
return dateB - dateA // Plus récent en premier
|
||||
})
|
||||
.slice(0, 20) // Limiter à 20
|
||||
})
|
||||
|
||||
function selectIP(ip) {
|
||||
ipStore.selectIP(ip)
|
||||
}
|
||||
|
||||
function formatTime(dateString) {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
const diff = now - date
|
||||
|
||||
// Moins d'une minute
|
||||
if (diff < 60000) {
|
||||
return 'À l\'instant'
|
||||
}
|
||||
|
||||
// Moins d'une heure
|
||||
if (diff < 3600000) {
|
||||
const minutes = Math.floor(diff / 60000)
|
||||
return `Il y a ${minutes} min`
|
||||
}
|
||||
|
||||
// Moins de 24h
|
||||
if (diff < 86400000) {
|
||||
const hours = Math.floor(diff / 3600000)
|
||||
return `Il y a ${hours}h`
|
||||
}
|
||||
|
||||
return date.toLocaleString('fr-FR')
|
||||
}
|
||||
</script>
|
||||
10
frontend/src/main.js
Normal file
10
frontend/src/main.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import './assets/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.mount('#app')
|
||||
230
frontend/src/stores/ipStore.js
Normal file
230
frontend/src/stores/ipStore.js
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* Store Pinia pour la gestion des IPs
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
export const useIPStore = defineStore('ip', () => {
|
||||
// État
|
||||
const ips = ref([])
|
||||
const selectedIP = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
const stats = ref({
|
||||
total: 0,
|
||||
online: 0,
|
||||
offline: 0,
|
||||
known: 0,
|
||||
unknown: 0
|
||||
})
|
||||
|
||||
// Filtres
|
||||
const filters = ref({
|
||||
showOnline: true,
|
||||
showOffline: true,
|
||||
showKnown: true,
|
||||
showUnknown: true,
|
||||
showFree: true
|
||||
})
|
||||
|
||||
// WebSocket
|
||||
const ws = ref(null)
|
||||
const wsConnected = ref(false)
|
||||
|
||||
// Computed
|
||||
const filteredIPs = computed(() => {
|
||||
return ips.value.filter(ip => {
|
||||
// Filtrer par statut
|
||||
if (ip.last_status === 'online' && !filters.value.showOnline) return false
|
||||
if (ip.last_status === 'offline' && !filters.value.showOffline) return false
|
||||
|
||||
// Filtrer par connu/inconnu
|
||||
if (ip.known && !filters.value.showKnown) return false
|
||||
if (!ip.known && !filters.value.showUnknown) return false
|
||||
|
||||
// Filtrer IP libres (pas de last_status)
|
||||
if (!ip.last_status && !filters.value.showFree) return false
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// Actions
|
||||
async function fetchIPs() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/ips/')
|
||||
ips.value = response.data
|
||||
await fetchStats()
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
console.error('Erreur chargement IPs:', err)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const response = await axios.get('/api/ips/stats/summary')
|
||||
stats.value = response.data
|
||||
} catch (err) {
|
||||
console.error('Erreur chargement stats:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateIP(ipAddress, data) {
|
||||
try {
|
||||
const response = await axios.put(`/api/ips/${ipAddress}`, data)
|
||||
|
||||
// Mettre à jour dans le store
|
||||
const index = ips.value.findIndex(ip => ip.ip === ipAddress)
|
||||
if (index !== -1) {
|
||||
ips.value[index] = response.data
|
||||
}
|
||||
|
||||
if (selectedIP.value?.ip === ipAddress) {
|
||||
selectedIP.value = response.data
|
||||
}
|
||||
|
||||
return response.data
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function getIPHistory(ipAddress, hours = 24) {
|
||||
try {
|
||||
const response = await axios.get(`/api/ips/${ipAddress}/history?hours=${hours}`)
|
||||
return response.data
|
||||
} catch (err) {
|
||||
console.error('Erreur chargement historique:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function startScan() {
|
||||
try {
|
||||
await axios.post('/api/scan/start')
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function selectIP(ip) {
|
||||
selectedIP.value = ip
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
selectedIP.value = null
|
||||
}
|
||||
|
||||
// WebSocket
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}/ws`
|
||||
|
||||
ws.value = new WebSocket(wsUrl)
|
||||
|
||||
ws.value.onopen = () => {
|
||||
console.log('WebSocket connecté')
|
||||
wsConnected.value = true
|
||||
|
||||
// Heartbeat toutes les 30s
|
||||
setInterval(() => {
|
||||
if (ws.value?.readyState === WebSocket.OPEN) {
|
||||
ws.value.send('ping')
|
||||
}
|
||||
}, 30000)
|
||||
}
|
||||
|
||||
ws.value.onmessage = (event) => {
|
||||
try {
|
||||
const message = JSON.parse(event.data)
|
||||
handleWebSocketMessage(message)
|
||||
} catch (err) {
|
||||
console.error('Erreur parsing WebSocket:', err)
|
||||
}
|
||||
}
|
||||
|
||||
ws.value.onerror = (error) => {
|
||||
console.error('Erreur WebSocket:', error)
|
||||
wsConnected.value = false
|
||||
}
|
||||
|
||||
ws.value.onclose = () => {
|
||||
console.log('WebSocket déconnecté')
|
||||
wsConnected.value = false
|
||||
|
||||
// Reconnexion après 5s
|
||||
setTimeout(connectWebSocket, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
function handleWebSocketMessage(message) {
|
||||
console.log('Message WebSocket:', message)
|
||||
|
||||
switch (message.type) {
|
||||
case 'scan_start':
|
||||
// Notification début de scan
|
||||
break
|
||||
|
||||
case 'scan_complete':
|
||||
// Rafraîchir les données après scan
|
||||
fetchIPs()
|
||||
stats.value = message.stats
|
||||
break
|
||||
|
||||
case 'ip_update':
|
||||
// Mise à jour d'une IP
|
||||
const updatedIP = ips.value.find(ip => ip.ip === message.data.ip)
|
||||
if (updatedIP) {
|
||||
Object.assign(updatedIP, message.data)
|
||||
}
|
||||
break
|
||||
|
||||
case 'new_ip':
|
||||
// Nouvelle IP détectée
|
||||
fetchIPs() // Recharger pour être sûr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function disconnectWebSocket() {
|
||||
if (ws.value) {
|
||||
ws.value.close()
|
||||
ws.value = null
|
||||
wsConnected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// État
|
||||
ips,
|
||||
selectedIP,
|
||||
loading,
|
||||
error,
|
||||
stats,
|
||||
filters,
|
||||
wsConnected,
|
||||
|
||||
// Computed
|
||||
filteredIPs,
|
||||
|
||||
// Actions
|
||||
fetchIPs,
|
||||
fetchStats,
|
||||
updateIP,
|
||||
getIPHistory,
|
||||
startScan,
|
||||
selectIP,
|
||||
clearSelection,
|
||||
connectWebSocket,
|
||||
disconnectWebSocket
|
||||
}
|
||||
})
|
||||
26
frontend/tailwind.config.js
Normal file
26
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{vue,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Palette Monokai (guidelines-css.md)
|
||||
monokai: {
|
||||
bg: '#272822',
|
||||
text: '#F8F8F2',
|
||||
comment: '#75715E',
|
||||
green: '#A6E22E',
|
||||
pink: '#F92672',
|
||||
cyan: '#66D9EF',
|
||||
purple: '#AE81FF',
|
||||
yellow: '#E6DB74',
|
||||
orange: '#FD971F',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
25
frontend/vite.config.js
Normal file
25
frontend/vite.config.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true
|
||||
},
|
||||
'/ws': {
|
||||
target: 'ws://localhost:8080',
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user