- {/* 主題切換按鈕 */}
-
setShowOptions(!showOptions)}
- className={`
- w-full rounded-lg p-3 flex items-center justify-between
- transition-all duration-300 text-left
-
- `}
- >
-
-
-
-
-
-
Theme Mode
-
- {currentThemeOption.label}
-
-
-
-
-
-
-
-
-
- {/* 主題選項下拉選單 */}
-
- {showOptions && (
-
- {themeOptions.map((option) => (
- {
- setTheme(option.value)
- setShowOptions(false)
- }}
- className={`
- w-full p-3 flex items-center space-x-3 transition-colors duration-200
- first:rounded-t-lg last:rounded-b-lg
- ${theme === option.value
- ? (actualTheme === 'dark'
- ? 'bg-blue-600 text-white'
- : 'bg-blue-500 text-white'
- )
- : (actualTheme === 'dark'
- ? 'text-gray-300 hover:bg-gray-700'
- : 'text-gray-700 hover:bg-gray-50'
- )
- }
- `}
- >
-
-
-
-
-
- {theme === option.value && (
-
- )}
-
- ))}
-
- )}
-
-
- {/* 點擊外部關閉 */}
- {showOptions && (
-
setShowOptions(false)}
- />
- )}
-
- )
-}
-
-export default ThemeToggle
\ No newline at end of file
diff --git a/mantis-frontend/src/config.ts b/mantis-frontend/src/config.ts
deleted file mode 100644
index 0548efa..0000000
--- a/mantis-frontend/src/config.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-let httpProtocol = "http";
-let websocketProtocol = "ws";
-const hostname = "192.168.1.3";
-const port = 8080;
-export const host = `${hostname}:${port}`;
-export const siteUrl = `${httpProtocol}://${host}`
-
-export const urls = {
- bootTime: `${httpProtocol}://${host}/misc/boot_time`,
- access_control: {
- ipv4: {
- white_list: `${httpProtocol}://${host}/ebpf/access_control/ipv4/source/white_list`,
- black_list: `${httpProtocol}://${host}/ebpf/access_control/ipv4/source/black_list`,
- },
- ipv6: {
- white_list: `${httpProtocol}://${host}/ebpf/access_control/ipv6/source/white_list`,
- black_list: `${httpProtocol}://${host}/ebpf/access_control/ipv6/source/black_list`,
- }
- }
-} as const;
-
-export const websocketUrl = {
- ipv4FlowStats: (direction: string, flow_direction: string, timeType: string) =>
- `${websocketProtocol}://${host}/ebpf/statistics/websocket/ipv4/${direction}/${flow_direction}/${timeType}`,
- ipv6FlowStats: (direction: string, flow_direction: string, timeType: string) =>
- `${websocketProtocol}://${host}/ebpf/statistics/websocket/ipv6/${direction}/${flow_direction}/${timeType}`,
-
- systemHealth: `${websocketProtocol}://${host}/health/websocket/metrics`,
- detectionAlert: `${websocketProtocol}://${host}/detection/websocket/alert`,
-} as const;
-
-export const REFRESH_INTERVAL = 50000;
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/404.tsx b/mantis-frontend/src/pages/404.tsx
deleted file mode 100644
index d6a4b15..0000000
--- a/mantis-frontend/src/pages/404.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-// pages/404.tsx
-import React from 'react';
-import Link from 'next/link';
-import Head from 'next/head';
-import { motion } from 'framer-motion';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- faHome,
- faArrowLeft,
- faExclamationTriangle,
- faShieldAlt,
- faRobot,
- faTachometerAlt,
- faChartBar
-} from '@fortawesome/free-solid-svg-icons';
-
-const Custom404: React.FC = () => {
- const navigationCards = [
- {
- href: "/dashboard",
- icon: faTachometerAlt,
- title: "Dashboard",
- description: "System Dashboard Overview",
- color: "blue"
- },
- {
- href: "/statistics",
- icon: faChartBar,
- title: "Statistics",
- description: "Data Statistics and Analysis",
- color: "green"
- },
- {
- href: "/access_control",
- icon: faShieldAlt,
- title: "Access Control",
- description: "Access Control Management",
- color: "red"
- },
- {
- href: "/detection",
- icon: faRobot,
- title: "Detection",
- description: "Threat Detection System",
- color: "purple"
- }
- ];
-
- const getColorClasses = (color: string) => {
- const colorMap = {
- blue: {
- bg: "bg-blue-500",
- hover: "hover:bg-blue-600",
- border: "border-blue-200",
- hoverBorder: "hover:border-blue-300",
- hoverBg: "hover:bg-blue-50",
- text: "text-blue-600"
- },
- purple: {
- bg: "bg-purple-500",
- hover: "hover:bg-purple-600",
- border: "border-purple-200",
- hoverBorder: "hover:border-purple-300",
- hoverBg: "hover:bg-purple-50",
- text: "text-purple-600"
- },
- green: {
- bg: "bg-green-500",
- hover: "hover:bg-green-600",
- border: "border-green-200",
- hoverBorder: "hover:border-green-300",
- hoverBg: "hover:bg-green-50",
- text: "text-green-600"
- },
- red: {
- bg: "bg-red-500",
- hover: "hover:bg-red-600",
- border: "border-red-200",
- hoverBorder: "hover:border-red-300",
- hoverBg: "hover:bg-red-50",
- text: "text-red-600"
- }
- };
- return colorMap[color as keyof typeof colorMap];
- };
-
- return (
- <>
-
-
404 - Page Not Found | NetGuardia
-
-
-
-
-
-
-
- {/* 主要錯誤區域 */}
-
-
- 404
-
-
-
- Page Not Found
-
-
-
- Sorry, the page you are looking for does not exist or has been moved.
- Please choose from the options below to continue using the NetGuardia system.
-
-
-
- {/* 導航卡片區域 */}
-
-
- Quick Navigation to Main Features
-
-
-
- {navigationCards.map((card, index) => {
- const colors = getColorClasses(card.color);
- return (
-
-
-
-
-
-
-
-
-
- {card.title}
-
-
- {card.description}
-
-
-
-
-
-
- );
- })}
-
-
-
- {/* 操作按鈕 */}
-
-
-
-
- Back to Home
-
-
-
- window.history.back()}
- className="w-full sm:w-auto bg-gray-600 text-white px-8 py-4 rounded-lg font-semibold hover:bg-gray-700 transition-all duration-300 flex items-center justify-center gap-3 shadow-lg"
- >
-
- Back to Previous Page
-
-
-
- {/* 底部信息 */}
-
-
-
- Need Help?
-
-
- If you believe this is a system error, please contact the technical support team or system administrator.
-
-
-
-
-
-
- >
- );
-};
-
-export default Custom404;
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/_app.tsx b/mantis-frontend/src/pages/_app.tsx
deleted file mode 100644
index e5065ff..0000000
--- a/mantis-frontend/src/pages/_app.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-// src/pages/_app.tsx - 深色模式版本
-import type { AppProps } from 'next/app'
-import { ErrorBoundary } from 'react-error-boundary'
-import { ThemeProvider } from '../providers/ThemeProvider'
-import { WebSocketProvider } from '../providers/WebSocketProvider'
-import { AccessControlProvider } from '../providers/AccessControlProvider'
-import ErrorDialog from '../components/ErrorDialog'
-import '../styles/globals.css'
-
-export default function App({ Component, pageProps }: AppProps) {
- return (
-
-
-
-
-
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/access-control.tsx b/mantis-frontend/src/pages/access-control.tsx
deleted file mode 100644
index 5901a2b..0000000
--- a/mantis-frontend/src/pages/access-control.tsx
+++ /dev/null
@@ -1,848 +0,0 @@
-import React, { useState, useEffect, useContext, useCallback, useMemo } from 'react'
-import Head from 'next/head'
-import { motion, AnimatePresence } from 'framer-motion'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import {
- faPlus,
- faTrash,
- faSearch,
- faFilter,
- faCheck,
- faShieldAlt,
- faNetworkWired,
- faGlobe,
- faInfoCircle,
- faTimes,
- faExclamationTriangle,
- faRefresh,
- faClock
-} from '@fortawesome/free-solid-svg-icons'
-import { AccessControlContext } from '../providers/AccessControlProvider'
-import { useTheme } from '../providers/ThemeProvider'
-import { putData, deleteData } from '../utils/connectionUtils'
-import { urls } from '../config'
-import Layout from '../components/Layout'
-import LoadingSpinner from '../components/LoadingSpinner'
-import {
- AccessControlItem,
- NotificationProps,
- ModalProps,
- ToggleButtonProps,
- StatsCardProps
-} from '../types/AccessControlTypes'
-
-// 通知組件
-const Notification: React.FC
= ({ message, type, onClose }) => {
- useEffect(() => {
- const timer = setTimeout(() => {
- onClose()
- }, 4000)
-
- return () => clearTimeout(timer)
- }, [onClose])
-
- const typeStyles = {
- success: 'bg-green-100 border-green-400 text-green-700',
- error: 'bg-red-100 border-red-400 text-red-700',
- warning: 'bg-yellow-100 border-yellow-400 text-yellow-700',
- info: 'bg-blue-100 border-blue-400 text-blue-700'
- }
-
- return (
-
-
- {message}
-
- ×
-
-
-
- )
-}
-
-// 模態框組件
-const Modal: React.FC = ({
- title,
- children,
- onClose,
- onSubmit,
- submitLabel = 'Submit',
- submitDisabled = false
- }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- return (
-
-
- e.stopPropagation()}
- >
-
-
-
- {children}
-
-
-
-
- Cancel
-
- {onSubmit && (
-
- {submitLabel}
-
- )}
-
-
-
-
- )
-}
-
-// 切換按鈕組件
-const ToggleButton: React.FC = ({ isActive, onClick, children, className = "" }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- return (
-
- {children}
-
- )
-}
-
-const StatsCard: React.FC = ({ title, value, icon, color }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- return (
-
-
-
-
{title}
-
{value.toLocaleString()}
-
-
-
-
-
-
- )
-}
-
-// 主要組件
-const AccessControl: React.FC = () => {
- const {
- currentData,
- filteredData,
- isLoading,
- setFilteredData,
- switchTo,
- refreshData,
- getCacheStatus
- } = useContext(AccessControlContext)
-
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- // 狀態管理
- const [isIPv6, setIsIPv6] = useState(false)
- const [listType, setListType] = useState<'black_list' | 'white_list'>('black_list')
- const [searchTerm, setSearchTerm] = useState('')
- const [isModalOpen, setIsModalOpen] = useState(false)
- const [newIp, setNewIp] = useState('')
- const [newPort, setNewPort] = useState('')
- const [blockAllPorts, setBlockAllPorts] = useState(false)
- const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)
- const [itemToDelete, setItemToDelete] = useState<{ ip: string; port: string | number } | null>(null)
- const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' | 'warning' | 'info' } | null>(null)
- const [isSubmitting, setIsSubmitting] = useState(false)
-
- // 切换IP版本和列表类型 - 无跳动的平滑切换
- const handleIPVersionChange = useCallback((newIsIPv6: boolean) => {
- if (newIsIPv6 !== isIPv6) {
- setIsIPv6(newIsIPv6)
- switchTo(newIsIPv6, listType)
- }
- }, [isIPv6, listType, switchTo])
-
- const handleListTypeChange = useCallback((newListType: 'black_list' | 'white_list') => {
- if (newListType !== listType) {
- setListType(newListType)
- switchTo(isIPv6, newListType)
- }
- }, [isIPv6, listType, switchTo])
-
- // 其他回调函数
- const handleAddItemClick = useCallback(() => {
- setIsModalOpen(true)
- setNewIp('')
- setNewPort('')
- setBlockAllPorts(false)
- }, [])
-
- const handleModalClose = useCallback(() => {
- setIsModalOpen(false)
- setNewIp('')
- setNewPort('')
- setBlockAllPorts(false)
- }, [])
-
- // 通知功能
- const showNotification = useCallback((message: string, type: 'success' | 'error' | 'warning' | 'info' = 'success') => {
- setNotification({ message, type })
- }, [])
-
- const closeNotification = useCallback(() => {
- setNotification(null)
- }, [])
-
- // 手动更新資料
- const handleRefresh = useCallback(async () => {
- try {
- await refreshData(isIPv6, listType)
- showNotification('Data updated', 'success')
- } catch (error) {
- showNotification('Update failed', 'error')
- }
- }, [isIPv6, listType, refreshData, showNotification])
-
- // 初始化資料 - 组件挂载时加载默认資料
- useEffect(() => {
- switchTo(isIPv6, listType)
- }, []) // 只在组件挂载时执行一次
-
- // 搜索过滤 - 基于currentData而不是重新获取
- useEffect(() => {
- if (!searchTerm.trim()) {
- setFilteredData(currentData)
- return
- }
-
- const lowerSearchTerm = searchTerm.toLowerCase()
- const filtered = currentData.filter(({ ip }) =>
- ip.toLowerCase().includes(lowerSearchTerm)
- )
- setFilteredData(filtered)
- }, [searchTerm, currentData, setFilteredData])
-
- // 展平資料,便於表格顯示
- const flatData = useMemo(() =>
- filteredData.flatMap(({ ip, ports }) =>
- ports.map((port) => ({ ip, port }))
- ),
- [filteredData]
- )
-
- // 統計資料
- const stats = useMemo(() => {
- const uniqueIPs = new Set(filteredData.map(item => item.ip)).size
- const totalEntries = flatData.length
- const allPortsEntries = flatData.filter(item => item.port === "0" || item.port === 0).length
-
- return {
- uniqueIPs,
- totalEntries,
- allPortsEntries,
- specificPortEntries: totalEntries - allPortsEntries
- }
- }, [filteredData, flatData])
-
- // IP 驗證
- const validateIP = useCallback((ip: string, isIPv6: boolean): boolean => {
- if (isIPv6) {
- // 簡單的 IPv6 驗證
- const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^::1$|^::$/
- return ipv6Regex.test(ip) || ip.includes('::')
- } else {
- // IPv4 驗證
- const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/
- if (!ipv4Regex.test(ip)) return false
-
- const parts = ip.split('.')
- return parts.every(part => {
- const num = parseInt(part, 10)
- return num >= 0 && num <= 255
- })
- }
- }, [])
-
- // 端口驗證
- const validatePort = useCallback((port: string): boolean => {
- if (!port.trim()) return false
- const portNum = parseInt(port, 10)
- return !isNaN(portNum) && portNum >= 1 && portNum <= 65535
- }, [])
-
- // 添加項目
- const handleAddItemSubmit = useCallback(async () => {
- if (!newIp.trim()) {
- showNotification('Please enter a valid IP address', 'error')
- return
- }
-
- if (!validateIP(newIp.trim(), isIPv6)) {
- showNotification(`Please enter a valid ${isIPv6 ? 'IPv6' : 'IPv4'} address`, 'error')
- return
- }
-
- if (!blockAllPorts && !validatePort(newPort)) {
- showNotification('Please enter a valid port number (1-65535)', 'error')
- return
- }
-
- const portToSend = blockAllPorts ? "0" : newPort
- const url = isIPv6
- ? urls.access_control.ipv6[listType]
- : urls.access_control.ipv4[listType]
-
- setIsSubmitting(true)
-
- try {
- await new Promise((resolve, reject) => {
- putData(
- url,
- `${newIp.trim()}:${portToSend}`,
- () => {
- showNotification(
- `Successfully added: ${newIp.trim()}:${blockAllPorts ? '*' : portToSend}`,
- 'success'
- )
- setIsModalOpen(false)
- setNewIp('')
- setNewPort('')
- setBlockAllPorts(false)
- // 更新当前資料
- refreshData(isIPv6, listType)
- resolve()
- },
- (error) => {
- showNotification(`Failed to add: ${error.message}`, 'error')
- reject(error)
- }
- )
- })
- } catch (error) {
- console.error('Add item error:', error)
- } finally {
- setIsSubmitting(false)
- }
- }, [newIp, newPort, blockAllPorts, isIPv6, listType, refreshData, showNotification, validateIP, validatePort])
-
- // 刪除處理
- const handleDeleteClick = useCallback((ip: string, port: string | number) => {
- setItemToDelete({ ip, port })
- setIsConfirmModalOpen(true)
- }, [])
-
- const handleDeleteCancel = useCallback(() => {
- setIsConfirmModalOpen(false)
- setItemToDelete(null)
- }, [])
-
- const handleDeleteConfirm = useCallback(async () => {
- if (!itemToDelete) return
-
- const { ip, port } = itemToDelete
- const formattedIp = isIPv6 ? `[${ip}]` : ip
- const url = isIPv6
- ? urls.access_control.ipv6[listType]
- : urls.access_control.ipv4[listType]
-
- setIsSubmitting(true)
-
- try {
- await new Promise((resolve, reject) => {
- deleteData(
- url,
- `${formattedIp}:${port}`,
- () => {
- showNotification(
- `Successfully deleted: ${formattedIp}:${port === "0" || port === 0 ? "*" : port}`,
- 'success'
- )
- // 更新当前資料
- refreshData(isIPv6, listType)
- setIsConfirmModalOpen(false)
- setItemToDelete(null)
- resolve()
- },
- (error) => {
- showNotification(`Failed to delete: ${error.message}`, 'error')
- setIsConfirmModalOpen(false)
- setItemToDelete(null)
- reject(error)
- }
- )
- })
- } catch (error) {
- console.error('Delete item error:', error)
- } finally {
- setIsSubmitting(false)
- }
- }, [itemToDelete, isIPv6, listType, refreshData, showNotification])
-
- // 搜索處理
- const handleSearchChange = useCallback((e: React.ChangeEvent) => {
- setSearchTerm(e.target.value)
- }, [])
-
- // 全部端口勾選處理
- const handleBlockAllPortsChange = useCallback((e: React.ChangeEvent) => {
- setBlockAllPorts(e.target.checked)
- if (e.target.checked) {
- setNewPort('')
- }
- }, [])
-
- // 获取缓存状态用于调试
- const cacheStatus = getCacheStatus()
-
- return (
- <>
-
- Access Control - NetGuardia
-
-
-
-
- {/* 通知 */}
-
- {notification && (
-
- )}
-
-
- {/* 頁面標題 */}
-
-
-
-
-
- Access Control Management
-
-
- Manage IPv4 and IPv6 network access whitelists and blacklists
-
-
-
-
-
- {/* 統計卡片 */}
-
-
-
-
-
-
-
- {/* 控制面板 */}
-
-
- {/* 搜索框 */}
-
-
-
- setSearchTerm(e.target.value)}
- placeholder="Search IP Address..."
- className={`w-full pl-8 pr-3 py-1 text-sm border rounded-lg focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 ${
- isDark ? 'bg-gray-700 border-gray-500 text-gray-300 placeholder-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'
- }`}
- />
-
-
-
- {/* IP 版本選擇 */}
-
-
IP Version:
-
- setIsIPv6(false)}
- >
- IPv4
-
- setIsIPv6(true)}
- >
- IPv6
-
-
-
-
- {/* 列表類型選擇 */}
-
-
List Type:
-
- handleListTypeChange("black_list")}
- >
-
- Blacklist
-
- handleListTypeChange("white_list")}
- >
-
- Whitelist
-
-
-
-
- {/* 操作按鈕 */}
-
-
-
- Update
-
-
-
-
- Add Rule
-
-
-
-
-
- {/* 資料表格 */}
-
-
-
-
- {isIPv6 ? 'IPv6' : 'IPv4'} {listType === 'black_list' ? 'Blacklist' : 'Whitelist'}
- {isLoading && (
-
- )}
-
-
- {flatData.length} Rules
-
-
-
-
- {/* 简化表格切换,移除奇怪的动画 */}
- {flatData.length === 0 ? (
-
-
-
- {searchTerm ? 'No matching results found' : 'No data yet'}
-
-
- {searchTerm ? 'Please try adjusting your search criteria' : 'Click the button above to add the first rule'}
-
- {!searchTerm && (
-
-
- Add Rule
-
- )}
-
- ) : (
-
-
-
-
-
- IP Address
-
-
- Port
-
-
- Action
-
-
-
-
- {flatData.map(({ ip, port }, index) => (
-
-
- {ip}
-
-
-
- {port === "0" || port === 0 ? "All ports (*)" : port}
-
-
-
- handleDeleteClick(ip, port)}
- title="Delete Rule"
- >
-
-
-
-
- ))}
-
-
-
- )}
-
-
- {/* 添加規則模態框 */}
- {isModalOpen && (
-
-
-
-
- IP Address
-
- setNewIp(e.target.value)}
- placeholder={isIPv6 ? "2001:db8::" : "192.168.1.1"}
- className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 ${
- isDark ? 'bg-gray-700 border-gray-600 text-gray-300 placeholder-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'
- }`}
- />
-
-
-
-
-
-
- Block All Ports
-
-
-
-
-
-
- Port
-
-
setNewPort(e.target.value)}
- placeholder="80"
- disabled={blockAllPorts}
- className={`w-full px-3 py-2 border rounded-lg focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 ${
- blockAllPorts
- ? (isDark ? 'bg-gray-600 text-gray-500 border-gray-600' : 'bg-gray-100 text-gray-500 border-gray-300')
- : (isDark ? 'bg-gray-700 border-gray-600 text-gray-300' : 'bg-white border-gray-300 text-gray-900')
- }`}
- />
- {blockAllPorts && (
-
- Block all ports for this IP
-
- )}
-
-
-
-
-
-
-
- Add to {listType === 'black_list' ? 'Blacklist' : 'Whitelist'}
-
-
- {listType === 'black_list'
- ? 'IP addresses in the Blacklist will be denied access'
- : 'Only IP addresses in the Whitelist can access'
- }
- {blockAllPorts && ', all ports will be affected'}
-
-
-
-
-
-
- )}
-
- {/* 確認刪除模態框 */}
- {isConfirmModalOpen && (
-
-
-
-
- Warning
-
-
-
- Are you sure you want to delete this rule?
-
-
- {itemToDelete && (
-
-
-
- IP Address:
- {itemToDelete.ip}
-
-
- Port:
-
- {itemToDelete.port === "0" || itemToDelete.port === 0
- ? "All Ports (*)"
- : itemToDelete.port
- }
-
-
-
- List Type:
-
- {listType === 'black_list' ? 'Blacklist' : 'Whitelist'}
-
-
-
-
- )}
-
-
-
-
-
- This action cannot be undone. Once deleted, this rule will take effect immediately.
-
-
-
-
-
- )}
-
- >
- )
-}
-
-export default AccessControl
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/dashboard.tsx b/mantis-frontend/src/pages/dashboard.tsx
deleted file mode 100644
index 0459af0..0000000
--- a/mantis-frontend/src/pages/dashboard.tsx
+++ /dev/null
@@ -1,1076 +0,0 @@
-import React, { useContext, useState, useEffect, useCallback, useRef } from 'react'
-import Head from 'next/head'
-import { motion } from 'framer-motion'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import {
- faNetworkWired,
- faGlobe,
- faArrowUp,
- faArrowDown,
- faServer,
- faChartLine,
- faPause,
- faPlay,
- faClock,
- faShieldAlt
-} from '@fortawesome/free-solid-svg-icons'
-import { WebsocketContext } from '../providers/WebSocketProvider'
-import Layout from '../components/Layout'
-import LoadingSpinner from '../components/LoadingSpinner'
-import { combineLatest } from 'rxjs'
-import { map, catchError } from 'rxjs/operators'
-import { of } from 'rxjs'
-import ReactEcharts from 'echarts-for-react'
-import * as echarts from 'echarts'
-import {
- TrendData,
- TrendDataPoint,
- TrafficData,
- UpdateControlProps,
- EChartsComponentProps
-} from '../types/DashboardTypes'
-import { useTheme } from "../providers/ThemeProvider"
-
-// 更新频率選項 (毫秒)
-const UPDATE_INTERVALS = [
- { label: "immediate", value: 0 },
- { label: "0.5s", value: 500 },
- { label: "1s", value: 1000 },
- { label: "2s", value: 2000 },
- { label: "3s", value: 3000 },
- { label: "5s", value: 5000 },
- { label: "10s", value: 10000 }
-];
-
-// 智能格式化位元組大小(返回数值和单位)
-const formatBytesWithUnit = (bytes: number): {value: number, unit: string} => {
- const units = ["B", "KB", "MB", "GB", "TB"]
- if (bytes < 1024) return {value: bytes, unit: "B"}
-
- let i = 0
- let value = bytes
-
- while (value >= 1024 && i < units.length - 1) {
- value /= 1024
- i++
- }
-
- return {value: parseFloat(value.toFixed(2)), unit: units[i]}
-}
-
-// 格式化位元組大小(用于显示)
-const formatBytes = (bytes: number): string => {
- const {value, unit} = formatBytesWithUnit(bytes)
- return `${value} ${unit}`
-}
-
-// 获取最适合的图表单位(更保守的切换策略)
-const getBestChartUnit = (dataArray: TrendDataPoint[]): {unit: string, divisor: number} => {
- if (dataArray.length === 0) return {unit: "B", divisor: 1}
-
- const maxBytes = Math.max(...dataArray.map(point => point.value))
-
- if (maxBytes < 10 * 1024) {
- return {unit: "B", divisor: 1}
- } else if (maxBytes < 10 * 1024 * 1024) {
- return {unit: "KB", divisor: 1024}
- } else if (maxBytes < 10 * 1024 * 1024 * 1024) {
- return {unit: "MB", divisor: 1024 * 1024}
- } else {
- return {unit: "GB", divisor: 1024 * 1024 * 1024}
- }
-}
-
-// 協議號對應名稱
-const getProtocolName = (protocol: number): string => {
- switch (protocol) {
- case 1: return 'ICMP'
- case 6: return 'TCP'
- case 17: return 'UDP'
- case 47: return 'GRE'
- case 50: return 'ESP'
- case 51: return 'AH'
- case 58: return 'ICMPv6'
- default: return `Proto ${protocol}`
- }
-}
-
-// 解析流量資料
-const parseFlowData = (rawData: any): any => {
- try {
- if (typeof rawData === 'object' && rawData !== null) {
- return rawData
- }
- return JSON.parse(rawData)
- } catch (error) {
- console.error("Error parsing flow data:", error)
- return {}
- }
-}
-
-const UpdateControl: React.FC = ({
- updateInterval,
- setUpdateInterval,
- isPaused,
- setIsPaused,
- lastUpdateTime,
- connectionStatus
- }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- return (
-
- {/* 更新頻率控制 */}
-
-
- Update interval:
- setUpdateInterval(Number(e.target.value))}
- className="text-sm border border-gray-300 rounded px-2 py-1 bg-white focus:outline-none focus:border-blue-500"
- >
- {UPDATE_INTERVALS.map(interval => (
-
- {interval.label}
-
- ))}
-
-
-
- {/* 暫停/恢復按鈕 */}
-
setIsPaused(!isPaused)}
- className={`px-3 py-1 rounded-md text-sm font-medium transition-colors flex items-center space-x-1 ${
- isPaused
- ? "bg-green-100 text-green-700 hover:bg-green-200"
- : "bg-orange-100 text-orange-700 hover:bg-orange-200"
- }`}
- >
-
- {isPaused ? "resume" : "pause"}
-
-
- {/* 連接狀態和最後更新時間 */}
-
- {lastUpdateTime && (
-
- Last update: {lastUpdateTime.toLocaleTimeString()}
-
- )}
-
-
- );
-};
-
-const EChartsComponent: React.FC = ({ data, type, isPaused }) => {
- const chartRef = useRef(null)
- const { actualTheme } = useTheme()
-
- const isDark = actualTheme === 'dark'
-
- const getOption = useCallback(() => {
- if (!data.ingressSource.length || !data.egressSource.length) {
- return {}
- }
-
- const validateData = (dataArray: TrendDataPoint[]) => {
- return dataArray.every(point =>
- point &&
- typeof point.value === 'number' &&
- !isNaN(point.value) &&
- point.time
- )
- }
-
- if (!validateData(data.ingressSource) || !validateData(data.egressSource)) {
- console.warn(`Invalid chart data detected for ${type}, skipping update`)
- return {}
- }
-
- const colors = type === 'ipv4'
- ? ['#3B82F6', '#EF4444']
- : ['#10B981', '#F59E0B']
-
- const timeLabels = data.ingressSource.map(point => point.time)
-
- const allData = [...data.ingressSource, ...data.egressSource]
- const {unit, divisor} = getBestChartUnit(allData)
-
- const ingressValues = data.ingressSource.map(point =>
- typeof point.value === 'number' && !isNaN(point.value) ?
- Math.max(0, point.value / divisor) : 0
- )
- const egressValues = data.egressSource.map(point =>
- typeof point.value === 'number' && !isNaN(point.value) ?
- Math.max(0, point.value / divisor) : 0
- )
-
- return {
- animation: false,
-
- title: {
- text: '',
- left: 'center',
- textStyle: {
- fontSize: 16,
- fontWeight: 'bold',
- color: isPaused ? '#F59E0B' : (isDark ? '#E5E7EB' : '#374151')
- }
- },
- tooltip: {
- trigger: 'axis',
- axisPointer: {
- type: 'cross',
- label: {
- backgroundColor: isDark ? '#4B5563' : '#6a7985'
- }
- },
- backgroundColor: isDark ? 'rgba(31, 41, 55, 0.95)' : 'rgba(0, 0, 0, 0.8)',
- borderColor: isDark ? '#4B5563' : 'transparent',
- textStyle: {
- color: isDark ? '#F3F4F6' : '#fff'
- },
- formatter: function(params: any) {
- let result = `Time: ${params[0].name} `
- let totalTraffic = 0
- params.forEach((param: any) => {
- const value = typeof param.value === 'number' ? param.value : 0
- result += `${param.seriesName}: ${value.toFixed(2)} ${unit} `
- totalTraffic += value
- })
- result += `Total traffic: ${totalTraffic.toFixed(2)} ${unit} `
- return result
- }
- },
- legend: {
- data: [
- `${type.toUpperCase()} Ingress Traffic`,
- `${type.toUpperCase()} Egress Traffic`
- ],
- top: 30,
- textStyle: {
- color: isDark ? '#D1D5DB' : '#6B7280'
- }
- },
- grid: {
- left: '3%',
- right: '4%',
- bottom: '3%',
- top: '60px',
- containLabel: true
- },
- xAxis: {
- type: 'category',
- boundaryGap: false,
- data: timeLabels,
- axisLabel: {
- color: isDark ? '#9CA3AF' : '#6B7280',
- rotate: 45,
- interval: Math.max(1, Math.floor(timeLabels.length / 6))
- },
- axisLine: {
- lineStyle: {
- color: isDark ? '#F3F4F6' : '#858997',
- }
- },
- splitLine: {
- show: false
- }
- },
- yAxis: {
- type: 'value',
- name: `Traffic (${unit})`,
- nameTextStyle: {
- color: isDark ? '#9CA3AF' : '#6B7280'
- },
- axisLabel: {
- color: isDark ? '#9CA3AF' : '#6B7280',
- formatter: `{value} ${unit}`
- },
- axisLine: {
- lineStyle: {
- color: isDark ? '#4B5563' : '#E5E7EB'
- }
- },
- splitLine: {
- lineStyle: {
- color: isDark ? '#F3F4F6' : '#858997'
- }
- },
- min: 0
- },
- series: [
- {
- name: `${type.toUpperCase()} Ingress Traffic`,
- type: 'line',
- smooth: true,
- smoothMonotone: 'x',
- symbol: 'circle',
- symbolSize: 4,
- showSymbol: true,
- itemStyle: { color: colors[0] },
- lineStyle: { width: 2.5, color: colors[0], cap: 'round', join: 'round' },
- areaStyle: {
- color: {
- type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
- colorStops: [
- { offset: 0, color: colors[0] + '80' },
- { offset: 1, color: colors[0] + '20' }
- ]
- }
- },
- data: ingressValues,
- emphasis: {
- focus: 'series',
- lineStyle: { width: 3 },
- itemStyle: { shadowBlur: 10, shadowColor: colors[0] + '80' }
- },
- animation: false
- },
- {
- name: `${type.toUpperCase()} Egress Traffic`,
- type: 'line',
- smooth: true,
- smoothMonotone: 'x',
- symbol: 'circle',
- symbolSize: 4,
- showSymbol: true,
- itemStyle: { color: colors[1] },
- lineStyle: { width: 2.5, color: colors[1], cap: 'round', join: 'round' },
- areaStyle: {
- color: {
- type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
- colorStops: [
- { offset: 0, color: colors[1] + '80' },
- { offset: 1, color: colors[1] + '20' }
- ]
- }
- },
- data: egressValues,
- emphasis: {
- focus: 'series',
- lineStyle: { width: 3 },
- itemStyle: { shadowBlur: 10, shadowColor: colors[1] + '80' }
- },
- animation: false
- }
- ]
- }
- }, [data, type, isPaused, isDark])
-
- const onChartReady = useCallback((chart: any) => {
- console.log(`${type} chart ready`)
- }, [type])
-
- const onEvents = {
- 'click': (params: any) => {
- console.log('Chart clicked:', params)
- }
- }
-
- return (
-
- )
-}
-
-const DONUT_COLORS: Array<{ start: string; end: string }> = [
- { start: '#EF4444', end: '#DC2626' },
- { start: '#F59E0B', end: '#D97706' },
- { start: '#3B82F6', end: '#2563EB' },
- { start: '#10B981', end: '#059669' },
- { start: '#8B5CF6', end: '#7C3AED' },
- { start: '#EC4899', end: '#DB2777' },
- { start: '#F97316', end: '#EA580C' },
- { start: '#06B6D4', end: '#0891B2' },
- { start: '#84CC16', end: '#65A30D' },
- { start: '#6366F1', end: '#4F46E5' }
-]
-
-const PROTOCOL_COLORS: Array<{ start: string; end: string }> = [
- { start: '#3B82F6', end: '#2563EB' },
- { start: '#10B981', end: '#059669' },
- { start: '#F59E0B', end: '#D97706' },
- { start: '#8B5CF6', end: '#7C3AED' },
- { start: '#EC4899', end: '#DB2777' },
- { start: '#06B6D4', end: '#0891B2' },
- { start: '#EF4444', end: '#DC2626' },
- { start: '#F97316', end: '#EA580C' },
- { start: '#84CC16', end: '#65A30D' },
- { start: '#6366F1', end: '#4F46E5' }
-]
-
-const DonutChart: React.FC<{
- data: Record
- colors?: Array<{ start: string; end: string }>
- totalLabel?: string
-}> = ({ data, colors = DONUT_COLORS, totalLabel = 'Total' }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- const chartData = Object.entries(data)
- .map(([name, value]) => ({ name, value }))
- .sort((a, b) => b.value - a.value)
-
- const totalAttacks = chartData.reduce((sum, item) => sum + item.value, 0)
-
- const getOption = () => {
- if (chartData.length === 0) {
- return {
- graphic: {
- elements: [{
- type: 'group',
- left: 'center',
- top: 'center',
- children: [
- {
- type: 'text',
- style: {
- text: 'Waiting for Data...',
- fontSize: 14,
- fill: isDark ? '#6B7280' : '#9CA3AF',
- textAlign: 'center'
- }
- }
- ]
- }]
- }
- }
- }
-
- return {
- graphic: { elements: [] },
- tooltip: {
- trigger: 'item',
- backgroundColor: isDark ? 'rgba(31, 41, 55, 0.95)' : 'rgba(255, 255, 255, 0.95)',
- borderColor: isDark ? '#4B5563' : '#E5E7EB',
- borderWidth: 1,
- textStyle: {
- color: isDark ? '#F3F4F6' : '#1F2937'
- },
- formatter: (params: any) => {
- const colorDot = ` `
- return `${colorDot}${params.name} Count: ${params.value} Proportion: ${params.percent}%`
- }
- },
- series: [
- {
- type: 'pie',
- radius: ['50%', '78%'],
- center: ['50%', '50%'],
- silent: true,
- avoidLabelOverlap: true,
- itemStyle: {
- borderRadius: 6,
- borderColor: isDark ? '#4B5563' : '#fff',
- borderWidth: 3
- },
- label: {
- show: true,
- position: 'center',
- formatter: () => `{total|${totalAttacks}}\n{label|${totalLabel}}`,
- rich: {
- total: {
- fontSize: 28,
- fontWeight: 'bold',
- color: isDark ? '#F3F4F6' : '#1F2937',
- lineHeight: 36
- },
- label: {
- fontSize: 13,
- color: isDark ? '#9CA3AF' : '#6B7280',
- lineHeight: 22
- }
- }
- },
- labelLine: {
- show: false
- },
- data: chartData.map((item, index) => {
- const colorPair = colors[index % colors.length]
- return {
- ...item,
- itemStyle: {
- color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
- { offset: 0, color: colorPair.start },
- { offset: 1, color: colorPair.end }
- ])
- }
- }
- }),
- animationType: 'scale',
- animationEasing: 'elasticOut',
- animationDelay: () => Math.random() * 200
- }
- ]
- }
- }
-
- return (
-
-
- {chartData.length > 0 && (
-
- {chartData.map((item, index) => {
- const colorPair = colors[index % colors.length]
- const percent = totalAttacks > 0 ? ((item.value / totalAttacks) * 100).toFixed(1) : '0'
- return (
-
-
-
-
- {item.name}
-
-
-
-
- {item.value}
-
-
- {percent}%
-
-
-
- )
- })}
-
- )}
-
- )
-}
-
-const Dashboard: React.FC = () => {
- const { actualTheme } = useTheme()
- const { getIPv4FlowStream, getIPv6FlowStream, getDetectionAlertStream } = useContext(WebsocketContext)
- const [isLoading, setIsLoading] = useState(true)
- const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'error'>('connecting')
- const [networkData, setNetworkData] = useState({})
-
- const [updateInterval, setUpdateInterval] = useState(2000)
- const [isPaused, setIsPaused] = useState(false)
- const [lastUpdateTime, setLastUpdateTime] = useState(null)
-
- const [apiTimeRange, setApiTimeRange] = useState("1min")
-
- const [attackTypeCounts, setAttackTypeCounts] = useState>({})
- const [protocolCounts, setProtocolCounts] = useState>({})
-
- const latestDataRef = useRef({})
- const lastUpdateRef = useRef(0)
- const updateIntervalRef = useRef(updateInterval)
- const isPausedRef = useRef(isPaused)
-
- const isDark = actualTheme === 'dark'
-
- useEffect(() => { updateIntervalRef.current = updateInterval }, [updateInterval])
- useEffect(() => { isPausedRef.current = isPaused }, [isPaused])
-
- const [trendDataIPv4, setTrendDataIPv4] = useState({
- ingressSource: [],
- egressSource: []
- })
-
- const [trendDataIPv6, setTrendDataIPv6] = useState({
- ingressSource: [],
- egressSource: []
- })
-
- const [trafficData, setTrafficData] = useState({
- ipv4: { ingressSource: 0, egressSource: 0 },
- ipv6: { ingressSource: 0, egressSource: 0 }
- })
-
- const calculateThroughput = useCallback((
- ipVersion: string,
- direction: string,
- trafficType: string
- ): number => {
- try {
- const key = `${direction}_${trafficType}`
- const connections = latestDataRef.current[key]?.[ipVersion] || {}
- let totalBytes = 0
-
- for (const connection in connections) {
- const connectionData = connections[connection]
- if (connectionData && typeof connectionData.bytes === "number" && connectionData.bytes >= 0) {
- totalBytes += connectionData.bytes
- }
- }
-
- return Math.max(0, totalBytes)
- } catch (error) {
- console.error(`Error calculating throughput for ${ipVersion} ${direction} ${trafficType}:`, error)
- return 0
- }
- }, [])
-
- const updateChartData = useCallback(() => {
- if (isPausedRef.current) return;
-
- const ipv4IngressSource = calculateThroughput("ipv4", "ingress", "source")
- const ipv4EgressSource = calculateThroughput("ipv4", "egress", "source")
- const ipv6IngressSource = calculateThroughput("ipv6", "ingress", "source")
- const ipv6EgressSource = calculateThroughput("ipv6", "egress", "source")
-
- setTrafficData({
- ipv4: { ingressSource: ipv4IngressSource, egressSource: ipv4EgressSource },
- ipv6: { ingressSource: ipv6IngressSource, egressSource: ipv6EgressSource }
- })
-
- const currentTime = new Date().toLocaleTimeString('en-GB', {
- hour12: false,
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit'
- })
-
- const updateTrendArray = (prevArray: TrendDataPoint[], newValue: number) => {
- const maxPoints = 16
- const newPoint = { time: currentTime, value: Math.max(0, newValue) }
-
- if (prevArray.length < maxPoints) {
- return [...prevArray, newPoint]
- }
-
- return [...prevArray.slice(1), newPoint]
- }
-
- setTrendDataIPv4(prev => ({
- ingressSource: updateTrendArray(prev.ingressSource, ipv4IngressSource),
- egressSource: updateTrendArray(prev.egressSource, ipv4EgressSource)
- }))
-
- setTrendDataIPv6(prev => ({
- ingressSource: updateTrendArray(prev.ingressSource, ipv6IngressSource),
- egressSource: updateTrendArray(prev.egressSource, ipv6EgressSource)
- }))
-
- setLastUpdateTime(new Date())
- }, [calculateThroughput])
-
- useEffect(() => {
- if (!isPaused && Object.keys(latestDataRef.current).length > 0) {
- updateChartData()
- lastUpdateRef.current = Date.now()
- }
- }, [isPaused, updateChartData])
-
- useEffect(() => {
- if (updateInterval === 0 || isPaused) {
- return
- }
-
- const intervalId = setInterval(() => {
- const now = Date.now()
- if (Object.keys(latestDataRef.current).length > 0 &&
- (now - lastUpdateRef.current) >= updateInterval &&
- !isPausedRef.current) {
- updateChartData()
- lastUpdateRef.current = now
- }
- }, Math.min(updateInterval / 4, 1000))
-
- return () => clearInterval(intervalId)
- }, [updateInterval, isPaused, updateChartData])
-
- useEffect(() => {
- if (!getIPv4FlowStream || !getIPv6FlowStream) {
- console.warn("WebSocket streams not available")
- setConnectionStatus('error')
- setIsLoading(false)
- return
- }
-
- return () => {
- console.log("Component unmounting - cleaning up WebSocket subscriptions")
- }
- }, [getIPv4FlowStream, getIPv6FlowStream])
-
- useEffect(() => {
- if (!getIPv4FlowStream || !getIPv6FlowStream) {
- setConnectionStatus('error')
- setIsLoading(false)
- return
- }
-
- console.log(`[TIME RANGE CHANGE] Switching to ${apiTimeRange}`)
-
- setTrendDataIPv4({ ingressSource: [], egressSource: [] })
- setTrendDataIPv6({ ingressSource: [], egressSource: [] })
- setNetworkData({})
- latestDataRef.current = {}
- setConnectionStatus('connecting')
-
- const PROTOCOLS = ["ipv4", "ipv6"] as const
- const DIRECTIONS = ["ingress", "egress"] as const
- const FLOW_DIRECTIONS = ["source", "destination"] as const
- const TIME_RANGES = ["1min", "10min", "1hour"] as const
-
- const streamConfigs: Array<{
- protocol: typeof PROTOCOLS[number]
- direction: typeof DIRECTIONS[number]
- flowDirection: typeof FLOW_DIRECTIONS[number]
- range: typeof TIME_RANGES[number]
- }> = []
-
- PROTOCOLS.forEach(protocol => {
- DIRECTIONS.forEach(direction => {
- FLOW_DIRECTIONS.forEach(flowDirection => {
- TIME_RANGES.forEach(range => {
- streamConfigs.push({ protocol, direction, flowDirection, range })
- })
- })
- })
- })
-
- const streams = streamConfigs.map(config => {
- const { protocol, direction, flowDirection } = config
- const getStream = protocol === "ipv4" ? getIPv4FlowStream : getIPv6FlowStream
-
- return getStream(direction, flowDirection, apiTimeRange).pipe(
- map((rawData: any) => {
- if (rawData === null || rawData === undefined) {
- return { key: `${direction}_${flowDirection}`, protocol, data: {} }
- }
-
- const parsedData = parseFlowData(rawData)
-
- if (!parsedData || typeof parsedData !== 'object') {
- return { key: `${direction}_${flowDirection}`, protocol, data: {} }
- }
-
- return { key: `${direction}_${flowDirection}`, protocol, data: parsedData }
- }),
- catchError(error => {
- console.error(`${protocol} ${direction}/${flowDirection} (${apiTimeRange}):`, error)
- return of({ key: `${direction}_${flowDirection}`, protocol, data: {} })
- })
- )
- })
-
- const subscription = combineLatest(streams).subscribe({
- next: (results) => {
- const validResults = results.filter(result => result !== null && result.data !== null)
-
- if (validResults.length === 0) {
- setConnectionStatus('connected')
- setIsLoading(false)
- return
- }
-
- const newData: any = {}
- validResults.forEach(result => {
- const { key, protocol, data } = result
- if (!newData[key]) newData[key] = {}
- newData[key][protocol] = data || {}
- })
-
- if (Object.keys(newData).length > 0) {
- latestDataRef.current = { ...latestDataRef.current, ...newData }
-
- setNetworkData((prevData: any) => {
- const updatedData = { ...prevData }
- Object.entries(newData).forEach(([key, protocolData]: [string, any]) => {
- if (!updatedData[key]) updatedData[key] = {}
- Object.entries(protocolData || {}).forEach(([protocol, data]) => {
- updatedData[key][protocol] = data
- })
- })
- return updatedData
- })
-
- const now = Date.now()
- const currentUpdateInterval = updateIntervalRef.current
- const currentIsPaused = isPausedRef.current
-
- const shouldUpdate = !currentIsPaused &&
- (currentUpdateInterval === 0 || (now - lastUpdateRef.current) >= currentUpdateInterval)
-
- if (shouldUpdate) {
- updateChartData()
- lastUpdateRef.current = now
- }
-
- setConnectionStatus('connected')
- setIsLoading(false)
- }
- },
- error: (error) => {
- console.error(`(${apiTimeRange}):`, error)
- setConnectionStatus('error')
- setIsLoading(false)
- }
- })
-
- return () => {
- console.log(`Cleaning up subscription for ${apiTimeRange}`)
- subscription.unsubscribe()
- }
- }, [getIPv4FlowStream, getIPv6FlowStream, apiTimeRange, updateChartData])
-
- // Subscribe to detection alerts for attack type / protocol breakdown charts
- useEffect(() => {
- if (!getDetectionAlertStream) return
-
- const subscription = getDetectionAlertStream().subscribe({
- next: (rawData: any) => {
- try {
- const data = typeof rawData === 'string' ? JSON.parse(rawData) : rawData
- if (!data || !data.is_attack) return
-
- if (data.attack_type) {
- setAttackTypeCounts(prev => ({
- ...prev,
- [data.attack_type]: (prev[data.attack_type] || 0) + 1
- }))
- }
-
- if (data.protocol !== undefined) {
- const protoName = getProtocolName(data.protocol)
- setProtocolCounts(prev => ({
- ...prev,
- [protoName]: (prev[protoName] || 0) + 1
- }))
- }
- } catch (error) {
- console.error("Error parsing detection alert for dashboard:", error)
- }
- },
- error: (error: any) => {
- console.error("Detection alert stream error in dashboard:", error)
- }
- })
-
- return () => subscription.unsubscribe()
- }, [getDetectionAlertStream])
-
- const statsCards = [
- {
- title: 'IPv4 Ingress Traffic',
- value: formatBytes(trafficData.ipv4.ingressSource),
- icon: faGlobe,
- iconColor: 'text-blue-600',
- bgColor: 'bg-blue-50'
- },
- {
- title: 'IPv4 Egress Traffic',
- value: formatBytes(trafficData.ipv4.egressSource),
- icon: faNetworkWired,
- iconColor: 'text-red-600',
- bgColor: 'bg-red-50'
- },
- {
- title: 'IPv6 Ingress Traffic',
- value: formatBytes(trafficData.ipv6.ingressSource),
- icon: faGlobe,
- iconColor: 'text-green-600',
- bgColor: 'bg-green-50'
- },
- {
- title: 'IPv6 Egress Traffic',
- value: formatBytes(trafficData.ipv6.egressSource),
- icon: faNetworkWired,
- iconColor: 'text-yellow-600',
- bgColor: 'bg-yellow-50'
- }
- ]
-
- if (isLoading) {
- return (
- <>
- Dashboard - NetGuardia
-
-
-
- >
- )
- }
-
- if (connectionStatus === 'error') {
- return (
- <>
- Dashboard - NetGuardia
-
-
-
-
-
-
-
Connection Error
-
Unable to establish WebSocket connection. Please check your network status.
-
window.location.reload()}
- className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
- >
- Reload
-
-
-
-
- >
- )
- }
-
- return (
- <>
-
- Dashboard - NetGuardia
-
-
-
-
- {/* 頁面標題 */}
-
-
-
-
- Network Traffic Monitoring
-
-
- Real-time monitoring of IPv4 and IPv6 network traffic status
-
-
-
-
- {/* API 時間範圍控制 */}
-
-
- Time Range:
- setApiTimeRange(e.target.value)}
- className="text-sm border border-gray-300 rounded px-3 py-1 bg-white focus:outline-none focus:border-blue-500"
- >
- 1 Minute
- 10 Minutes
- 1 Hour
-
-
-
-
-
-
-
-
- {/* 統計卡片 */}
-
- {statsCards.map((card, index) => (
-
-
-
-
- {card.title}
-
-
- {card.value}
-
-
-
-
-
-
-
- ))}
-
-
- {/* 圖表區域 */}
-
-
-
-
-
IPv4 Network Usage Trends
-
-
-
-
-
-
-
-
IPv6 Network Usage Trends
-
-
-
-
-
- {/* 攻擊統計圓餅圖 */}
-
-
-
-
-
Attack Type Distribution
-
-
-
-
-
-
-
-
Attack Protocol Distribution
-
-
-
-
-
- >
- )
-}
-
-export default Dashboard
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/detection.tsx b/mantis-frontend/src/pages/detection.tsx
deleted file mode 100644
index ad7ce42..0000000
--- a/mantis-frontend/src/pages/detection.tsx
+++ /dev/null
@@ -1,856 +0,0 @@
-import React, { useContext, useEffect, useState, useCallback, useMemo, useRef } from 'react'
-import Head from 'next/head'
-import { motion, AnimatePresence } from 'framer-motion'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import {
- faRobot,
- faShieldAlt,
- faExclamationTriangle,
- faFilter,
- faCheck,
- faGlobe,
- faTimes,
- faClock,
- faArrowRight,
- faEye,
- faList,
- faPause,
- faPlay,
-} from '@fortawesome/free-solid-svg-icons'
-import { WebsocketContext } from '../providers/WebSocketProvider'
-import { useTheme } from '../providers/ThemeProvider'
-import { putData } from '../utils/connectionUtils'
-import { urls } from '../config'
-import Layout from '../components/Layout'
-import LoadingSpinner from '../components/LoadingSpinner'
-
-// ---------------------------------------------------------------------------
-// Types
-// ---------------------------------------------------------------------------
-
-type AlertSource = 'ml' | 'rule' | 'fusion'
-type AlertSeverity = 'high' | 'critical'
-
-interface UnifiedAlert {
- timestamp: number
- flow_key: string
- src_ip: string
- dst_ip: string
- src_port: number
- dst_port: number
- protocol: number
- source: AlertSource
- severity: AlertSeverity
- is_attack: boolean
- attack_type: string | null
- confidence: number
- ae_score: number
- rule_sid: number | null
- rule_msg: string | null
-}
-
-interface PriorityInfo {
- color: string
- bgColor: string
- label: string
-}
-
-interface NotificationProps {
- message: string
- type: 'success' | 'error' | 'warning' | 'info'
- onClose: () => void
-}
-
-interface AlertDetailsProps {
- log: UnifiedAlert
- onClose: () => void
- showNotification: (message: string, type: 'success' | 'error' | 'warning' | 'info') => void
-}
-
-interface AlertItemProps {
- log: UnifiedAlert
- index: number
- onClick: (index: number) => void
-}
-
-interface FilterButtonProps {
- isActive: boolean
- onClick: () => void
- children: React.ReactNode
- className?: string
- activeClassName?: string
- activeHoverClassName?: string
-}
-
-// ---------------------------------------------------------------------------
-// Constants & helpers
-// ---------------------------------------------------------------------------
-
-const UPDATE_INTERVALS = [
- { label: 'immediate', value: 0 },
- { label: '1s', value: 1000 },
- { label: '3s', value: 3000 },
- { label: '5s', value: 5000 },
- { label: '10s', value: 10000 },
-]
-
-const SOURCE_CONFIG: Record = {
- ml: { label: 'ML', color: '#7c3aed', bgColor: 'rgba(124,58,237,0.1)' },
- rule: { label: 'Rule', color: '#0284c7', bgColor: 'rgba(2,132,199,0.1)' },
- fusion: { label: 'Fusion', color: '#0f766e', bgColor: 'rgba(15,118,110,0.1)' },
-}
-
-const SEVERITY_CONFIG: Record = {
- critical: { color: '#e53e3e', bgColor: 'rgba(239,68,68,0.1)', label: 'Critical' },
- high: { color: '#dd6b20', bgColor: 'rgba(251,146,60,0.1)', label: 'High' },
-}
-
-const getProtocolName = (protocol: number): string => {
- switch (protocol) {
- case 1: return 'ICMP'
- case 6: return 'TCP'
- case 17: return 'UDP'
- case 47: return 'GRE'
- case 50: return 'ESP'
- case 51: return 'AH'
- case 58: return 'ICMPv6'
- default: return `Proto ${protocol}`
- }
-}
-
-const formatTimestamp = (timestamp: number): string => {
- if (!timestamp) return 'N/A'
- return new Date(timestamp * 1000).toLocaleString('en-GB', {
- year: 'numeric', month: '2-digit', day: '2-digit',
- hour: '2-digit', minute: '2-digit', second: '2-digit',
- })
-}
-
-const parseAlertData = (rawData: any): UnifiedAlert | null => {
- try {
- const data = typeof rawData === 'string' ? JSON.parse(rawData) : rawData
- if (!data || typeof data !== 'object') return null
-
- const source: AlertSource = data.source === 'ml' || data.source === 'rule' || data.source === 'fusion'
- ? data.source : 'ml'
- const severity: AlertSeverity = data.severity === 'critical' ? 'critical' : 'high'
-
- return {
- timestamp: data.timestamp ?? 0,
- flow_key: data.flow_key ?? '',
- src_ip: data.src_ip ?? 'Unknown',
- dst_ip: data.dst_ip ?? 'Unknown',
- src_port: data.src_port ?? 0,
- dst_port: data.dst_port ?? 0,
- protocol: data.protocol ?? 0,
- source,
- severity,
- is_attack: data.is_attack ?? false,
- attack_type: data.attack_type ?? null,
- confidence: data.confidence ?? 0,
- ae_score: data.ae_score ?? 0,
- rule_sid: data.rule_sid ?? null,
- rule_msg: data.rule_msg ?? null,
- }
- } catch {
- return null
- }
-}
-
-// ---------------------------------------------------------------------------
-// Notification
-// ---------------------------------------------------------------------------
-
-const Notification: React.FC = ({ message, type, onClose }) => {
- useEffect(() => {
- const timer = setTimeout(onClose, 4000)
- return () => clearTimeout(timer)
- }, [onClose])
-
- const typeStyles = {
- success: 'bg-green-100 border-green-400 text-green-700',
- error: 'bg-red-100 border-red-400 text-red-700',
- warning: 'bg-yellow-100 border-yellow-400 text-yellow-700',
- info: 'bg-blue-100 border-blue-400 text-blue-700',
- }
-
- return (
-
-
- {message}
- ×
-
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// AlertDetails
-// ---------------------------------------------------------------------------
-
-const AlertDetails: React.FC = ({ log, onClose, showNotification }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
- const priorityInfo = SEVERITY_CONFIG[log.severity]
- const sourceInfo = SOURCE_CONFIG[log.source]
-
- const isIPv6Source = log.src_ip.includes(':') && !log.src_ip.includes('.')
- const isIPv6Dest = log.dst_ip.includes(':') && !log.dst_ip.includes('.')
-
- const handleBlockIP = useCallback((ip: string, port: number, isSource: boolean, blockAllPorts = false) => {
- const isIPv6 = isSource ? isIPv6Source : isIPv6Dest
- const formattedIp = isIPv6 ? `[${ip}]` : ip
- const url = isIPv6 ? urls.access_control.ipv6.black_list : urls.access_control.ipv4.black_list
- const portToSend = blockAllPorts ? '0' : String(port)
- const displayPort = blockAllPorts ? '*' : String(port)
-
- putData(
- url,
- `${formattedIp}:${portToSend}`,
- () => showNotification(`Successfully added to blacklist: ${ip}:${displayPort}`, 'success'),
- (error) => showNotification(`Failed to add to blacklist: ${error.message}`, 'error'),
- )
- }, [isIPv6Source, isIPv6Dest, showNotification])
-
- const handleAllowIP = useCallback((ip: string, port: number, isSource: boolean, allowAllPorts = false) => {
- const isIPv6 = isSource ? isIPv6Source : isIPv6Dest
- const formattedIp = isIPv6 ? `[${ip}]` : ip
- const url = isIPv6 ? urls.access_control.ipv6.white_list : urls.access_control.ipv4.white_list
- const portToSend = allowAllPorts ? '0' : String(port)
- const displayPort = allowAllPorts ? '*' : String(port)
-
- putData(
- url,
- `${formattedIp}:${portToSend}`,
- () => showNotification(`Successfully added to whitelist: ${ip}:${displayPort}`, 'success'),
- (error) => showNotification(`Failed to add to whitelist: ${error.message}`, 'error'),
- )
- }, [isIPv6Source, isIPv6Dest, showNotification])
-
- return (
-
-
- {/* Header */}
-
-
-
- Alert Details
-
-
-
-
-
-
- {/* Basic info */}
-
-
- Timestamp:
- {formatTimestamp(log.timestamp)}
-
-
- Severity:
-
-
- {priorityInfo.label}
-
-
-
- Source:
-
- {sourceInfo.label}
-
-
-
- Attack Type:
- {log.attack_type ?? '—'}
-
-
- Protocol:
- {getProtocolName(log.protocol)}
-
-
- Is Attack:
-
- {log.is_attack ? 'Yes' : 'No'}
-
-
-
-
- {/* ML scores — only when source is ml or fusion */}
- {(log.source === 'ml' || log.source === 'fusion') && (
-
-
ML Score Details:
-
- {[
- { label: 'Confidence', value: log.confidence },
- { label: 'AE Score', value: log.ae_score },
- ].map(({ label, value }) => (
-
- {label}:
- = 1.0 ? 'text-red-500' :
- value >= 0.5 ? 'text-orange-500' :
- value >= 0.1 ? 'text-yellow-500' :
- 'text-green-500'
- }`}>
- {value.toFixed(4)}
-
-
- ))}
-
-
- )}
-
- {/* Rule details — only when source is rule or fusion */}
- {(log.source === 'rule' || log.source === 'fusion') && (log.rule_sid !== null || log.rule_msg !== null) && (
-
-
Rule Match:
-
- {log.rule_sid !== null && (
-
- SID:
- {log.rule_sid}
-
- )}
- {log.rule_msg !== null && (
-
- Message:
- {log.rule_msg}
-
- )}
-
-
- )}
-
- {/* Connection details */}
-
-
Connection Details
-
-
-
Source
-
{log.src_ip}
-
Port: {log.src_port}
-
-
-
-
Destination
-
{log.dst_ip}
-
Port: {log.dst_port}
-
-
-
-
- {/* IP management */}
-
-
IP Management
-
- {[
- { label: 'Source IP', ip: log.src_ip, port: log.src_port, isSource: true, bgClass: isDark ? 'bg-blue-900/30' : 'bg-blue-50' },
- { label: 'Destination IP', ip: log.dst_ip, port: log.dst_port, isSource: false, bgClass: isDark ? 'bg-red-900/30' : 'bg-red-50' },
- ].map(({ label, ip, port, isSource, bgClass }) => (
-
-
{label}: {ip}
-
- handleBlockIP(ip, port, isSource)}
- >
-
- Block Port {port}
-
- handleBlockIP(ip, port, isSource, true)}
- >
-
- Block All Ports
-
- handleAllowIP(ip, port, isSource)}
- >
-
- Allow Port {port}
-
- handleAllowIP(ip, port, isSource, true)}
- >
-
- Allow All Ports
-
-
-
- ))}
-
-
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// AlertItem
-// ---------------------------------------------------------------------------
-
-const AlertItem: React.FC = ({ log, index, onClick }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
- const priorityInfo = SEVERITY_CONFIG[log.severity]
- const sourceInfo = SOURCE_CONFIG[log.source]
-
- const handleClick = useCallback(() => onClick(index), [index, onClick])
-
- return (
-
-
-
-
-
-
-
-
- {formatTimestamp(log.timestamp)}
-
-
-
- {sourceInfo.label}
-
- {log.attack_type && (
-
- {log.attack_type}
-
- )}
-
-
-
- {log.src_ip}:{log.src_port}
-
- {log.dst_ip}:{log.dst_port}
- ({getProtocolName(log.protocol)})
-
-
-
- {priorityInfo.label}
-
- {(log.source === 'ml' || log.source === 'fusion') && (
-
- {log.confidence.toFixed(4)}
-
- )}
- {(log.source === 'rule' || log.source === 'fusion') && log.rule_sid !== null && (
-
- SID {log.rule_sid}
-
- )}
-
-
-
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// FilterButton
-// ---------------------------------------------------------------------------
-
-const FilterButton: React.FC = ({
- isActive, onClick, children,
- className = '', activeClassName = '', activeHoverClassName = '',
- }) => {
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- return (
-
- {children}
-
- )
-}
-
-// ---------------------------------------------------------------------------
-// Main page
-// ---------------------------------------------------------------------------
-
-const Detection: React.FC = () => {
- const { getDetectionAlertStream } = useContext(WebsocketContext)
- const { actualTheme } = useTheme()
- const isDark = actualTheme === 'dark'
-
- const [logs, setLogs] = useState([])
- const [selectedLogIndex, setSelectedLogIndex] = useState(null)
- const [severityFilter, setSeverityFilter] = useState('all')
- const [sourceFilter, setSourceFilter] = useState('all')
- const [notification, setNotification] = useState<{ message: string; type: 'success' | 'error' | 'warning' | 'info' } | null>(null)
- const [isLoading, setIsLoading] = useState(true)
- const [isPaused, setIsPaused] = useState(false)
- const [updateInterval, setUpdateInterval] = useState(0)
- const [lastUpdateTime, setLastUpdateTime] = useState(null)
-
- const bufferRef = useRef([])
- const updateIntervalRef = useRef(updateInterval)
- const isPausedRef = useRef(isPaused)
-
- useEffect(() => { updateIntervalRef.current = updateInterval }, [updateInterval])
- useEffect(() => { isPausedRef.current = isPaused }, [isPaused])
-
- const showNotification = useCallback((message: string, type: 'success' | 'error' | 'warning' | 'info' = 'success') => {
- setNotification({ message, type })
- }, [])
-
- const closeNotification = useCallback(() => setNotification(null), [])
-
- const flushBuffer = useCallback(() => {
- if (bufferRef.current.length === 0) return
- const newEntries = [...bufferRef.current]
- bufferRef.current = []
- setLogs(prev => [...newEntries, ...prev].slice(0, 200))
- setLastUpdateTime(new Date())
- }, [])
-
- useEffect(() => {
- if (updateInterval === 0 || isPaused) return
- const timer = setInterval(flushBuffer, updateInterval)
- return () => clearInterval(timer)
- }, [updateInterval, isPaused, flushBuffer])
-
- // Reads isPaused via ref so this callback never changes, avoiding WS re-subscription on pause toggle
- const processNewLogData = useCallback((rawData: any) => {
- if (isPausedRef.current) return
- const alert = parseAlertData(rawData)
- if (!alert) return
-
- if (updateIntervalRef.current === 0) {
- setLogs(prev => [alert, ...prev.slice(0, 199)])
- setLastUpdateTime(new Date())
- } else {
- bufferRef.current.push(alert)
- }
- }, [])
-
- useEffect(() => {
- if (!getDetectionAlertStream) {
- setIsLoading(false)
- return
- }
-
- const loadingTimeout = setTimeout(() => setIsLoading(false), 3000)
-
- const subscription = getDetectionAlertStream().subscribe({
- next: (data: any) => {
- clearTimeout(loadingTimeout)
- processNewLogData(data)
- setIsLoading(false)
- },
- error: () => {
- clearTimeout(loadingTimeout)
- setIsLoading(false)
- },
- })
-
- return () => {
- clearTimeout(loadingTimeout)
- subscription.unsubscribe()
- }
- }, [getDetectionAlertStream, processNewLogData])
-
- const filteredLogs = useMemo(() => {
- return logs.filter(log => {
- const severityOk = severityFilter === 'all' || log.severity === severityFilter
- const sourceOk = sourceFilter === 'all' || log.source === sourceFilter
- return severityOk && sourceOk
- })
- }, [logs, severityFilter, sourceFilter])
-
- const stats = useMemo(() => ({
- total: logs.length,
- critical: logs.filter(l => l.severity === 'critical').length,
- high: logs.filter(l => l.severity === 'high').length,
- ml: logs.filter(l => l.source === 'ml').length,
- rule: logs.filter(l => l.source === 'rule').length,
- fusion: logs.filter(l => l.source === 'fusion').length,
- uniqueIPs: new Set([...logs.map(l => l.src_ip), ...logs.map(l => l.dst_ip)]).size,
- }), [logs])
-
- const handleAlertClick = useCallback((index: number) => setSelectedLogIndex(index), [])
- const handleCloseDetails = useCallback(() => setSelectedLogIndex(null), [])
- const togglePause = useCallback(() => setIsPaused(p => !p), [])
-
- if (isLoading) {
- return (
- <>
- Detection - NetGuardia
-
-
-
-
-
- >
- )
- }
-
- return (
- <>
-
- Detection - NetGuardia
-
-
-
-
-
- {notification && (
-
- )}
-
-
- {/* Page header */}
-
-
-
- Threat Detection
-
-
- Real-time alerts from ML inference, rule matching, and fusion engine
-
-
-
- {/* Stats cards */}
-
- {[
- { label: 'Total', value: stats.total, icon: faList, bg: 'bg-blue-500' },
- { label: 'Critical', value: stats.critical, icon: faExclamationTriangle, bg: 'bg-red-500' },
- { label: 'High', value: stats.high, icon: faExclamationTriangle, bg: 'bg-orange-500' },
- { label: 'ML', value: stats.ml, icon: faRobot, bg: 'bg-purple-500' },
- { label: 'Rule', value: stats.rule, icon: faShieldAlt, bg: 'bg-sky-500' },
- { label: 'Fusion', value: stats.fusion, icon: faShieldAlt, bg: 'bg-teal-500' },
- { label: 'Unique IPs', value: stats.uniqueIPs, icon: faGlobe, bg: 'bg-indigo-500' },
- ].map(({ label, value, icon, bg }, i) => (
-
-
-
- ))}
-
-
- {/* Controls & filters */}
-
- {/* System controls */}
-
-
-
- Update Interval:
- setUpdateInterval(Number(e.target.value))}
- className={`px-3 py-1 border rounded-md text-sm focus:outline-none focus:border-blue-500 ${
- isDark ? 'bg-gray-600 border-gray-500 text-gray-300' : 'bg-white border-gray-300 text-gray-700'
- }`}
- >
- {UPDATE_INTERVALS.map(i => (
- {i.label}
- ))}
-
-
-
-
-
- {isPaused ? 'Resume' : 'Pause'}
-
-
- {lastUpdateTime && (
-
- Last Update: {lastUpdateTime.toLocaleTimeString()}
-
- )}
-
-
- {/* Severity filter */}
-
- Severity:
- { setSeverityFilter('all'); setSelectedLogIndex(null) }}>
- All
-
- { setSeverityFilter('critical'); setSelectedLogIndex(null) }}
- activeClassName="bg-red-600 text-white shadow-lg" activeHoverClassName="hover:bg-red-700"
- >
- Critical ({stats.critical})
-
- { setSeverityFilter('high'); setSelectedLogIndex(null) }}
- activeClassName="bg-orange-600 text-white shadow-lg" activeHoverClassName="hover:bg-orange-700"
- >
- High ({stats.high})
-
-
-
- {/* Source filter */}
-
- Source:
- { setSourceFilter('all'); setSelectedLogIndex(null) }}>
- All
-
- { setSourceFilter('ml'); setSelectedLogIndex(null) }}
- activeClassName="bg-purple-600 text-white shadow-lg" activeHoverClassName="hover:bg-purple-700"
- >
- ML ({stats.ml})
-
- { setSourceFilter('rule'); setSelectedLogIndex(null) }}
- activeClassName="bg-sky-600 text-white shadow-lg" activeHoverClassName="hover:bg-sky-700"
- >
- Rule ({stats.rule})
-
- { setSourceFilter('fusion'); setSelectedLogIndex(null) }}
- activeClassName="bg-teal-600 text-white shadow-lg" activeHoverClassName="hover:bg-teal-700"
- >
- Fusion ({stats.fusion})
-
-
-
-
- {/* Main content */}
-
-
-
-
-
-
-
- Security Alerts
-
-
- {filteredLogs.length} Alerts
-
-
-
-
-
- {filteredLogs.length === 0 ? (
-
-
-
- {logs.length === 0 ? 'No Alerts Detected' : 'No Matching Alerts'}
-
-
- {logs.length === 0
- ? 'Alerts will appear here when threats are detected'
- : 'Try adjusting filter criteria'}
-
-
- ) : (
-
- {filteredLogs.map((log, index) => (
-
- ))}
-
- )}
-
-
-
-
- {selectedLogIndex !== null && filteredLogs[selectedLogIndex] && (
-
-
-
- )}
-
-
- >
- )
-}
-
-export default Detection
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/index.tsx b/mantis-frontend/src/pages/index.tsx
deleted file mode 100644
index 536811f..0000000
--- a/mantis-frontend/src/pages/index.tsx
+++ /dev/null
@@ -1,1472 +0,0 @@
-// src/pages/index.tsx
-import Head from 'next/head'
-import {motion, AnimatePresence} from 'framer-motion'
-import {useEffect, useState, useContext, useMemo, useCallback} from 'react'
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
-import {
- faShieldAlt,
- faNetworkWired,
- faServer,
- faChartLine,
- faExclamationTriangle,
- faGlobe,
- faEye,
- faFireAlt,
- faRobot,
- faClock,
- faArrowUp,
- faArrowDown,
- faBolt,
- faUsers,
- faDatabase,
- faLock,
- faPlay,
- faMemory,
- faThermometerHalf,
- faWifi,
- faCheckCircle,
- faExclamationCircle,
- faTimesCircle,
- faRefresh,
- faPause,
- faDesktop,
- faWeight,
- faTachometerAlt,
- faTimes,
- faMicrochip,
- faHdd,
- faFire,
- faInfoCircle,
-} from '@fortawesome/free-solid-svg-icons'
-import Layout from '../components/Layout'
-import {WebsocketContext} from '../providers/WebSocketProvider'
-import {useTheme} from '../providers/ThemeProvider'
-import LoadingSpinner from '../components/LoadingSpinner'
-import Link from 'next/link'
-import {
- SystemHealthData,
- HealthMetric,
- SystemDetailModalProps,
- HealthCardProps,
- LoadAverageProps,
- NetworkStatsProps
-} from '../types/HomeTypes'
-
-// 工具函數
-const formatBytes = (bytes: number): string => {
- const units = ['B', 'KB', 'MB', 'GB', 'TB']
- if (bytes === 0) return '0 B'
-
- const i = Math.floor(Math.log(bytes) / Math.log(1024))
- const value = bytes / Math.pow(1024, i)
-
- return `${value.toFixed(1)} ${units[i]}`
-}
-
-const formatUptimeFromSeconds = (seconds: number): string => {
- const days = Math.floor(seconds / (24 * 3600))
- const hours = Math.floor((seconds % (24 * 3600)) / 3600)
- const minutes = Math.floor((seconds % 3600) / 60)
-
- if (days > 0) {
- return `${days} days ${hours} hours ${minutes} minutes`
- } else if (hours > 0) {
- return `${hours} hours ${minutes} minutes`
- } else {
- return `${minutes} minutes`
- }
-}
-
-const getStatusFromValue = (value: number, type: 'cpu' | 'memory' | 'temperature' | 'load'): 'good' | 'warning' | 'critical' => {
- switch (type) {
- case 'cpu':
- if (value < 50) return 'good'
- if (value < 80) return 'warning'
- return 'critical'
- case 'memory':
- if (value < 60) return 'good'
- if (value < 85) return 'warning'
- return 'critical'
- case 'temperature':
- if (value < 45) return 'good'
- if (value < 70) return 'warning'
- return 'critical'
- case 'load':
- if (value < 1.0) return 'good'
- if (value < 2.0) return 'warning'
- return 'critical'
- default:
- return 'good'
- }
-}
-
-const getStatusColor = (status: 'good' | 'warning' | 'critical' | 'unknown') => {
- switch (status) {
- case 'good':
- return {
- color: 'text-green-600',
- bgColor: 'bg-green-100',
- iconColor: 'text-green-500',
- dotColor: 'bg-green-500'
- }
- case 'warning':
- return {
- color: 'text-yellow-600',
- bgColor: 'bg-yellow-100',
- iconColor: 'text-yellow-500',
- dotColor: 'bg-yellow-500'
- }
- case 'critical':
- return {
- color: 'text-red-600',
- bgColor: 'bg-red-100',
- iconColor: 'text-red-500',
- dotColor: 'bg-red-500'
- }
- default:
- return {
- color: 'text-gray-600',
- bgColor: 'bg-gray-100',
- iconColor: 'text-gray-500',
- dotColor: 'bg-gray-500'
- }
- }
-}
-
-// 系統詳情模態框組件
-const SystemDetailModal: React.FC = ({ isOpen, onClose, systemHealth, type }) => {
- const {actualTheme} = useTheme()
- const isDark = actualTheme === 'dark'
-
- if (!isOpen || !systemHealth) return null
-
- const getModalContent = () => {
- switch (type) {
- case 'cpu':
- return {
- title: 'CPU Details',
- icon: faMicrochip,
- color: 'text-blue-600',
- content: (
-
- {/* CPU 基本資訊 */}
-
-
-
- Processor Information
-
-
-
-
Processor model:
-
{systemHealth.cpu_details?.cpu_brand}
-
-
-
Number of cores:
-
{systemHealth.cpu_details?.core_count} cores
-
-
-
-
Base frequency:
-
{systemHealth.cpu_details?.cpu_frequency} MHz
-
-
-
-
Overall usage:
-
{systemHealth.cpu_details?.cpu_usage.toFixed(1)}%
-
-
-
-
-
- {/* 各核心使用率 */}
- {systemHealth.cpu_details?.cores && (
-
-
-
- Usage per core
-
-
- {systemHealth.cpu_details.cores.map((core) => {
- const usage = core.usage_percent
- const status = getStatusFromValue(usage, 'cpu')
- const statusColors = getStatusColor(status)
-
- return (
-
-
-
- Core {core.core_id}
-
-
- {usage.toFixed(1)}%
-
-
-
- {/* 進度條 */}
-
-
-
- {core.frequency} MHz
-
-
- )
- })}
-
-
- )}
-
- {/* 系統負載 */}
- {systemHealth.load_average ? (
-
-
-
- System load average
-
-
- {[
- {label: '1 minute', value: systemHealth.load_average.one_minute},
- {label: '5 minutes', value: systemHealth.load_average.five_minute},
- {label: '15 minutes', value: systemHealth.load_average.fifteen_minute}
- ].map((load) => {
- const status = getStatusFromValue(load.value, 'load')
- const statusColors = getStatusColor(status)
-
- return (
-
-
{load.label}
-
- {load.value.toFixed(2)}
-
-
-
- {status === 'good' ? 'Normal' : status === 'warning' ? 'Warning' : 'Overloaded'}
-
-
- )
- })}
-
-
- ) : (
-
-
-
-
System load data unavailable
-
-
- )}
-
- )
- }
-
- case 'memory':
- const memoryUsed = systemHealth.memory_usage.used
- const memoryTotal = systemHealth.memory_usage.total
- const memoryAvailable = systemHealth.memory_usage.available
- const memoryPercent = systemHealth.memory_usage.usage_percent
- const swapUsed = systemHealth.memory_usage.swap_used
- const swapTotal = systemHealth.memory_usage.swap_total
- const swapPercent = swapTotal > 0 ? (swapUsed / swapTotal) * 100 : 0
-
- return {
- title: 'Memory Details',
- icon: faMemory,
- color: 'text-green-600',
- content: (
-
- {/* 實體記憶體 */}
-
-
-
- Physical Memory (RAM)
-
-
- {/* 記憶體使用率圓餅圖式進度條 */}
-
-
-
- {/* 背景圓環 */}
-
- {/* 進度圓環 */}
-
-
-
-
-
{memoryPercent.toFixed(1)}%
-
-
Used
-
-
-
-
-
-
-
-
-
Total Capacity
-
{formatBytes(memoryTotal)}
-
-
-
Used
-
{formatBytes(memoryUsed)}
-
-
-
Available
-
{formatBytes(memoryAvailable)}
-
-
-
Usage
-
- {memoryPercent.toFixed(1)}%
-
-
-
-
-
- {/* 交換空間 */}
- {swapTotal > 0 && (
-
-
-
- Swap
-
-
-
-
- Usage
- {swapPercent.toFixed(1)}%
-
-
-
-
-
-
-
-
-
Used
-
{formatBytes(swapUsed)}
-
-
-
-
- )}
-
- )
- }
-
- case 'temperature':
- const temp = systemHealth.temperature
- const tempStatus = getStatusFromValue(temp, 'temperature')
- const tempColors = getStatusColor(tempStatus)
- const tempPercent = Math.min((temp / 100) * 100, 100)
-
- return {
- title: 'Temperature Details',
- icon: faThermometerHalf,
- color: 'text-orange-600',
- content: (
-
- {/* 溫度顯示 */}
-
-
-
- Current system temperature
-
-
- {/* 溫度數值顯示 */}
-
-
- {temp.toFixed(1)}°C
-
-
-
-
- {tempStatus === 'good' ? 'Normal temperature' :
- tempStatus === 'warning' ? 'High temperature' : 'Too high a temperature'}
-
-
-
-
- {/* 漸層溫度條 */}
-
- {/* 溫度條容器 */}
-
- {/* 漸層背景 */}
-
-
- {/* 當前溫度進度 */}
-
-
-
- {/* 溫度刻度 */}
-
- {[0, 25, 45, 70, 100].map((value) => (
-
- ))}
-
-
-
-
- {/* 溫度控制建議 */}
-
-
-
-
-
Normal
-
-
0-45°C
-
-
-
-
-
-
Danger
-
-
70-100°C
-
-
-
-
-
- )
- }
- default:
- return {title: '', icon: faServer, color: 'text-gray-600', content: No Data
}
- }
- }
-
- const modalContent = getModalContent()
-
- return (
-
-
- e.stopPropagation()}
- >
- {/* 模態框標題 */}
-
-
-
-
- {modalContent.title}
-
-
-
-
-
-
-
- {/* 模態框內容 */}
-
- {modalContent.content}
-
-
-
-
- )
-}
-
-const HealthCard: React.FC = ({metric, index, systemHealth, onClick}) => {
- const {actualTheme} = useTheme()
- const statusColors = getStatusColor(metric.status)
- const [showTooltip, setShowTooltip] = useState(false)
- const isDark = actualTheme === 'dark'
-
- // 計算進度條百分比
- const getProgressPercentage = (): number => {
- if (typeof metric.value !== 'number') return 0
-
- switch (metric.name) {
- case 'CPU usage':
- case 'Memory usage':
- return metric.value
- case 'System temperature':
- // 溫度以100°C為最大值計算百分比
- return Math.min((metric.value / 100) * 100, 100)
- default:
- return 0
- }
- }
-
- // 獲取詳細資訊
- const getDetailedInfo = (): string => {
- if (!systemHealth) return ''
-
- switch (metric.name) {
- case 'CPU usage':
- return `Processor Load: ${systemHealth.cpu_details?.cpu_usage.toFixed(1)}%\nStatus: ${metric.status === 'good' ? 'Normal' : metric.status === 'warning' ? 'Warning' : 'Critical'}`
- case 'Memory usage':
- return `Total Memory: ${formatBytes(systemHealth.memory_usage.total)}\nUsed: ${formatBytes(systemHealth.memory_usage.used)}\nAvailable Memory: ${formatBytes(systemHealth.memory_usage.available)}\nUsage: ${systemHealth.memory_usage.usage_percent.toFixed(1)}%`
- case 'System temperature':
- return `Current Temperature: ${systemHealth.temperature.toFixed(1)}°C\nStatus: ${metric.status === 'good' ? 'Normal' : metric.status === 'warning' ? 'High' : 'Critical'}`
- default:
- return metric.description || ''
- }
- }
-
- const progressPercentage = getProgressPercentage()
- const hasProgressBar = metric.name.includes('usage') || metric.name.includes('temperature')
- const isClickable = ['CPU Usage', 'Memory Usage', 'System Temperature'].includes(metric.name)
-
- return (
- setShowTooltip(true)}
- onMouseLeave={() => setShowTooltip(false)}
- onClick={isClickable ? onClick : undefined}
- >
-
-
-
-
- {typeof metric.value === 'number' ?
- metric.value.toFixed(metric.name.includes('Usage') || metric.name.includes('Temperature') ? 1 : 0) :
- metric.value
- }
- {metric.unit && {metric.unit} }
-
- {metric.description && (
-
{metric.description}
- )}
-
- {/* 進度條 */}
- {hasProgressBar && (
-
- )}
-
-
-
- {metric.status === 'good' ? 'Normal' :
- metric.status === 'warning' ? 'Warn' :
- metric.status === 'critical' ? 'Urgent' : 'Unknown'}
-
-
-
-
-
-
-
-
- {/* 懸停提示框 */}
- {showTooltip && getDetailedInfo() && !isClickable && (
-
-
{getDetailedInfo()}
- {/* 箭頭 */}
-
-
- )}
-
- )
-}
-
-const LoadAverageDetails: React.FC = ({loadAverage}) => {
- if (!loadAverage) {
- return (
-
-
-
- System Load Average
-
-
-
-
System load data is not available
-
-
- )
- }
-
- const loadMetrics = [
- {
- label: '1 minute',
- value: loadAverage.one_minute,
- color: getStatusFromValue(loadAverage.one_minute, 'load')
- },
- {
- label: '5 minutes',
- value: loadAverage.five_minute,
- color: getStatusFromValue(loadAverage.five_minute, 'load')
- },
- {
- label: '15 minutes',
- value: loadAverage.fifteen_minute,
- color: getStatusFromValue(loadAverage.fifteen_minute, 'load')
- }
- ]
-
- const getLoadDescription = (value: number): string => {
- if (value < 0.5) return 'System Idle'
- if (value < 1.0) return 'Load Normal'
- if (value < 1.5) return 'Load High'
- if (value < 2.0) return 'Load Very High'
- return 'System Overloaded'
- }
-
- const getBarWidth = (value: number): number => {
- // 以 3.0 為最大值來計算進度條寬度
- return Math.min((value / 3.0) * 100, 100)
- }
-
- return (
-
-
-
- System Load Average
-
-
-
- {loadMetrics.map((metric, index) => {
- const statusColors = getStatusColor(metric.color)
- const barWidth = getBarWidth(metric.value)
-
- return (
-
-
-
{metric.label}
-
-
- {metric.value.toFixed(2)}
-
-
- {getLoadDescription(metric.value)}
-
-
-
-
- {/* 進度條 */}
-
-
-
- 0.0
- 1.0
- 2.0
- 3.0+
-
-
- )
- })}
-
-
- )
-}
-
-const NetworkStats: React.FC = ({networkStats}) => {
- const {actualTheme} = useTheme();
- const isDark = actualTheme === 'dark';
- const interfaces = [
- {key: 'ingress', name: 'Ingress Network', icon: faArrowDown, color: 'text-blue-600'},
- {key: 'egress', name: 'Egress Network', icon: faArrowUp, color: 'text-green-600'},
- // {key: 'management', name: 'Management Network', icon: faDesktop, color: 'text-purple-600'}
- ]
-
- return (
-
-
-
- Network Interface Status
-
-
-
- {interfaces.map((iface, index) => {
- const stats = networkStats[iface.key as keyof typeof networkStats]
- const hasErrors = stats.errors_received > 0 || stats.errors_transmitted > 0
-
- return (
-
-
-
-
- {iface.name}
- ({stats.interface})
-
- {hasErrors ? (
-
- ) : (
-
- )}
-
-
-
-
-
Received
-
{formatBytes(stats.bytes_received)}
-
{stats.packets_received.toLocaleString()} packets
-
-
-
-
Transmitted
-
{formatBytes(stats.bytes_transmitted)}
-
{stats.packets_transmitted.toLocaleString()} packets
-
-
-
-
- {hasErrors && (
-
-
- Errors: Received {stats.errors_received}, Transmitted {stats.errors_transmitted}
-
-
- )}
-
- )
- })}
-
-
- )
-}
-
-// 主要組件
-export default function Home() {
- const {actualTheme} = useTheme()
- const {bootTime, getSystemHealthStream} = useContext(WebsocketContext)
- const [currentTime, setCurrentTime] = useState(new Date())
- const [systemHealth, setSystemHealth] = useState(null)
- const [isLoading, setIsLoading] = useState(true)
- const [error, setError] = useState(null)
- const [isPaused, setIsPaused] = useState(false)
- const [lastUpdateTime, setLastUpdateTime] = useState(null)
- const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'error'>('connecting')
-
- const isDark = actualTheme === 'dark'
-
- // 模態框狀態
- const [modalState, setModalState] = useState<{
- isOpen: boolean
- type: 'cpu' | 'memory' | 'temperature' | null
- }>({
- isOpen: false,
- type: null
- })
-
- // 更新當前時間
- useEffect(() => {
- const timer = setInterval(() => {
- setCurrentTime(new Date())
- }, 1000)
-
- return () => clearInterval(timer)
- }, [])
-
- const processSystemHealthData = useCallback((rawData: any) => {
- if (isPaused) return
-
- try {
- // 解析 WebSocket 資料
- const parsedData = typeof rawData === 'object' ? rawData : JSON.parse(rawData)
-
- // 驗證必要欄位
- if (parsedData && typeof parsedData === 'object') {
- const healthData: SystemHealthData = {
- timestamp: parsedData.timestamp || Date.now() / 1000,
- boot_time: parsedData.boot_time,
- uptime_seconds: parsedData.uptime_seconds,
- system_info: {
- kernel_version: parsedData.system_info?.kernel_version || 'Unknown',
- os_name: parsedData.system_info?.os_name || 'Unknown',
- os_version: parsedData.system_info?.os_version || 'Unknown',
- architecture: parsedData.system_info?.architecture || 'Unknown',
- total_processes: parsedData.system_info?.total_processes || 0
- },
- cpu_details: parsedData.cpu_details ? {
- cpu_brand: parsedData.cpu_details.cpu_brand || 'Unknown',
- core_count: parsedData.cpu_details.core_count || 0,
- cpu_usage: parsedData.cpu_details.cpu_usage || 0,
- cpu_frequency: parsedData.cpu_details.cpu_frequency || 0,
- cores: parsedData.cpu_details.cores || []
- } : undefined,
- memory_usage: {
- total: parsedData.memory_usage?.total || 0,
- used: parsedData.memory_usage?.used || 0,
- available: parsedData.memory_usage?.available || 0,
- usage_percent: parsedData.memory_usage?.usage_percent || 0,
- swap_total: parsedData.memory_usage?.swap_total || 0,
- swap_used: parsedData.memory_usage?.swap_used || 0
- },
- network_stats: {
- ingress: parsedData.network_stats?.ingress || {
- interface: 'Unknown',
- bytes_received: 0,
- bytes_transmitted: 0,
- packets_received: 0,
- packets_transmitted: 0,
- errors_received: 0,
- errors_transmitted: 0
- },
- egress: parsedData.network_stats?.egress || {
- interface: 'Unknown',
- bytes_received: 0,
- bytes_transmitted: 0,
- packets_received: 0,
- packets_transmitted: 0,
- errors_received: 0,
- errors_transmitted: 0
- },
- management: parsedData.network_stats?.management || {
- interface: 'Unknown',
- bytes_received: 0,
- bytes_transmitted: 0,
- packets_received: 0,
- packets_transmitted: 0,
- errors_received: 0,
- errors_transmitted: 0
- }
- },
- load_average: parsedData.load_average || null,
- temperature: parsedData.temperature || 0
- }
-
- setSystemHealth(healthData)
- setLastUpdateTime(new Date())
- }
- } catch (error) {
- console.error("Error processing system health data:", error)
- }
- }, [isPaused])
-
- // WebSocket 訂閱效果
- useEffect(() => {
- if (!getSystemHealthStream) {
- setError('WebSocket service unavailable')
- setIsLoading(false)
- return
- }
-
- console.log("Initializing System Health WebSocket subscription...")
- setError(null)
- setIsLoading(true)
-
- // 訂閱系統健康串流
- const subscription = getSystemHealthStream().subscribe({
- next: (data: any) => {
- processSystemHealthData(data)
- setConnectionStatus('connected')
- setIsLoading(false)
- },
- error: (error: any) => {
- console.error("System health WebSocket error:", error)
- setConnectionStatus('error')
- setError('WebSocket connection failed')
- setIsLoading(false)
- }
- })
-
- // 清理函數:只取消訂閱,不關閉 WebSocket 連線
- return () => {
- console.log("Unsubscribing from System Health stream")
- subscription.unsubscribe()
- }
- }, [getSystemHealthStream, processSystemHealthData])
-
- // 暫停/恢復處理
- useEffect(() => {
- if (!isPaused && systemHealth) {
- // 恢復時更新時間戳
- setLastUpdateTime(new Date())
- }
- }, [isPaused, systemHealth])
-
- // 連接狀態初始化(可選,用於顯示連接狀態)
- useEffect(() => {
- if (!getSystemHealthStream) {
- console.warn("WebSocket streams not available")
- setConnectionStatus('error')
- setIsLoading(false)
- return
- }
-
- console.log("WebSocket service available")
- setConnectionStatus('connecting')
- }, [getSystemHealthStream])
-
- // 計算系統健康指標
- const healthMetrics = useMemo((): HealthMetric[] => {
- if (!systemHealth) return []
-
- const cpuUsage = systemHealth.cpu_details?.cpu_usage || 0
- const memoryUsage = systemHealth.memory_usage.usage_percent
- const temperature = systemHealth.temperature
-
- const metrics: HealthMetric[] = [
- {
- name: 'CPU Usage',
- value: cpuUsage,
- unit: '%',
- status: getStatusFromValue(cpuUsage, 'cpu'),
- icon: faMicrochip,
- color: 'bg-blue-500',
- bgColor: 'bg-blue-50',
- description: `Processor Load`
- },
- {
- name: 'Memory Usage',
- value: memoryUsage,
- unit: '%',
- status: getStatusFromValue(memoryUsage, 'memory'),
- icon: faMemory,
- color: 'bg-green-500',
- bgColor: 'bg-green-50',
- description: `Used ${formatBytes(systemHealth.memory_usage.used)} / ${formatBytes(systemHealth.memory_usage.total)}`
- },
- {
- name: 'System Temperature',
- value: temperature,
- unit: '°C',
- status: getStatusFromValue(temperature, 'temperature'),
- icon: faThermometerHalf,
- color: 'bg-orange-500',
- bgColor: 'bg-orange-50',
- description: 'Hardware Temperature'
- }
- ]
-
- return metrics
- }, [systemHealth])
-
- // 處理卡片點擊
- const handleCardClick = useCallback((metricName: string) => {
- let type: 'cpu' | 'memory' | 'temperature' | null = null
-
- switch (metricName) {
- case 'CPU Usage':
- type = 'cpu'
- break
- case 'Memory Usage':
- type = 'memory'
- break
- case 'System Temperature':
- type = 'temperature'
- break
- default:
- return
- }
-
- setModalState({isOpen: true, type})
- }, [])
-
- // 關閉模態框
- const closeModal = useCallback(() => {
- setModalState({isOpen: false, type: null})
- }, [])
-
- // 快速導航項目
- const quickNavigation = [
- {
- title: 'Network Traffic Monitoring',
- description: 'View IPv4/IPv6 traffic statistics and trend analysis',
- icon: faChartLine,
- color: 'bg-blue-500',
- href: '/dashboard',
- badge: 'Real-time'
- },
- {
- title: 'Statistics Analysis',
- description: 'In-depth analysis of network usage patterns and performance metrics',
- icon: faDatabase,
- color: 'bg-green-500',
- href: '/statistics',
- badge: 'Analysis'
- },
- {
- title: 'Access Control',
- description: 'Manage IP whitelists, blacklists, and firewall rules',
- icon: faLock,
- color: 'bg-red-500',
- href: '/access-control',
- badge: 'Security'
- },
- // {
- // title: 'AI Threat Detection',
- // description: 'Intelligent detection of abnormal behavior and potential network threats',
- // icon: faRobot,
- // color: 'bg-purple-500',
- // href: '/ai-detection',
- // badge: 'AI'
- // }
- ]
-
- if (isLoading && !systemHealth) {
- return (
- <>
-
- Dashboard - NetGuardia
-
-
-
-
- >
- )
- }
-
- return (
- <>
-
- NetGuardia - Network Security Monitoring System
-
-
-
-
-
-
- {/* 系統詳情模態框 */}
-
-
- {/* 歡迎標題 */}
-
-
-
-
- Welcome to NetGuardia
-
-
- Network Security Monitoring System - Protecting your network environment with
- comprehensive security monitoring
-
-
-
- {/* 系統狀態控制 */}
-
- {/* 系統狀態指示器 */}
-
-
-
- {error ? 'System Error' : systemHealth ? 'System Normal' : 'Connecting'}
-
-
-
- {/* 暫停/恢復按鈕 */}
-
setIsPaused(!isPaused)}
- className={`px-3 py-1 rounded-lg text-sm font-medium transition-colors ${
- isPaused
- ? "bg-green-100 text-green-700 hover:bg-green-200"
- : "bg-orange-100 text-orange-700 hover:bg-orange-200"
- }`}
- >
-
- {isPaused ? "Resume" : "Pause"}
-
-
- {/* 手動更新按鈕 */}
-
window.location.reload()}
- className="px-3 py-1 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors flex items-center space-x-1"
- >
-
- Update
-
-
- {/* 最後更新時間 */}
- {lastUpdateTime && (
-
- Last Updated: {lastUpdateTime.toLocaleTimeString()}
-
- )}
-
-
-
-
- {/* 錯誤狀態 */}
- {error && (
-
-
-
- System status loading failed: {error}
-
-
- )}
-
- {/* 系統健康狀態卡片 - 可點擊 */}
- {healthMetrics.length > 0 && (
-
- {healthMetrics.map((metric, index) => (
- handleCardClick(metric.name)}
- />
- ))}
-
- )}
-
- {/* 系統詳細資訊 */}
- {systemHealth && (
-
- {/* 系統資訊 */}
-
-
-
- System Information
-
-
-
- {/* 基本系統資訊 */}
-
-
- System
- {systemHealth.system_info.os_name} {systemHealth.system_info.os_version} ({systemHealth.system_info.architecture}, {systemHealth.system_info.kernel_version})
-
- {systemHealth.cpu_details && (
- <>
-
- CPU Model
- {systemHealth.cpu_details.cpu_brand}
-
-
- CPU Cores
- {systemHealth.cpu_details.core_count}
-
-
- CPU Frequency
- {systemHealth.cpu_details.cpu_frequency} MHz
-
- >
- )}
-
- RAM Size
- {formatBytes(systemHealth.memory_usage.total)}
-
-
- System Time
- {new Date(systemHealth.timestamp * 1000).toLocaleString('zh-TW')}
-
- {systemHealth.uptime_seconds && (
-
- Uptime
- {formatUptimeFromSeconds(systemHealth.uptime_seconds)}
-
- )}
-
-
-
-
- )}
-
- {/* 網路狀態和系統負載 */}
- {systemHealth && (
-
- {/* 網路狀態 - 占滿整行 */}
-
-
- )}
-
- {/* 快速導航 */}
-
- Quick
- Navigation
-
- {quickNavigation.map((item, index) => (
-
-
-
-
-
-
-
-
- {item.badge}
-
-
-
- {item.title}
-
-
- {item.description}
-
-
-
-
- ))}
-
-
-
- >
- )
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/statistics.tsx b/mantis-frontend/src/pages/statistics.tsx
deleted file mode 100644
index f162c5c..0000000
--- a/mantis-frontend/src/pages/statistics.tsx
+++ /dev/null
@@ -1,943 +0,0 @@
-import React, { useContext, useState, useEffect, useCallback, useMemo, useRef } from "react";
-import Head from 'next/head';
-import { motion } from 'framer-motion';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- faChartBar,
- faNetworkWired,
- faGlobe,
- faSearch,
- faFilter,
- faSortUp,
- faSortDown,
- faSort,
- faRefresh,
- faDownload,
- faInfoCircle,
- faPause,
- faPlay,
- faClock
-} from '@fortawesome/free-solid-svg-icons';
-import { WebsocketContext } from "../providers/WebSocketProvider";
-import { useTheme } from '../providers/ThemeProvider';
-import Layout from '../components/Layout';
-import LoadingSpinner from '../components/LoadingSpinner';
-import { combineLatest, of } from 'rxjs';
-import { map, catchError, throttleTime, distinctUntilChanged } from 'rxjs/operators';
-import {
- ToggleButtonProps,
- TableHeaderProps,
- UpdateControlProps,
- ControlPanelProps,
- DataTableProps,
- NoDataStateProps,
- StatsOverviewProps
-} from '../types/StatisticsTypes';
-
-// 常量定義
-const TRAFFIC_TYPES = ["source", "destination"];
-const DIRECTIONS = ["ingress", "egress"];
-const TIME_RANGES = ["1min", "10min", "1hour"];
-
-// 更新频率选項 (毫秒)
-const UPDATE_INTERVALS = [
- { label: "immediate", value: 0 },
- { label: "1s", value: 1000 },
- { label: "3s", value: 3000 },
- { label: "5s", value: 5000 },
- { label: "10s", value: 10000 }
-];
-
-// 格式化時間戳
-const formatTimestamp = (timestamp: number | string, bootTime: number | null): string => {
- if (!timestamp || !bootTime) return "Loading...";
- const date = new Date((bootTime + Number(timestamp)) / 1e6);
- date.setHours(date.getHours() + 8); // GMT+8
- return date.toISOString().replace("T", " ").split(".")[0];
-};
-
-// 解析流量資料
-const parseFlowData = (rawData: any): any => {
- try {
- if (typeof rawData === 'object' && rawData !== null) {
- return rawData;
- }
- return JSON.parse(rawData);
- } catch (error) {
- console.error("Error parsing flow data:", error);
- return {};
- }
-};
-
-// 格式化位元組大小
-const formatBytes = (bytes: number): string => {
- const units = ["B", "KB", "MB", "GB", "TB"];
- if (bytes < 1024) return `${bytes} B`;
-
- let i = 0;
- let value = bytes;
-
- while (value >= 1024 && i < units.length - 1) {
- value /= 1024;
- i++;
- }
-
- return `${value.toFixed(2)} ${units[i]}`;
-};
-
-// IP 排序輔助函數
-const getIPSortValue = (ip: string, isIPv6: boolean): any => {
- if (isIPv6) {
- const cleanIPv6 = ip.replace(/\[|\]|:[0-9]+/g, "");
- const expandIPv6 = (ip: string) => {
- const parts = ip.split("::");
- const left = (parts[0] || "").split(":");
- const right = (parts[1] || "").split(":");
- const missingZeros = 8 - (left.length + right.length);
- const zeros = Array(missingZeros).fill("0");
- return [...left, ...zeros, ...right].map((part) => part || "0");
- };
-
- const expanded = expandIPv6(cleanIPv6);
- try {
- const hexString = expanded.reduce((acc, part) => acc + part.padStart(4, "0"), "");
- return BigInt("0x" + hexString);
- } catch (error) {
- console.error("Error converting IPv6 to number:", error);
- return ip;
- }
- } else {
- try {
- return ip.split(".").reduce((acc, octet) => acc * 256 + parseInt(octet, 10), 0);
- } catch (error) {
- console.error("Error converting IPv4 to number:", error);
- return ip;
- }
- }
-};
-
-const ToggleButton: React.FC = ({ isActive, onClick, children, className = "" }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- return (
-
- {children}
-
- );
-};
-
-const TableHeader: React.FC = ({ column, sortConfig, onSort }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- return (
- onSort(column.key)}
- className={`px-4 py-3 text-left text-xs font-medium uppercase tracking-wider cursor-pointer transition-colors ${
- sortConfig.key === column.key
- ? (isDark ? 'bg-blue-900/30 text-blue-300' : 'bg-blue-50 text-gray-500')
- : (isDark ? 'text-gray-400 hover:bg-gray-700' : 'text-gray-500 hover:bg-gray-100')
- }`}
- >
-
- {column.label}
-
-
-
- );
-};
-
-const UpdateControl: React.FC = ({
- updateInterval,
- setUpdateInterval,
- isPaused,
- setIsPaused,
- lastUpdateTime
- }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- return (
-
- {/* 更新频率 */}
-
-
- Update Interval:
- setUpdateInterval(Number(e.target.value))}
- className={`px-3 py-1 border rounded-md text-sm focus:outline-none focus:border-blue-500 ${
- isDark ? 'bg-gray-600 border-gray-500 text-gray-300' : 'bg-white border-gray-300 text-gray-700'
- }`}
- >
- {UPDATE_INTERVALS.map(interval => (
-
- {interval.label}
-
- ))}
-
-
-
- {/* 暂停/恢复按钮 */}
-
setIsPaused(!isPaused)}
- className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
- isPaused
- ? "bg-green-100 text-green-700 hover:bg-green-200"
- : "bg-orange-100 text-orange-700 hover:bg-orange-200"
- }`}
- >
-
- {isPaused ? "resume" : "pause"}
-
-
- {/* 更新資料按钮 */}
-
window.location.reload()}
- className="px-3 py-1 bg-blue-600 text-white rounded-md text-sm font-medium hover:bg-blue-700 transition-colors flex items-center space-x-1"
- >
-
- Update
-
-
- {/* 最后更新时间 */}
- {lastUpdateTime && (
-
- Last Update: {lastUpdateTime.toLocaleTimeString()}
-
- )}
-
- );
-};
-
-const ControlPanel: React.FC = ({
- isIPv6, setIsIPv6,
- direction, setDirection,
- trafficType, setTrafficType,
- timeRange, setTimeRange,
- searchTerm, setSearchTerm,
- updateInterval, setUpdateInterval,
- isPaused, setIsPaused,
- lastUpdateTime
- }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- return (
-
- {/* 更新控制 */}
-
-
-
- {/* 搜索框 */}
-
-
-
- setSearchTerm(e.target.value)}
- placeholder="Search IP Address..."
- className={`w-full pl-8 pr-3 py-1 text-sm border rounded-lg focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 ${
- isDark ? 'bg-gray-700 border-gray-500 text-gray-300 placeholder-gray-400' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'
- }`}
- />
-
-
-
- {/* IP Version Selection */}
-
-
IP Version:
-
- setIsIPv6(false)}
- >
- IPv4
-
- setIsIPv6(true)}
- >
- IPv6
-
-
-
-
- {/* 流量方向 */}
-
-
Direction:
-
- setDirection("ingress")}
- >
- Ingress
-
- setDirection("egress")}
- >
- Egress
-
-
-
-
- {/* 流量類型 */}
-
-
Traffic Type:
-
- setTrafficType("source")}
- >
- Source
-
- setTrafficType("destination")}
- >
- Destination
-
-
-
-
- {/* 時間範圍 */}
-
-
Time:
-
- {TIME_RANGES.map((range) => (
- setTimeRange(range)}
- >
- {range}
-
- ))}
-
-
-
-
- );
-};
-
-const DataTable: React.FC = ({ data, sortConfig, handleSort, bootTime, isUpdating }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- const columns = [
- { key: "ip", label: "IP Address" },
- { key: "geo", label: "Location" },
- { key: "bytes", label: "Bytes" },
- { key: "packets", label: "Packets" },
- { key: "last_seen", label: "Last Seen (GMT+8)" }
- ];
-
- const formatGeoLocation = (geo: any) => {
- if (!geo) {
- return (
-
- Unknown
-
- );
- }
-
- return (
-
-
- {geo.country_code || 'Unknown'}
-
-
- );
- };
-
- return (
-
- {/* 表格容器 - 设置最大高度和滚动 */}
-
-
- {/* 固定表头 */}
-
-
- {columns.map((column) => (
-
- ))}
-
-
-
- {data.map((row, index) => (
-
-
- {row.ip}
-
-
- {formatGeoLocation(row.geo)}
-
-
- {formatBytes(row.bytes)}
-
-
- {row.packets.toLocaleString()}
-
-
- {formatTimestamp(row.last_seen, bootTime)}
-
-
- ))}
-
-
-
-
- {/* 資料条数显示 */}
- {data.length > 0 && (
-
-
- Showing {data.length} records
-
-
- )}
-
- );
-};
-
-const NoDataState: React.FC = ({ isSearchFiltered }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- return (
-
-
- {isSearchFiltered ? (
- <>
- No matching results found
- Please try adjusting your search criteria
- >
- ) : (
- <>
- No data found
- Please try adjusting your filters or time range
- >
- )}
-
- );
-};
-
-const StatsOverview: React.FC = ({ data, isIPv6, direction, trafficType }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- const totalBytes = data.reduce((sum, item) => sum + item.bytes, 0);
- const totalPackets = data.reduce((sum, item) => sum + item.packets, 0);
- const uniqueIPs = new Set(
- data.map(item => {
- // 移除端口部分,只保留IP
- if (item.ip.includes('[') && item.ip.includes(']:')) {
- // IPv6: [ip]:port -> ip
- return item.ip.split(']:')[0] + ']';
- } else if (item.ip.includes(':') && !item.ip.includes('[')) {
- // IPv4: ip:port -> ip
- return item.ip.split(':').slice(0, -1).join(':');
- }
- return item.ip;
- })
- ).size;
-
- const stats = [
- {
- title: 'Total Traffic',
- value: formatBytes(totalBytes),
- icon: faNetworkWired,
- color: 'bg-blue-500'
- },
- {
- title: 'Total Packets',
- value: totalPackets.toLocaleString(),
- icon: faChartBar,
- color: 'bg-green-500'
- },
- {
- title: 'Unique IPs',
- value: uniqueIPs.toLocaleString(),
- icon: faGlobe,
- color: 'bg-purple-500'
- }
- ];
-
- return (
-
- {stats.map((stat, index) => (
-
-
-
-
- {stat.title}
-
-
- {stat.value}
-
-
-
-
-
-
-
- ))}
-
- );
-};
-
-// 主要統計組件
-const Statistics: React.FC = () => {
- const { bootTime, getIPv4FlowStream, getIPv6FlowStream } = useContext(WebsocketContext);
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- // 狀態管理
- const [isIPv6, setIsIPv6] = useState(false);
- const [trafficType, setTrafficType] = useState("source");
- const [direction, setDirection] = useState("ingress");
- const [timeRange, setTimeRange] = useState("1min");
- const [sortConfig, setSortConfig] = useState({ key: null, direction: "desc" });
- const [searchTerm, setSearchTerm] = useState("");
- const [isLoading, setIsLoading] = useState(true);
- const [flowData, setFlowData] = useState({});
- const [error, setError] = useState(null);
-
- // 更新控制狀態
- const [updateInterval, setUpdateInterval] = useState(3000);
- const [isPaused, setIsPaused] = useState(false);
- const [lastUpdateTime, setLastUpdateTime] = useState(null);
- const [isUpdating, setIsUpdating] = useState(false);
-
- // 用於存儲最新資料和控制更新的 refs
- const latestDataRef = useRef({});
- const lastUpdateRef = useRef(0);
- const updateIntervalRef = useRef(updateInterval);
- const isPausedRef = useRef(isPaused);
-
- // 同步 refs 與 state
- useEffect(() => {
- updateIntervalRef.current = updateInterval;
- }, [updateInterval]);
-
- useEffect(() => {
- isPausedRef.current = isPaused;
- }, [isPaused]);
-
- // 解析流量資料 - 使用 useCallback 保持穩定引用
- const parseFlowData = useCallback((rawData: any): any => {
- try {
- if (typeof rawData === 'object' && rawData !== null) {
- return rawData;
- }
- return JSON.parse(rawData);
- } catch (error) {
- console.error("Error parsing flow data:", error);
- return {};
- }
- }, []);
-
- // 處理 WebSocket 資料 - 使用 useCallback 保持穩定引用
- const processWebSocketData = useCallback((results: any[]) => {
- const validResults = results.filter(result => result !== null);
- if (validResults.length === 0) return;
-
- const newData: any = {};
- validResults.forEach(result => {
- if (!result) return;
- const { key, protocol, data } = result;
- if (!newData[key]) {
- newData[key] = {};
- }
- newData[key][protocol] = data;
- });
-
- if (Object.keys(newData).length > 0) {
- latestDataRef.current = newData;
-
- // 根據當前狀態決定是否立即更新 UI
- const now = Date.now();
- const currentUpdateInterval = updateIntervalRef.current;
- const currentIsPaused = isPausedRef.current;
-
- const shouldUpdate = !currentIsPaused &&
- (currentUpdateInterval === 0 || (now - lastUpdateRef.current) >= currentUpdateInterval);
-
- if (shouldUpdate) {
- setIsUpdating(true);
- setFlowData(newData);
- setLastUpdateTime(new Date());
- lastUpdateRef.current = now;
- setTimeout(() => setIsUpdating(false), 300);
- }
- }
- }, []);
-
- useEffect(() => {
- if (!getIPv4FlowStream || !getIPv6FlowStream) {
- setError("WebSocket service unavailable");
- setIsLoading(false);
- return;
- }
-
- console.log("Initializing Statistics WebSocket subscriptions...");
- setError(null);
- setIsLoading(true);
-
- // 使用循環動態生成所有配置
- const DIRECTIONS = ['ingress', 'egress'] as const;
- const TRAFFIC_TYPES = ['source', 'destination'] as const;
- const TIME_RANGES = ['1min', '10min', '1hour'] as const;
-
- const streamConfigs: Array<{
- dir: typeof DIRECTIONS[number];
- type: typeof TRAFFIC_TYPES[number];
- range: typeof TIME_RANGES[number];
- protocol: 'ipv4' | 'ipv6';
- }> = [];
-
- // 生成所有組合 (2 directions × 2 types × 3 ranges × 2 protocols = 24 configs)
- DIRECTIONS.forEach(dir => {
- TRAFFIC_TYPES.forEach(type => {
- TIME_RANGES.forEach(range => {
- streamConfigs.push({ dir, type, range, protocol: "ipv4" });
- streamConfigs.push({ dir, type, range, protocol: "ipv6" });
- });
- });
- });
-
- console.log(`Total stream configs: ${streamConfigs.length}`); // 應該是 24
-
- // 創建所有資料流的 Observable
- const streams = streamConfigs.map(config => {
- const { dir, type, range, protocol } = config;
- const getStream = protocol === "ipv4" ? getIPv4FlowStream : getIPv6FlowStream;
-
- return getStream(dir, type, range).pipe(
- map((rawData: any) => ({
- key: `${dir}_${type}_${range}`,
- protocol,
- data: parseFlowData(rawData)
- })),
- catchError(error => {
- console.error(`Error in ${protocol} stream (${dir}, ${type}, ${range}):`, error);
- return of(null);
- })
- );
- });
-
- // 訂閱合併的資料流
- const subscription = combineLatest(streams).subscribe({
- next: (results) => {
- processWebSocketData(results);
- setIsLoading(false);
- },
- error: (error) => {
- console.error("WebSocket subscription error:", error);
- setError("WebSocket connection error");
- setIsLoading(false);
- }
- });
-
- // 清理函數:只取消訂閱,不關閉 WebSocket 連線
- return () => {
- console.log("Unsubscribing from Statistics WebSocket streams");
- subscription.unsubscribe();
- };
- }, [getIPv4FlowStream, getIPv6FlowStream, parseFlowData, processWebSocketData]);
-
- useEffect(() => {
- if (!isPaused && Object.keys(latestDataRef.current).length > 0) {
- // 恢復時立即更新一次
- setFlowData(latestDataRef.current);
- setLastUpdateTime(new Date());
- lastUpdateRef.current = Date.now();
- }
- }, [isPaused]);
-
- useEffect(() => {
- // 即時模式或暫停時不需要定時器
- if (updateInterval === 0 || isPaused) {
- return;
- }
-
- const intervalId = setInterval(() => {
- const now = Date.now();
- if (Object.keys(latestDataRef.current).length > 0 &&
- (now - lastUpdateRef.current) >= updateInterval &&
- !isPausedRef.current) {
- setIsUpdating(true);
- setFlowData(latestDataRef.current);
- setLastUpdateTime(new Date());
- lastUpdateRef.current = now;
- setTimeout(() => setIsUpdating(false), 300);
- }
- }, Math.min(updateInterval / 4, 1000)); // 檢查頻率為更新間隔的 1/4,最少 1 秒
-
- return () => clearInterval(intervalId);
- }, [updateInterval, isPaused]);
-
- // 提取當前選擇的資料
- const parseCurrentData = useCallback(() => {
- const key = `${direction}_${trafficType}_${timeRange}`;
- const protocol = isIPv6 ? "ipv6" : "ipv4";
- const data = flowData[key]?.[protocol];
-
- if (!data) return null;
-
- return Object.entries(data).map(([ip, details]: [string, any]) => ({
- ip,
- bytes: details.bytes || 0,
- geo: details.geo || null,
- packets: details.packets || 0,
- last_seen: details.last_seen || 0,
- }));
- }, [flowData, direction, trafficType, timeRange, isIPv6]);
-
- // 排序和過濾資料
- const sortedData = useMemo(() => {
- const data = parseCurrentData();
- if (!data) return null;
-
- if (!sortConfig.key) {
- if (searchTerm.trim()) {
- return data.filter((row) =>
- row.ip.toLowerCase().includes(searchTerm.trim().toLowerCase())
- );
- }
- return data;
- }
-
- // 排序
- const sortedData = [...data].sort((a, b) => {
- let valA = a[sortConfig.key as keyof typeof a];
- let valB = b[sortConfig.key as keyof typeof b];
-
- if (sortConfig.key === "ip") {
- valA = getIPSortValue(valA as string, isIPv6);
- valB = getIPSortValue(valB as string, isIPv6);
- }
-
- if (valA < valB) {
- return sortConfig.direction === "asc" ? -1 : 1;
- }
- if (valA > valB) {
- return sortConfig.direction === "asc" ? 1 : -1;
- }
- return 0;
- });
-
- // 過濾
- if (searchTerm.trim()) {
- return sortedData.filter((row) =>
- row.ip.toLowerCase().includes(searchTerm.trim().toLowerCase())
- );
- }
-
- return sortedData;
- }, [parseCurrentData, sortConfig, searchTerm, isIPv6]);
-
- // 檢查是否由於搜索而過濾
- const isSearchFiltered = useMemo(() => {
- return searchTerm.trim() !== "" && parseCurrentData() !== null;
- }, [searchTerm, parseCurrentData]);
-
- // ============ 事件處理函數 ============
-
- // 處理排序
- const handleSort = useCallback((key: string) => {
- setSortConfig(prevConfig => {
- const direction = prevConfig.key === key && prevConfig.direction === "asc" ? "desc" : "asc";
- return { key, direction };
- });
- }, []);
-
- // 搜索處理
- const handleSearchChange = useCallback((e: React.ChangeEvent) => {
- setSearchTerm(e.target.value);
- }, []);
-
- if (isLoading) {
- return (
- <>
-
- Statistics - NetGuardia
-
-
-
-
- >
- );
- }
-
- if (error) {
- return (
- <>
-
- Statistics - NetGuardia
-
-
-
-
-
⚠️
-
Loading Error
-
{error}
-
window.location.reload()}
- className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
- >
- Reload
-
-
-
-
- >
- );
- }
-
- return (
- <>
-
- Statistics - NetGuardia
-
-
-
-
- {/* 頁面標題 */}
-
-
-
-
- Network Traffic Statistics
-
-
- Analyze and monitor network traffic data, view detailed connection statistics
-
-
-
-
-
- {/* 控制面板 */}
-
-
- {/* 統計概覽 */}
- {sortedData && sortedData.length > 0 && (
-
- )}
-
- {/* 資料表格 */}
-
- {sortedData === null ? (
-
- ) : sortedData.length === 0 ? (
-
- ) : (
-
- )}
-
-
- >
- );
-};
-
-export default Statistics;
\ No newline at end of file
diff --git a/mantis-frontend/src/pages/traffic-map.tsx b/mantis-frontend/src/pages/traffic-map.tsx
deleted file mode 100644
index d2dac7a..0000000
--- a/mantis-frontend/src/pages/traffic-map.tsx
+++ /dev/null
@@ -1,793 +0,0 @@
-import React, { useContext, useState, useEffect, useCallback, useMemo, useRef } from 'react';
-import Head from 'next/head';
-import { motion } from 'framer-motion';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import {
- faMapMarkedAlt,
- faGlobe,
- faNetworkWired,
- faFilter,
- faLayerGroup,
- faExpand,
- faCompress,
- faInfoCircle,
- faTimes,
- faPause,
- faPlay,
- faClock,
- faRefresh
-} from '@fortawesome/free-solid-svg-icons';
-import Layout from '../components/Layout';
-import LoadingSpinner from '../components/LoadingSpinner';
-import { useTheme } from '../providers/ThemeProvider';
-import { WebsocketContext } from '../providers/WebSocketProvider';
-import { combineLatest, of } from 'rxjs';
-import { map, catchError } from 'rxjs/operators';
-import {
- MapPoint,
- MapConfig,
- GeoLocation,
- FlowDetails,
- FlowDataRecord,
- ProtocolData,
- FlowDataState
-} from "../types/TrafficMapTypes";
-
-const INITIAL_MAP_CONFIG: MapConfig = {
- center: [0, 20],
- zoom: 0,
- minZoom: 0,
- maxZoom: 8,
- scrollZoom: true,
- boxZoom: false,
- doubleClickZoom: false,
- touchZoomRotate: false,
- dragRotate: false,
- pitchWithRotate: false,
- renderWorldCopies: false
-};
-
-const formatBytes = (bytes: number): string => {
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
- if (bytes < 1024) return `${bytes} B`;
-
- let i = 0;
- let value = bytes;
-
- while (value >= 1024 && i < units.length - 1) {
- value /= 1024;
- i++;
- }
-
- return `${value.toFixed(2)} ${units[i]}`;
-};
-
-const ToggleButton: React.FC<{
- isActive: boolean;
- onClick: () => void;
- children: React.ReactNode;
- isDark?: boolean;
-}> = ({ isActive, onClick, children, isDark = false }) => (
-
- {children}
-
-);
-
-const ControlPanel: React.FC<{
- isIPv6: boolean;
- setIsIPv6: (value: boolean) => void;
- direction: string;
- setDirection: (value: string) => void;
- trafficType: string;
- setTrafficType: (value: string) => void;
- timeRange: string;
- setTimeRange: (value: string) => void;
- isPaused: boolean;
- setIsPaused: (value: boolean) => void;
- onRefresh: () => void;
- lastUpdateTime: Date | null;
-}> = ({
- isIPv6,
- setIsIPv6,
- direction,
- setDirection,
- trafficType,
- setTrafficType,
- timeRange,
- setTimeRange,
- isPaused,
- setIsPaused,
- onRefresh,
- lastUpdateTime
- }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- const TIME_RANGES = ["1min", "10min", "1hour"];
-
- return (
-
-
-
-
IP Version:
-
- setIsIPv6(false)}
- isDark={isDark}
- >
- IPv4
-
- setIsIPv6(true)}
- isDark={isDark}
- >
- IPv6
-
-
-
-
-
-
Direction:
-
- setDirection('ingress')}
- isDark={isDark}
- >
- Ingress
-
- setDirection('egress')}
- isDark={isDark}
- >
- Egress
-
-
-
-
-
-
Traffic Type:
-
- setTrafficType('source')}
- isDark={isDark}
- >
- Source
-
- setTrafficType('destination')}
- isDark={isDark}
- >
- Destination
-
-
-
-
-
-
Time:
-
- {TIME_RANGES.map((range) => (
- setTimeRange(range)}
- isDark={isDark}
- >
- {range}
-
- ))}
-
-
-
-
-
setIsPaused(!isPaused)}
- className={`px-3 py-1 rounded-md text-sm font-medium transition-colors ${
- isPaused
- ? 'bg-green-100 text-green-700 hover:bg-green-200'
- : 'bg-orange-100 text-orange-700 hover:bg-orange-200'
- }`}
- >
-
- {isPaused ? 'Resume' : 'Pause'}
-
-
-
-
- Refresh
-
-
- {lastUpdateTime && (
-
- Last Update: {lastUpdateTime.toLocaleTimeString()}
-
- )}
-
-
-
- );
-};
-
-const MapComponent: React.FC<{
- points: MapPoint[];
- isDark: boolean;
-}> = ({ points, isDark }) => {
- const mapContainer = useRef(null);
- const map = useRef(null);
-
- useEffect(() => {
- if (!mapContainer.current) return;
-
- const initMap = async () => {
- const maplibregl = await import('maplibre-gl');
-
- if (map.current) return;
-
- map.current = new maplibregl.Map({
- container: mapContainer.current!,
- style: isDark
- ? 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'
- : 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
- center: INITIAL_MAP_CONFIG.center,
- zoom: INITIAL_MAP_CONFIG.zoom,
- minZoom: INITIAL_MAP_CONFIG.minZoom,
- maxZoom: INITIAL_MAP_CONFIG.maxZoom,
- scrollZoom: INITIAL_MAP_CONFIG.scrollZoom,
- boxZoom: INITIAL_MAP_CONFIG.boxZoom,
- doubleClickZoom: INITIAL_MAP_CONFIG.doubleClickZoom,
- touchZoomRotate: INITIAL_MAP_CONFIG.touchZoomRotate,
- dragRotate: INITIAL_MAP_CONFIG.dragRotate,
- pitchWithRotate: INITIAL_MAP_CONFIG.pitchWithRotate,
- renderWorldCopies: INITIAL_MAP_CONFIG.renderWorldCopies
- });
-
- map.current.on('load', () => {
- if (!map.current) return;
-
- const style = map.current.getStyle();
- if (style && style.layers) {
- style.layers.forEach((layer: any) => {
- if (layer.type === 'symbol') {
- map.current.setLayoutProperty(layer.id, 'visibility', 'none');
- }
- });
- }
- });
- };
-
- initMap().catch(error => {
- console.error('Failed to initialize map:', error);
- });
-
- return () => {
- if (map.current) {
- map.current.remove();
- map.current = null;
- }
- };
- }, [isDark]);
-
- useEffect(() => {
- if (!map.current) return;
-
- const updateMarkers = async () => {
- const maplibregl = await import('maplibre-gl');
-
- const updateLayers = () => {
- if (!map.current) return;
-
- if (!points.length) {
- if (map.current.getSource('traffic-points')) {
- const source = map.current.getSource('traffic-points') as any;
- source.setData({
- type: 'FeatureCollection',
- features: []
- });
- }
- return;
- }
-
- const geojsonData = {
- type: 'FeatureCollection',
- features: points.map(point => ({
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: [point.longitude, point.latitude]
- },
- properties: {
- ip: point.ip,
- regions: point.regions,
- code: point.code,
- city: point.city,
- bytes: point.bytes,
- packets: point.packets,
- type: point.type,
- traffic: formatBytes(point.bytes)
- }
- }))
- };
-
- if (map.current.getSource('traffic-points')) {
- const source = map.current.getSource('traffic-points') as any;
- source.setData(geojsonData);
- return;
- }
-
- map.current.addSource('traffic-points', {
- type: 'geojson',
- data: geojsonData
- });
-
- map.current.addLayer({
- id: 'traffic-points-glow',
- type: 'circle',
- source: 'traffic-points',
- paint: {
- 'circle-color': [
- 'match',
- ['get', 'type'],
- 'source', '#3B82F6',
- 'destination', '#EF4444',
- '#8B5CF6'
- ],
- 'circle-radius': 16,
- 'circle-opacity': 0.3,
- 'circle-blur': 1
- }
- });
-
- map.current.addLayer({
- id: 'traffic-points-layer',
- type: 'circle',
- source: 'traffic-points',
- paint: {
- 'circle-color': [
- 'match',
- ['get', 'type'],
- 'source', '#3B82F6',
- 'destination', '#EF4444',
- '#8B5CF6'
- ],
- 'circle-radius': 5,
- 'circle-opacity': 0.9,
- 'circle-blur': 0.2
- }
- });
-
- map.current.on('mouseenter', 'traffic-points-layer', (e: any) => {
- const coordinates = e.features[0].geometry.coordinates.slice();
- const props = e.features[0].properties;
-
- const popupContent = `
-
- ${props.ip}
- Regions: ${props.regions}
- Code: ${props.code}
- City: ${props.city}
- Traffic: ${props.traffic}
- Packets: ${props.packets.toLocaleString()}
- Type: ${props.type}
-
- `;
-
- const popup = new maplibregl.Popup({
- closeButton: false,
- closeOnClick: false
- })
- .setLngLat(coordinates)
- .setHTML(popupContent)
- .addTo(map.current);
-
- (map.current as any)._hoverPopup = popup;
- map.current.getCanvas().style.cursor = 'pointer';
- });
-
- map.current.on('mouseleave', 'traffic-points-layer', () => {
- if ((map.current as any)._hoverPopup) {
- (map.current as any)._hoverPopup.remove();
- (map.current as any)._hoverPopup = null;
- }
- map.current.getCanvas().style.cursor = '';
- });
- };
-
- if (map.current.isStyleLoaded()) {
- updateLayers();
- } else {
- map.current.once('load', updateLayers);
- }
- };
-
- updateMarkers();
- }, [points]);
-
- return (
-
- );
-};
-
-const StatsCard: React.FC<{
- title: string;
- value: string | number;
- icon: any;
- color: string;
-}> = ({ title, value, icon, color }) => {
- const { actualTheme } = useTheme();
- const isDark = actualTheme === 'dark';
-
- return (
-
-
-
-
- {title}
-
-
- {value}
-
-
-
-
-
-
-
- );
-};
-
-const TrafficMap: React.FC = () => {
- const { actualTheme } = useTheme();
- const { getIPv4FlowStream, getIPv6FlowStream, bootTime } = useContext(WebsocketContext);
- const isDark = actualTheme === 'dark';
-
- // 狀態管理
- const [isIPv6, setIsIPv6] = useState(false);
- const [direction, setDirection] = useState('ingress');
- const [trafficType, setTrafficType] = useState('source');
- const [timeRange, setTimeRange] = useState('1min');
- const [isPaused, setIsPaused] = useState(false);
- const [lastUpdateTime, setLastUpdateTime] = useState(null);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
-
- const [mapPoints, setMapPoints] = useState([]);
- const [flowData, setFlowData] = useState({});
-
- const latestDataRef = useRef({});
- const isPausedRef = useRef(isPaused);
-
- // 同步 isPaused 到 ref
- useEffect(() => {
- isPausedRef.current = isPaused;
- }, [isPaused]);
-
- // 解析流量資料 - 使用 useCallback 保持穩定引用
- const parseFlowData = useCallback((rawData: any): FlowDataRecord => {
- try {
- if (typeof rawData === 'object' && rawData !== null) {
- return rawData;
- }
- return JSON.parse(rawData);
- } catch (error) {
- console.error("Error parsing flow data:", error);
- return {};
- }
- }, []);
-
- // 處理 WebSocket 資料 - 使用 useCallback 保持穩定引用
- const processWebSocketData = useCallback((results: any[]) => {
- if (isPausedRef.current) return;
-
- const validResults = results.filter(result => result !== null);
- if (validResults.length === 0) return;
-
- const newData: any = {};
- validResults.forEach(result => {
- if (!result) return;
- const { key, protocol, data } = result;
- if (!newData[key]) {
- newData[key] = {};
- }
- newData[key][protocol] = data;
- });
-
- if (Object.keys(newData).length > 0) {
- latestDataRef.current = newData;
- setFlowData(newData);
- setLastUpdateTime(new Date());
- }
- }, []);
-
- useEffect(() => {
- if (!getIPv4FlowStream || !getIPv6FlowStream) {
- setError("WebSocket service unavailable");
- setIsLoading(false);
- return;
- }
-
- console.log("Initializing Traffic Map WebSocket subscriptions...");
- setError(null);
- setIsLoading(true);
-
- // 定義所有需要訂閱的配置
- const DIRECTIONS = ['ingress', 'egress'] as const;
- const TRAFFIC_TYPES = ['source', 'destination'] as const;
- const TIME_RANGES = ['1min', '10min', '1hour'] as const;
-
- const streamConfigs: Array<{
- dir: typeof DIRECTIONS[number];
- type: typeof TRAFFIC_TYPES[number];
- range: typeof TIME_RANGES[number];
- protocol: 'ipv4' | 'ipv6';
- }> = [];
-
- // 生成所有組合 (2 directions × 2 types × 3 ranges × 2 protocols = 24 configs)
- DIRECTIONS.forEach(dir => {
- TRAFFIC_TYPES.forEach(type => {
- TIME_RANGES.forEach(range => {
- streamConfigs.push({ dir, type, range, protocol: "ipv4" });
- streamConfigs.push({ dir, type, range, protocol: "ipv6" });
- });
- });
- });
-
- console.log(`Total stream configs for Traffic Map: ${streamConfigs.length}`); // 24
-
- // 創建所有串流的 Observable
- const streams = streamConfigs.map(config => {
- const { dir, type, range, protocol } = config;
- const getStream = protocol === "ipv4" ? getIPv4FlowStream : getIPv6FlowStream;
-
- return getStream(dir, type, range).pipe(
- map((rawData: any) => ({
- key: `${dir}_${type}_${range}`,
- protocol,
- data: parseFlowData(rawData)
- })),
- catchError(error => {
- console.error(`Error in ${protocol} stream (${dir}/${type}/${range}):`, error);
- return of(null);
- })
- );
- });
-
- // 訂閱合併的串流
- const subscription = combineLatest(streams).subscribe({
- next: (results) => {
- processWebSocketData(results);
- setIsLoading(false);
- },
- error: (error) => {
- console.error("WebSocket subscription error:", error);
- setError("WebSocket connection error");
- setIsLoading(false);
- }
- });
-
- // 清理函數:只取消訂閱,不關閉 WebSocket 連線
- return () => {
- console.log("Unsubscribing from Traffic Map WebSocket streams");
- subscription.unsubscribe();
- };
- }, [getIPv4FlowStream, getIPv6FlowStream, parseFlowData, processWebSocketData]);
-
- useEffect(() => {
- if (!isPaused && Object.keys(latestDataRef.current).length > 0) {
- // 恢復時立即更新一次
- setFlowData(latestDataRef.current);
- setLastUpdateTime(new Date());
- }
- }, [isPaused]);
-
- useEffect(() => {
- const key = `${direction}_${trafficType}_${timeRange}`;
- const protocol = isIPv6 ? "ipv6" : "ipv4";
- const data: FlowDataRecord | undefined = flowData[key]?.[protocol];
-
- if (!data) {
- setMapPoints([]);
- return;
- }
-
- const points: MapPoint[] = [];
- const seenLocations: Record = {};
-
- Object.entries(data).forEach(([ipPort, details]: [string, FlowDetails]) => {
- // 檢查地理位置資料是否有效
- if (!details.geo ||
- details.geo.latitude === null ||
- details.geo.longitude === null) {
- return;
- }
-
- // 提取 IP 地址(移除端口)
- let ip = ipPort;
- if (ipPort.includes('[') && ipPort.includes(']:')) {
- // IPv6 格式: [ip]:port
- ip = ipPort.split(']:')[0].replace('[', '') + ']';
- } else if (ipPort.includes(':')) {
- // IPv4 格式: ip:port
- const parts = ipPort.split(':');
- ip = parts.slice(0, -1).join(':');
- }
-
- const locationKey = `${details.geo.latitude},${details.geo.longitude}`;
-
- // 合併相同位置的多個 IP
- if (seenLocations[locationKey]) {
- const existingPoint = seenLocations[locationKey];
- existingPoint.bytes += details.bytes || 0;
- existingPoint.packets += details.packets || 0;
- if (!existingPoint.ip.includes(ip)) {
- existingPoint.ip += `, ${ip}`;
- }
- } else {
- const newPoint: MapPoint = {
- id: ipPort,
- ip: ip,
- latitude: details.geo.latitude,
- longitude: details.geo.longitude,
- bytes: details.bytes || 0,
- packets: details.packets || 0,
- regions: details.geo.country || 'Unknown',
- city: details.geo.city || 'Unknown',
- code: details.geo.country_code || 'Unknown',
- type: trafficType as 'source' | 'destination'
- };
- seenLocations[locationKey] = newPoint;
- points.push(newPoint);
- }
- });
-
- setMapPoints(points);
- }, [flowData, direction, trafficType, timeRange, isIPv6]);
-
- const handleRefresh = useCallback(() => {
- if (!isPaused && Object.keys(latestDataRef.current).length > 0) {
- setFlowData(latestDataRef.current);
- setLastUpdateTime(new Date());
- }
- }, [isPaused]);
-
- const stats = useMemo(() => {
- const totalBytes = mapPoints.reduce((sum, point) => sum + point.bytes, 0);
- const totalPackets = mapPoints.reduce((sum, point) => sum + point.packets, 0);
-
- const uniqueIPs = new Set();
- mapPoints.forEach(point => {
- const ips = point.ip.split(',').map(ip => ip.trim());
- ips.forEach(ip => uniqueIPs.add(ip));
- });
-
- const uniqueRegions = new Set(mapPoints.map(point => point.regions)).size;
-
- return {
- totalBytes: formatBytes(totalBytes),
- totalPackets: totalPackets.toLocaleString(),
- uniqueIPs: uniqueIPs.size,
- uniqueRegions
- };
- }, [mapPoints]);
-
- if (isLoading) {
- return (
- <>
-
- Traffic Map - NetGuardia
-
-
-
-
-
-
- >
- );
- }
-
- return (
- <>
-
- Traffic Map - NetGuardia
-
-
-
-
-
-
-
-
- Network Traffic Map
-
-
- Visualize network traffic geographic distribution in real-time
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default TrafficMap;
\ No newline at end of file
diff --git a/mantis-frontend/src/providers/AccessControlProvider.tsx b/mantis-frontend/src/providers/AccessControlProvider.tsx
deleted file mode 100644
index 9861ce2..0000000
--- a/mantis-frontend/src/providers/AccessControlProvider.tsx
+++ /dev/null
@@ -1,278 +0,0 @@
-import React, { createContext, useState, useCallback, useMemo, useRef } from 'react'
-import { fetchData } from '../utils/connectionUtils'
-import { urls } from '../config'
-
-interface AccessControlItem {
- ip: string;
- ports: (string | number)[];
-}
-
-interface CachedData {
- [key: string]: {
- data: AccessControlItem[];
- timestamp: number;
- isLoading: boolean;
- }
-}
-
-interface AccessControlContextType {
- // 当前显示的数据
- currentData: AccessControlItem[];
- filteredData: AccessControlItem[];
- isLoading: boolean;
-
- // 数据管理方法
- setFilteredData: (data: AccessControlItem[]) => void;
- switchTo: (isIPv6: boolean, listType: string) => void;
- refreshData: (isIPv6?: boolean, listType?: string) => Promise;
-
- // 缓存状态
- getCacheStatus: () => { [key: string]: { lastUpdate: Date; isStale: boolean } };
-}
-
-export const AccessControlContext = createContext({
- currentData: [],
- filteredData: [],
- isLoading: false,
- setFilteredData: () => {},
- switchTo: () => {},
- refreshData: async () => {},
- getCacheStatus: () => ({}),
-})
-
-interface AccessControlProviderProps {
- children: React.ReactNode
-}
-
-// 缓存过期时间 (10分钟)
-const CACHE_EXPIRE_TIME = 10 * 60 * 1000;
-
-// 生成缓存键
-const getCacheKey = (isIPv6: boolean, listType: string): string => {
- return `${isIPv6 ? 'ipv6' : 'ipv4'}_${listType}`;
-}
-
-// 检查缓存是否过期
-const isCacheStale = (timestamp: number): boolean => {
- return Date.now() - timestamp > CACHE_EXPIRE_TIME;
-}
-
-export const AccessControlProvider: React.FC = ({ children }) => {
- // 缓存所有数据,避免重复请求
- const [cache, setCache] = useState({})
- const [filteredData, setFilteredData] = useState([])
- const [currentKey, setCurrentKey] = useState('')
-
- // 使用 ref 追踪正在进行的请求,避免重复请求
- const pendingRequests = useRef>(new Set())
-
- // 从缓存或 API 获取数据
- const fetchCachedData = useCallback(async (
- isIPv6: boolean,
- listType: string,
- forceRefresh: boolean = false
- ): Promise => {
- const key = getCacheKey(isIPv6, listType);
- const cachedItem = cache[key];
-
- // 如果有有效缓存且不强制刷新,直接返回
- if (!forceRefresh && cachedItem && !isCacheStale(cachedItem.timestamp) && !cachedItem.isLoading) {
- return cachedItem.data;
- }
-
- // 如果正在请求中,等待完成
- if (pendingRequests.current.has(key)) {
- return new Promise((resolve) => {
- const checkInterval = setInterval(() => {
- if (!pendingRequests.current.has(key)) {
- clearInterval(checkInterval);
- const updatedCache = cache[key];
- resolve(updatedCache ? updatedCache.data : []);
- }
- }, 100);
- });
- }
-
- // 标记正在加载
- pendingRequests.current.add(key);
-
- // 立即更新缓存状态为加载中
- setCache(prev => ({
- ...prev,
- [key]: {
- data: cachedItem?.data || [],
- timestamp: cachedItem?.timestamp || 0,
- isLoading: true
- }
- }));
-
- try {
- const url = isIPv6
- ? urls.access_control.ipv6[listType as keyof typeof urls.access_control.ipv6]
- : urls.access_control.ipv4[listType as keyof typeof urls.access_control.ipv4];
-
- const data = await new Promise((resolve, reject) => {
- fetchData(
- url,
- (response) => {
- try {
- const parsedData = JSON.parse(response);
- const dataArray: AccessControlItem[] = Object.entries(parsedData).map(([ip, ports]) => ({
- ip,
- ports: ports as (string | number)[],
- }));
- resolve(dataArray);
- } catch (error) {
- console.error("Error parsing data:", error);
- reject(new Error("Invalid data format"));
- }
- },
- (error) => {
- console.error(`Error fetching from ${url}:`, error);
- reject(new Error("Network error: Please check your connection"));
- }
- );
- });
-
- // 更新缓存
- setCache(prev => ({
- ...prev,
- [key]: {
- data,
- timestamp: Date.now(),
- isLoading: false
- }
- }));
-
- return data;
-
- } catch (error) {
- // 发生错误时,恢复缓存状态
- setCache(prev => ({
- ...prev,
- [key]: {
- data: cachedItem?.data || [],
- timestamp: cachedItem?.timestamp || 0,
- isLoading: false
- }
- }));
-
- throw error;
- } finally {
- // 移除请求标记
- pendingRequests.current.delete(key);
- }
- }, [cache]);
-
- // 切换到指定的IP版本和列表类型
- const switchTo = useCallback(async (isIPv6: boolean, listType: string) => {
- const key = getCacheKey(isIPv6, listType);
- setCurrentKey(key);
-
- try {
- const data = await fetchCachedData(isIPv6, listType);
- setFilteredData(data);
- } catch (error) {
- console.error(`Failed to switch to ${key}:`, error);
- // 发生错误时显示空数据,但不影响用户体验
- setFilteredData([]);
- }
- }, [fetchCachedData]);
-
- // 刷新数据
- const refreshData = useCallback(async (isIPv6?: boolean, listType?: string) => {
- // 如果没有指定参数,刷新当前数据
- if (isIPv6 === undefined || listType === undefined) {
- if (!currentKey) return;
- const [ipVersion, list] = currentKey.split('_');
- const isV6 = ipVersion === 'ipv6';
- await switchTo(isV6, list);
- return;
- }
-
- // 刷新指定数据
- try {
- const data = await fetchCachedData(isIPv6, listType, true);
- const key = getCacheKey(isIPv6, listType);
-
- // 如果刷新的是当前显示的数据,更新显示
- if (key === currentKey) {
- setFilteredData(data);
- }
- } catch (error) {
- console.error(`Failed to refresh data for ${getCacheKey(isIPv6, listType)}:`, error);
- throw error;
- }
- }, [currentKey, fetchCachedData, switchTo]);
-
- // 获取缓存状态(用于调试和监控)
- const getCacheStatus = useCallback(() => {
- const status: { [key: string]: { lastUpdate: Date; isStale: boolean } } = {};
-
- Object.entries(cache).forEach(([key, item]) => {
- status[key] = {
- lastUpdate: new Date(item.timestamp),
- isStale: isCacheStale(item.timestamp)
- };
- });
-
- return status;
- }, [cache]);
-
- // 计算当前数据和加载状态
- const currentData = useMemo(() => {
- return currentKey ? (cache[currentKey]?.data || []) : [];
- }, [cache, currentKey]);
-
- const isLoading = useMemo(() => {
- return currentKey ? (cache[currentKey]?.isLoading || false) : false;
- }, [cache, currentKey]);
-
- // 预加载常用数据(IPv4 黑名单)
- React.useEffect(() => {
- // 在组件挂载时预加载 IPv4 黑名单,这是最常用的
- const preloadKey = getCacheKey(false, 'black_list');
- if (!cache[preloadKey]) {
- fetchCachedData(false, 'black_list').catch(error => {
- console.warn('Failed to preload IPv4 blacklist:', error);
- });
- }
- }, []); // 只在组件挂载时执行一次
-
- // 定期清理过期缓存
- React.useEffect(() => {
- const cleanupInterval = setInterval(() => {
- setCache(prev => {
- const now = Date.now();
- const cleaned: CachedData = {};
-
- Object.entries(prev).forEach(([key, item]) => {
- // 保留最近 30 分钟内的缓存
- if (now - item.timestamp < 30 * 60 * 1000) {
- cleaned[key] = item;
- }
- });
-
- return cleaned;
- });
- }, 5 * 60 * 1000); // 每5分钟清理一次
-
- return () => clearInterval(cleanupInterval);
- }, []);
-
- const contextValue = useMemo((): AccessControlContextType => ({
- currentData,
- filteredData,
- isLoading,
- setFilteredData,
- switchTo,
- refreshData,
- getCacheStatus,
- }), [currentData, filteredData, isLoading, switchTo, refreshData, getCacheStatus]);
-
- return (
-
- {children}
-
- );
-};
\ No newline at end of file
diff --git a/mantis-frontend/src/providers/ThemeProvider.tsx b/mantis-frontend/src/providers/ThemeProvider.tsx
deleted file mode 100644
index 08fb631..0000000
--- a/mantis-frontend/src/providers/ThemeProvider.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-// src/providers/ThemeProvider.tsx
-'use client'
-
-import React, { createContext, useContext, useEffect, useState } from 'react'
-
-type Theme = 'light' | 'dark' | 'system'
-
-interface ThemeContextType {
- theme: Theme
- actualTheme: 'light' | 'dark'
- setTheme: (theme: Theme) => void
- toggleTheme: () => void
-}
-
-const ThemeContext = createContext(undefined)
-
-export const useTheme = () => {
- const context = useContext(ThemeContext)
- if (!context) {
- throw new Error('useTheme must be used within a ThemeProvider')
- }
- return context
-}
-
-export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const [theme, setTheme] = useState('system')
- const [actualTheme, setActualTheme] = useState<'light' | 'dark'>('light')
-
- const getSystemTheme = (): 'light' | 'dark' => {
- if (typeof window !== 'undefined') {
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
- }
- return 'light'
- }
-
- const calculateActualTheme = (currentTheme: Theme): 'light' | 'dark' => {
- if (currentTheme === 'system') {
- return getSystemTheme()
- }
- return currentTheme
- }
-
- useEffect(() => {
- const savedTheme = localStorage.getItem('theme') as Theme
- if (savedTheme && ['light', 'dark', 'system'].includes(savedTheme)) {
- setTheme(savedTheme)
- }
- }, [])
-
- useEffect(() => {
- const newActualTheme = calculateActualTheme(theme)
- setActualTheme(newActualTheme)
-
- if (typeof window !== 'undefined') {
- const root = window.document.documentElement
- root.classList.remove('light', 'dark')
- root.classList.add(newActualTheme)
-
- root.setAttribute('data-theme', newActualTheme)
-
- localStorage.setItem('theme', theme)
- }
- }, [theme])
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
-
- const handleChange = () => {
- if (theme === 'system') {
- const newActualTheme = calculateActualTheme(theme)
- setActualTheme(newActualTheme)
-
- const root = window.document.documentElement
- root.classList.remove('light', 'dark')
- root.classList.add(newActualTheme)
- root.setAttribute('data-theme', newActualTheme)
- }
- }
-
- mediaQuery.addEventListener('change', handleChange)
- return () => mediaQuery.removeEventListener('change', handleChange)
- }
- }, [theme])
-
- const toggleTheme = () => {
- setTheme((currentTheme: Theme) => {
- switch (currentTheme) {
- case 'light':
- return 'dark'
- case 'dark':
- return 'system'
- case 'system':
- return 'light'
- default:
- return 'light'
- }
- })
- }
-
- const handleSetTheme = (newTheme: Theme) => {
- setTheme(newTheme)
- }
-
- return (
-
- {children}
-
- )
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/providers/WebSocketProvider.tsx b/mantis-frontend/src/providers/WebSocketProvider.tsx
deleted file mode 100644
index 2370645..0000000
--- a/mantis-frontend/src/providers/WebSocketProvider.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-import React, { createContext, useState, useEffect, useRef, useCallback } from 'react'
-import { websocketUrl, urls } from '../config'
-import { createWebSocket, fetchData } from '../utils/connectionUtils'
-import { BehaviorSubject } from 'rxjs'
-import { share } from 'rxjs/operators'
-
-interface WebSocketContextType {
- bootTime: number | null;
- getDetectionAlertStream: () => any;
- getIPv4FlowStream: (direction: string, flow_direction: string, timeType: string) => any;
- getIPv6FlowStream: (direction: string, flow_direction: string, timeType: string) => any;
- getSystemHealthStream: () => any;
- getLatestFlowData: (protocol: 'ipv4' | 'ipv6', direction: string, flow_direction: string, timeType: string) => any;
- getLatestSystemHealth: () => any;
-}
-
-export const WebsocketContext = createContext({
- bootTime: null,
- getDetectionAlertStream: () => null,
- getIPv4FlowStream: () => null,
- getIPv6FlowStream: () => null,
- getSystemHealthStream: () => null,
- getLatestFlowData: () => null,
- getLatestSystemHealth: () => null,
-})
-
-export const WebSocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
- const wsRefs = useRef<{ [key: string]: WebSocket }>({})
- const [bootTime, setBootTime] = useState(null)
- const retryDelays = useRef<{ [key: string]: number }>({})
- const dataSubjects = useRef<{ [key: string]: BehaviorSubject }>({})
- const latestDataCache = useRef<{ [key: string]: any }>({})
-
- const fetchBootTime = async () => {
- try {
- await fetchData(
- urls.bootTime,
- (data) => setBootTime(Number(data.trim())),
- (error) => {
- throw new Error(`Failed to fetch boot_time: ${error?.message || 'Unknown error'}`)
- }
- )
- } catch (error) {
- console.error('Boot time fetch error:', error)
- }
- }
-
- const getOrCreateSubject = (url: string) => {
- if (!dataSubjects.current[url]) {
- dataSubjects.current[url] = new BehaviorSubject(null)
- }
- return dataSubjects.current[url]
- }
-
- const getWebSocketStream = (url: string) => {
- const subject = getOrCreateSubject(url)
- return subject.asObservable().pipe(share())
- }
-
- const setupWebSocket = (url: string, retryCount: number = 0) => {
- if (wsRefs.current[url] && wsRefs.current[url].readyState === WebSocket.OPEN) {
- console.log(`WebSocket already connected: ${url}`)
- return wsRefs.current[url]
- }
-
- if (wsRefs.current[url]) {
- wsRefs.current[url].close()
- }
-
- const ws = createWebSocket(
- url,
- (data) => {
- latestDataCache.current[url] = data
- getOrCreateSubject(url).next(data)
- },
- (error) => console.error(`WebSocket error for ${url}:`, error)
- )
-
- ws.onclose = () => {
- console.warn(`WebSocket for ${url} closed. Retrying...`)
- delete wsRefs.current[url]
- let delay = retryDelays.current[url] || 1000
- retryDelays.current[url] = Math.min(delay * 2, 30000)
- setTimeout(() => setupWebSocket(url, retryCount + 1), delay)
- }
-
- ws.onopen = () => {
- console.log(`WebSocket connected: ${url}`)
- retryDelays.current[url] = 1000
- }
-
- wsRefs.current[url] = ws
- return ws
- }
-
- const getLatestFlowData = useCallback((
- protocol: 'ipv4' | 'ipv6',
- direction: string,
- flow_direction: string,
- timeType: string
- ) => {
- const urlFunc = protocol === 'ipv4' ? websocketUrl.ipv4FlowStats : websocketUrl.ipv6FlowStats
- const url = urlFunc(direction, flow_direction, timeType)
- return latestDataCache.current[url] || null
- }, [])
-
- const getLatestSystemHealth = useCallback(() => {
- return latestDataCache.current[websocketUrl.systemHealth] || null
- }, [])
-
- const getDetectionAlertStream = useCallback(() => {
- return getWebSocketStream(websocketUrl.detectionAlert)
- }, [])
-
- const getIPv4FlowStream = useCallback((direction: string, flow_direction: string, timeType: string) => {
- const url = websocketUrl.ipv4FlowStats(direction, flow_direction, timeType)
- return getWebSocketStream(url)
- }, [])
-
- const getIPv6FlowStream = useCallback((direction: string, flow_direction: string, timeType: string) => {
- const url = websocketUrl.ipv6FlowStats(direction, flow_direction, timeType)
- return getWebSocketStream(url)
- }, [])
-
- const getSystemHealthStream = useCallback(() => {
- return getWebSocketStream(websocketUrl.systemHealth)
- }, [])
-
- const initializeWebSockets = useCallback(() => {
- console.log("Initializing all WebSocket connections...")
-
- setupWebSocket(websocketUrl.detectionAlert)
- setupWebSocket(websocketUrl.systemHealth)
-
- const directions = ["ingress", "egress"]
- const flowDirections = ["source", "destination"]
- const timeTypes = ["1min", "10min", "1hour"]
-
- directions.forEach(direction => {
- flowDirections.forEach(flow_direction => {
- timeTypes.forEach(timeType => {
- setupWebSocket(websocketUrl.ipv4FlowStats(direction, flow_direction, timeType))
- setupWebSocket(websocketUrl.ipv6FlowStats(direction, flow_direction, timeType))
- })
- })
- })
-
- console.log(`Total WebSocket connections: ${Object.keys(wsRefs.current).length}`)
- }, [])
-
- const closeWebSockets = useCallback(() => {
- console.log("Closing all WebSocket connections...")
-
- Object.entries(wsRefs.current).forEach(([url, ws]) => {
- try {
- ws.close()
- } catch (error) {
- console.error(`Failed to close WebSocket ${url}:`, error)
- }
- })
- wsRefs.current = {}
-
- Object.values(dataSubjects.current).forEach(subject => {
- subject.complete()
- })
- dataSubjects.current = {}
- latestDataCache.current = {}
- }, [])
-
- useEffect(() => {
- fetchBootTime()
- initializeWebSockets()
- return () => closeWebSockets()
- }, [])
-
- return (
-
- {children}
-
- )
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/styles/globals.css b/mantis-frontend/src/styles/globals.css
deleted file mode 100644
index 0ef832d..0000000
--- a/mantis-frontend/src/styles/globals.css
+++ /dev/null
@@ -1,68 +0,0 @@
-@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:wght@300;400;500;600&family=Noto+Sans+TC:wght@300;400;500;600&family=Noto+Sans+SC:wght@300;400;500;600&family=Noto+Sans+JP:wght@300;400;500;600&display=swap');
-@import "tailwindcss";
-
-svg.svg-inline--fa {
- display: inline-block;
- height: 1em;
- overflow: visible;
- vertical-align: -0.125em;
-}
-
-/* 確保圖標尺寸穩定 */
-.svg-inline--fa {
- width: 1em;
-}
-
-/* 針對有固定寬高的圖標 */
-.svg-inline--fa.fa-fw {
- width: 1.25em;
-}
-
-/* 防止尺寸跳動 */
-.svg-inline--fa {
- transition: none !important;
-}
-
-/* 確保初始渲染就有正確尺寸 */
-.svg-inline--fa:not(:root) {
- overflow: visible;
- box-sizing: content-box;
-}
-
-::-webkit-scrollbar {
- width: 8px;
- height: 8px;
-}
-
-::-webkit-scrollbar-thumb {
- background: rgba(59, 130, 246, 0.3); /* 使用 Tailwind blue-500 */
- border-radius: 10px;
- transition: background 0.2s ease-in-out; /* 使用完整的 transition 屬性 */
-}
-
-::-webkit-scrollbar-thumb:hover {
- background: rgba(59, 130, 246, 0.6); /* 懸停時加深 */
-}
-
-::-webkit-scrollbar-track {
- background: rgba(243, 244, 246, 0.5); /* 淺灰色背景,提供更好的對比 */
- border-radius: 10px;
- transition: background 0.15s ease; /* 軌道也加上過渡效果 */
-}
-
-/* 可選:為暗色主題準備的樣式 */
-@media (prefers-color-scheme: dark) {
- ::-webkit-scrollbar-thumb {
- background: rgba(96, 165, 250, 0.4); /* 暗色主題下使用 blue-400 */
- transition: background 0.2s ease-in-out;
- }
-
- ::-webkit-scrollbar-thumb:hover {
- background: rgba(96, 165, 250, 0.7);
- }
-
- ::-webkit-scrollbar-track {
- background: rgba(31, 41, 55, 0.3); /* 暗色主題下的軌道背景 */
- transition: background 0.15s ease;
- }
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/types/AIDetectionTypes.tsx b/mantis-frontend/src/types/AIDetectionTypes.tsx
deleted file mode 100644
index d30e428..0000000
--- a/mantis-frontend/src/types/AIDetectionTypes.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-// ai detection types
-export interface AlertLog {
- timestamp: number
- flow_key: string
- src_ip: string
- dst_ip: string
- src_port: number
- dst_port: number
- protocol: number
- is_attack: boolean
- attack_type: string
- confidence: number
- ae_score: number
- rf_score: number
- ensemble_score: number
-}
-
-export interface PriorityInfo {
- color: string
- bgColor: string
- icon: any
- label: string
-}
-
-export interface NotificationProps {
- message: string
- type: 'success' | 'error' | 'warning' | 'info'
- onClose: () => void
-}
-
-export interface AlertDetailsProps {
- log: AlertLog
- onClose: () => void
- showNotification: (message: string, type?: 'success' | 'error' | 'warning' | 'info') => void
-}
-
-export interface AlertItemProps {
- log: AlertLog
- index: number
- onClick: (index: number) => void
-}
-
-export interface FilterButtonProps {
- isActive: boolean
- onClick: () => void
- children: React.ReactNode
- className?: string
- activeClassName?: string
- activeHoverClassName?: string
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/types/AccessControlTypes.tsx b/mantis-frontend/src/types/AccessControlTypes.tsx
deleted file mode 100644
index 70038a6..0000000
--- a/mantis-frontend/src/types/AccessControlTypes.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-// access control types
-export interface AccessControlItem {
- ip: string
- ports: (string | number)[]
-}
-
-export interface NotificationProps {
- message: string
- type: 'success' | 'error' | 'warning' | 'info'
- onClose: () => void
-}
-
-export interface ModalProps {
- title: string
- children: React.ReactNode
- onClose: () => void
- onSubmit?: () => void
- submitLabel?: string
- submitDisabled?: boolean
-}
-
-export interface ToggleButtonProps {
- isActive: boolean
- onClick: () => void
- children: React.ReactNode
- className?: string
-}
-
-export interface StatsCardProps {
- title: string
- value: number
- icon: any
- color: string
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/types/DashboardTypes.tsx b/mantis-frontend/src/types/DashboardTypes.tsx
deleted file mode 100644
index 1afc087..0000000
--- a/mantis-frontend/src/types/DashboardTypes.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-// dashboard types
-export interface TrendDataPoint {
- time: string
- value: number
-}
-
-export interface TrendData {
- ingressSource: TrendDataPoint[]
- egressSource: TrendDataPoint[]
-}
-
-export interface TrafficData {
- ipv4: {
- ingressSource: number
- egressSource: number
- }
- ipv6: {
- ingressSource: number
- egressSource: number
- }
-}
-
-export interface UpdateControlProps {
- updateInterval: number;
- setUpdateInterval: (value: number) => void;
- isPaused: boolean;
- setIsPaused: (value: boolean) => void;
- lastUpdateTime: Date | null;
- connectionStatus: string;
-}
-
-export interface EChartsComponentProps {
- data: TrendData
- type: 'ipv4' | 'ipv6'
- isPaused: boolean
-}
diff --git a/mantis-frontend/src/types/HomeTypes.tsx b/mantis-frontend/src/types/HomeTypes.tsx
deleted file mode 100644
index b8c7499..0000000
--- a/mantis-frontend/src/types/HomeTypes.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-// index types
-export interface SystemHealthData {
- timestamp: number
- boot_time?: number
- uptime_seconds?: number
- system_info: {
- kernel_version: string,
- os_name: string,
- os_version: string,
- architecture: string,
- total_processes: number
- }
- cpu_details?: {
- cpu_brand: string
- core_count: number
- cpu_usage: number
- cpu_frequency: number
- cores: Array<{
- core_id: number
- usage_percent: number
- frequency: number
- }>
- }
- memory_usage: {
- total: number
- used: number
- available: number
- usage_percent: number
- swap_total: number
- swap_used: number
- }
- network_stats: {
- ingress: NetworkInterface
- egress: NetworkInterface
- management: NetworkInterface
- }
- load_average: {
- one_minute: number
- five_minute: number
- fifteen_minute: number
- } | null
- temperature: number
-}
-
-export interface NetworkInterface {
- interface: string
- bytes_received: number
- bytes_transmitted: number
- packets_received: number
- packets_transmitted: number
- errors_received: number
- errors_transmitted: number
-}
-
-export interface HealthMetric {
- name: string
- value: number | string
- unit?: string
- status: 'good' | 'warning' | 'critical' | 'unknown'
- icon: any
- color: string
- bgColor: string
- description?: string
-}
-
-export interface SystemDetailModalProps {
- isOpen: boolean
- onClose: () => void
- systemHealth: SystemHealthData | null
- type: 'cpu' | 'memory' | 'temperature'
-}
-
-export interface HealthCardProps {
- metric: HealthMetric
- index: number
- systemHealth?: SystemHealthData | null
- onClick?: () => void
-}
-
-export interface LoadAverageProps {
- loadAverage: SystemHealthData['load_average']
-}
-
-export interface NetworkStatsProps {
- networkStats: SystemHealthData['network_stats']
-}
diff --git a/mantis-frontend/src/types/StatisticsTypes.tsx b/mantis-frontend/src/types/StatisticsTypes.tsx
deleted file mode 100644
index be882ae..0000000
--- a/mantis-frontend/src/types/StatisticsTypes.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-// statistics types
-export interface ToggleButtonProps {
- isActive: boolean;
- onClick: () => void;
- children: React.ReactNode;
- className?: string;
-}
-
-export interface TableHeaderProps {
- column: { key: string; label: string };
- sortConfig: { key: string | null; direction: string };
- onSort: (key: string) => void;
-}
-
-export interface UpdateControlProps {
- updateInterval: number;
- setUpdateInterval: (value: number) => void;
- isPaused: boolean;
- setIsPaused: (value: boolean) => void;
- lastUpdateTime: Date | null;
-}
-
-export interface ControlPanelProps {
- isIPv6: boolean;
- setIsIPv6: (value: boolean) => void;
- direction: string;
- setDirection: (value: string) => void;
- trafficType: string;
- setTrafficType: (value: string) => void;
- timeRange: string;
- setTimeRange: (value: string) => void;
- searchTerm: string;
- setSearchTerm: (value: string) => void;
- updateInterval: number;
- setUpdateInterval: (value: number) => void;
- isPaused: boolean;
- setIsPaused: (value: boolean) => void;
- lastUpdateTime: Date | null;
-}
-
-export interface DataTableProps {
- data: any[];
- sortConfig: { key: string | null; direction: string };
- handleSort: (key: string) => void;
- bootTime: number | null;
- isUpdating: boolean;
-}
-
-export interface NoDataStateProps {
- isSearchFiltered: boolean;
-}
-
-export interface StatsOverviewProps {
- data: any[];
- isIPv6: boolean;
- direction: string;
- trafficType: string;
-}
\ No newline at end of file
diff --git a/mantis-frontend/src/types/TrafficMapTypes.tsx b/mantis-frontend/src/types/TrafficMapTypes.tsx
deleted file mode 100644
index 2571dbd..0000000
--- a/mantis-frontend/src/types/TrafficMapTypes.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-// traffic map types
-
-export interface MapPoint {
- id: string;
- ip: string;
- latitude: number;
- longitude: number;
- bytes: number;
- packets: number;
- code: string;
- city: string;
- regions: string;
- type: 'source' | 'destination';
-}
-
-export interface MapConfig {
- center: [number, number];
- zoom: number;
- minZoom: number,
- maxZoom: number,
- scrollZoom: boolean,
- boxZoom: boolean,
- doubleClickZoom: boolean,
- touchZoomRotate: boolean,
- dragRotate: boolean,
- pitchWithRotate: boolean,
- renderWorldCopies: boolean
-}
-
-export interface GeoLocation {
- latitude: number | null;
- longitude: number | null;
- city?: string;
- country?: string;
- country_code?: string;
-}
-
-export interface FlowDetails {
- bytes: number;
- packets: number;
- geo: GeoLocation;
-}
-
-export interface FlowDataRecord {
- [ipPort: string]: FlowDetails;
-}
-
-export interface ProtocolData {
- ipv4?: FlowDataRecord;
- ipv6?: FlowDataRecord;
-}
-
-export interface FlowDataState {
- [key: string]: ProtocolData;
-}
diff --git a/mantis-frontend/src/utils/connectionUtils.ts b/mantis-frontend/src/utils/connectionUtils.ts
deleted file mode 100644
index bc26871..0000000
--- a/mantis-frontend/src/utils/connectionUtils.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-export const createWebSocket = (
- url: string,
- onMessage: (data: any) => void,
- onError?: (error: Event) => void
-): WebSocket => {
- const ws = new WebSocket(url)
-
- ws.onmessage = (event) => {
- try {
- const data = JSON.parse(event.data)
- onMessage(data)
- } catch (error) {
- console.error("Failed to parse WebSocket message:", error)
- }
- }
-
- ws.onerror = (error) => {
- console.error("WebSocket Error:", error)
- if (onError) onError(error)
- }
-
- return ws
-}
-
-export const fetchData = async (
- url: string,
- onSuccess: (data: string) => void,
- onError?: (error?: Error) => void
-): Promise => {
- try {
- const response = await fetch(url)
- if (!response.ok) throw new Error(`HTTP Error: ${response.status}`)
- const data = await response.text()
- onSuccess(data)
- } catch (error) {
- console.error(`Failed to fetch data from URL: ${url}`)
- if (onError) onError(error as Error)
- }
-}
-
-export const putData = async (
- url: string,
- payload: any,
- onSuccess?: (data?: any) => void,
- onError?: (error: Error) => void
-): Promise => {
- try {
- const response = await fetch(url, {
- method: "PUT",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(payload),
- })
-
- if (!response.ok) throw new Error(`HTTP Error: ${response.status}`)
-
- const contentType = response.headers.get("Content-Type")
- const isJsonResponse = contentType && contentType.includes("application/json")
-
- const data = isJsonResponse ? await response.json() : null
-
- if (onSuccess) onSuccess(data)
- } catch (error) {
- console.error(`Failed to send PUT request to URL: ${url}`, error)
- if (onError) onError(error as Error)
- }
-}
-
-export const deleteData = async (
- url: string,
- data: any,
- onSuccess?: (data: string) => void,
- onError?: (error: Error) => void
-): Promise => {
- try {
- const response = await fetch(url, {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- })
-
- if (!response.ok) throw new Error(`HTTP Error: ${response.status}`)
-
- const responseData = await response.text()
- if (onSuccess) onSuccess(responseData)
- } catch (error) {
- console.error(`Failed to send DELETE request to URL: ${url}`, error)
- if (onError) onError(error as Error)
- }
-}
\ No newline at end of file
diff --git a/mantis-frontend/start.bat b/mantis-frontend/start.bat
deleted file mode 100644
index eb83804..0000000
--- a/mantis-frontend/start.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-@echo off
-title KWL-BOT v2.0
-npm run dev
-pause
\ No newline at end of file
diff --git a/mantis-frontend/start.sh b/mantis-frontend/start.sh
deleted file mode 100644
index d68e96c..0000000
--- a/mantis-frontend/start.sh
+++ /dev/null
@@ -1,83 +0,0 @@
-#!/bin/bash
-
-# 切換到工作目錄
-cd /home
-
-# 檢查是否為首次安裝(通過檢查是否存在特定標記文件)
-if [ ! -f ".setup_complete" ]; then
- echo "First installation detected, starting system initialization..."
-
- # 更新系統套件
- apt-get update
- apt-get upgrade -y
-
- # 安裝必要的系統套件
- apt install tzdata -y
- apt install -y ca-certificates curl gnupg
-
- echo "Installing Node.js LTS (v22.x)..."
-
- # 創建 keyrings 目錄
- sudo mkdir -p /etc/apt/keyrings
-
- # 下載並添加 NodeSource GPG key
- curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
-
- # 設定 Node.js 主要版本號
- NODE_MAJOR=22
-
- # 添加 NodeSource repository
- echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
-
- # 更新套件索引
- apt update
-
- # 安裝 Node.js (包含 npm)
- apt install nodejs -y
-
- # 驗證安裝
- echo "Node.js installation completed!"
- echo "Node.js version: $(node --version)"
- echo "npm version: $(npm --version)"
-
- # 創建安裝完成標記
- touch .setup_complete
- echo "System initialization completed!"
-else
- echo "System initialization already completed, skipping installation steps..."
- echo "Current Node.js version: $(node --version)"
- echo "Current npm version: $(npm --version)"
-fi
-
-# 安裝/更新 npm 依賴套件
-echo "Installing/updating npm dependencies..."
-if [ -f "package.json" ]; then
- npm install
-else
- echo "No package.json found, skipping npm install..."
-fi
-
-# 運行應用程式
-echo "Starting application..."
-if [ -f "package.json" ]; then
- # 檢查是否有 build script
- if grep -q '"build"' package.json 2>/dev/null; then
- echo "Running build script..."
- npm run build
- else
- echo "No build script found, skipping build step..."
- fi
-
- # 檢查是否有 start script
- if grep -q '"start:https"' package.json 2>/dev/null; then
- echo "Running start script for https..."
- npm run start:https
- elif grep -q '"start"' package.json 2>/dev/null; then
- echo "Running start script..."
- npm run start
- else
- echo "No start script found, please configure your package.json start script..."
- fi
-else
- echo "No package.json found, please initialize your Node.js project first with 'npm init'"
-fi
\ No newline at end of file
diff --git a/mantis-frontend/tailwind.config.js b/mantis-frontend/tailwind.config.js
deleted file mode 100644
index dd77b57..0000000
--- a/mantis-frontend/tailwind.config.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import typography from '@tailwindcss/typography';
-
-/** @type {import('tailwindcss').Config} */
-export default {
- content: [
- "./pages/**/*.{js,ts,jsx,tsx}",
- "./components/**/*.{js,ts,jsx,tsx}",
- "./src/**/*.{js,ts,jsx,tsx}",
- ],
- theme: {
- extend: {},
- },
- plugins: [typography],
-}
\ No newline at end of file
diff --git a/mantis-frontend/tsconfig.json b/mantis-frontend/tsconfig.json
deleted file mode 100644
index 55963a4..0000000
--- a/mantis-frontend/tsconfig.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "compilerOptions": {
- "target": "es2024",
- "lib": [
- "dom",
- "dom.iterable",
- "esnext"
- ],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": false,
- "noEmit": true,
- "incremental": true,
- "module": "esnext",
- "esModuleInterop": true,
- "allowSyntheticDefaultImports": true,
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "react-jsx"
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx"
- ],
- "exclude": [
- "node_modules"
- ]
-}