diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 33fdb08..71a2de2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "axios": "^1.7.7", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" } @@ -3484,6 +3485,15 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.20.0", + "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -16207,6 +16217,38 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "6.27.0", + "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.20.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.27.0", + "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmmirror.com/react-scripts/-/react-scripts-5.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 58a5d6a..b860d37 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,7 @@ "axios": "^1.7.7", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-router-dom": "^6.27.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/frontend/src/App.css b/frontend/src/App.css index b77d9a8..e762e16 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,114 +1,135 @@ -@keyframes glow-border { - 0% { - box-shadow: inset 0 0 0 2px #00e6e6; - } - 25% { - box-shadow: inset 0 0 0 2px #00e6e6, 2px 0 0 0 #00e6e6; - } - 50% { - box-shadow: inset 0 0 0 2px #00e6e6, 2px 0 0 0 #00e6e6, 0 2px 0 0 #00e6e6; - } - 75% { - box-shadow: inset 0 0 0 2px #00e6e6, 2px 0 0 0 #00e6e6, 0 2px 0 0 #00e6e6, -2px 0 0 0 #00e6e6; - } - 100% { - box-shadow: inset 0 0 0 2px #00e6e6, 2px 0 0 0 #00e6e6, 0 2px 0 0 #00e6e6, -2px 0 0 0 #00e6e6, 0 -2px 0 0 #00e6e6; - } +/* App.css */ +body { + background-color: #f0f8ff; /* AliceBlue */ + color: #2f6c99; /* Navy */ + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + min-height: 100vh; /* Minimum height set to 100% of the viewport height */ } -body { - background-color: #1e1e1e; - color: #ffffff; - font-family: Arial, sans-serif; +.App { + display: flex; + flex-direction: column; + min-height: 100vh; /* Ensure the App component takes the full height */ } .App-header { - text-align: center; + background-color: #4abbe1; /*背景颜色*/ padding: 20px; + text-align: center; } -h1 { - color: #eff4f0; - font-size: 4em; - margin-bottom: 20px; +.App-content { + flex: 1; /* This makes the main content take up the remaining space */ + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 40px 20px; + background: linear-gradient(135deg, #e0f7fa, #80deea); /* 渐变背景颜色 */ + border-radius: 0px; /* 圆角边框 */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* 阴影效果 */ } -h2 { - color: #2196f3; - font-size: 2em; - margin-top: 20px; - margin-bottom: 10px; +.App-footer { + background-color: #4abbe1; /*背景颜色*/ + padding: 10px; + text-align: center; } -h3 { - color: #ff9800; - font-size: 1.75em; - margin-top: 15px; - margin-bottom: 10px; -} - -h4 { - color: #f44336; - font-size: 1.5em; - margin-top: 10px; - margin-bottom: 5px; +.glow { + color: #ffffff; /* White */ + text-shadow: 0 0 10px #00bfff, 0 0 20px #00bfff, 0 0 30px #00bfff, 0 0 40px #00bfff, 0 0 50px #00bfff, 0 0 60px #00bfff, 0 0 70px #00bfff; + font-size: 2.5em; /* 调整字体大小 */ } input, button { - margin: 10px 0; + margin: 20px 10px; padding: 10px; - border: none; + border: 1px solid #000080; /* Navy */ border-radius: 5px; -} - -input { - width: 80%; - max-width: 300px; -} - -button { - background-color: #2196f3; - color: #ffffff; cursor: pointer; + font-size: 1.2em; /* 调整字体大小 */ } button:hover { - background-color: #1976d2; + background-color: #104e8b; /* DodgerBlue Darker */ } -section { - background-color: #333333; - padding: 20px; - margin: 20px 0; - border-radius: 10px; - animation: glow-border 4s infinite; +nav ul { + list-style-type: none; + padding: 0; + margin: 20px 0; /* 增加上下间距 */ +} + +nav li { + display: inline; + margin: 0 10px; +} + +nav a { + color: #ffffff; /* White */ + text-decoration: none; + font-size: 1.5em; /* 调整字体大小 */ + padding: 5px 19px; + border-radius: 5px; + background-color: #2db5c391; /* DodgerBlue */ + transition: background-color 0.3s, transform 0.3s; +} + +nav a:hover { + text-decoration: underline; + background-color: #104e8b; /* DodgerBlue Darker */ + transform: scale(1.05); } .log-info { - background-color: #444444; - color: #ffffff; + height: 200px; + overflow-y: scroll; + background-color: #ffffff; /* White */ padding: 10px; + border: 1px solid #b3d9ff; /* Light Blue Border */ border-radius: 5px; - animation: glow-border 4s infinite; + margin-top: 20px; } -ul { - list-style-type: none; - padding: 0; -} - -li { - background-color: #444444; +.log-entry { margin: 5px 0; + padding: 5px; + border-bottom: 1px solid #b3d9ff; /* Light Blue Border */ +} + +.log-entry:last-child { + border-bottom: none; +} + +.log-timestamp { + color: #007acc; /* Blue */ + font-weight: bold; +} + +.log-message { + color: #333333; /* Dark Gray */ +} + +.node-info, .heartbeat-info, .nodes-list { + margin-top: 20px; + background-color: #e6f7ff; /* Light Blue */ padding: 10px; + border: 1px solid #b3d9ff; /* Light Blue Border */ border-radius: 5px; + width: 100%; + max-width: 600px; } -.container { - display: flex; - justify-content: space-between; +.info-item { + margin-bottom: 10px; } -.left-panel, .right-panel { - width: 48%; +pre { + white-space: pre-wrap; /* Wrap long lines */ + word-wrap: break-word; /* Break long words */ } \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js index 76bf8c2..dc6df9d 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,118 +1,38 @@ -import React, { useEffect, useState } from 'react'; -import axios from 'axios'; +import React from 'react'; +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; +import HomePage from './HomePage'; +import NodePage from './NodePage'; +import HeartbeatPage from './HeartbeatPage'; +import NodesListPage from './NodesListPage'; import WebSocketComponent from './WebSocketComponent'; import './App.css'; function App() { - const [node, setNode] = useState(null); - const [heartbeat, setHeartbeat] = useState(null); - const [nodesList, setNodesList] = useState([]); - const [ip, setIp] = useState(''); - const [count, setCount] = useState(''); - - const fetchNodes = async () => { - try { - const response = await axios.get('/server/show_nodes'); - setNodesList(response.data); - } catch (error) { - console.error('Error fetching nodes:', error); - } - }; - - const fetchNode = async (ip) => { - try { - const response = await axios.get('/server/get_node', { params: { ip } }); - setNode(response.data); - } catch (error) { - console.error('Error fetching node:', error); - } - }; - - const fetchHeartbeat = async (ip) => { - try { - const response = await axios.get('/server/heartbeat', { params: { ip } }); - setHeartbeat(response.data); - } catch (error) { - console.error('Error fetching heartbeat:', error); - } - }; - - const fetchNodesList = async (count) => { - try { - const response = await axios.get('/server/send_nodes_list', { params: { count } }); - setNodesList(response.data); - } catch (error) { - console.error('Error fetching nodes list:', error); - } - }; - - useEffect(() => { - fetchNodes(); // 获取所有节点 - }, []); - - const handleFetchNode = () => { - fetchNode(ip); // 根据输入的 IP 获取单个节点 - }; - - const handleFetchHeartbeat = () => { - fetchHeartbeat(ip); // 根据输入的 IP 获取心跳信息 - }; - - const handleFetchNodesList = () => { - fetchNodesList(count); // 根据输入的数量获取节点列表 - }; - return ( -
-
-

The server

-
-
-
-

get node

- setIp(e.target.value)} - placeholder="Enter node ip" - /> - - {node ?

{JSON.stringify(node)}

:

here is nothing!

} -
+ +
+ {/* Header 部分仅放置导航或标题 */} +
+
-
-

heartbeat

- - {heartbeat ?

{JSON.stringify(heartbeat)}

:

here is nothing!

} -
+ {/* Main 部分用来渲染路由的内容 */} +
+ + } /> + } /> + } /> + } /> + } /> + +
-
-

nodes list

- setCount(e.target.value)} - placeholder="Enter the number of nodes" - /> - - {nodesList.length > 0 ? ( -
    - {nodesList.map((node, index) => ( -
  • {JSON.stringify(node)}
  • - ))} -
- ) : ( -

here is nothing!

- )} -
-
-
- -
-
-
-
+ {/* Footer 部分 */} + + + ); } -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/HeartbeatPage.js b/frontend/src/HeartbeatPage.js new file mode 100644 index 0000000..4fc67c2 --- /dev/null +++ b/frontend/src/HeartbeatPage.js @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import { Link } from 'react-router-dom'; +import './App.css'; + +const HeartbeatPage = () => { + const [ip, setIp] = useState(''); + const [heartbeat, setHeartbeat] = useState(null); + + const fetchHeartbeat = async (ip) => { + try { + const response = await axios.get('/server/heartbeat', { params: { ip } }); + setHeartbeat(response.data); + } catch (error) { + console.error('Error fetching heartbeat:', error); + } + }; + + const handleFetchHeartbeat = () => { + fetchHeartbeat(ip); + }; + + return ( +
+ +

Heartbeat

+

请输入节点 IP 地址以获取心跳状态信息:

+ setIp(e.target.value)} + placeholder="Enter node IP" + /> + + {heartbeat ? ( +
+

Heartbeat Information:

+
Status: {heartbeat.status}
+
+ ) : ( +

这里没有任何信息!

+ )} +
+ ); +}; + +export default HeartbeatPage; \ No newline at end of file diff --git a/frontend/src/HomePage.js b/frontend/src/HomePage.js new file mode 100644 index 0000000..af7042a --- /dev/null +++ b/frontend/src/HomePage.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; +import './App.css'; +const HomePage = () => { + return ( +
+

The server

+

欢迎来到服务器管理页面。请选择一个操作:

+ +
+ ); +}; + +export default HomePage; \ No newline at end of file diff --git a/frontend/src/NodePage.js b/frontend/src/NodePage.js new file mode 100644 index 0000000..0a73f63 --- /dev/null +++ b/frontend/src/NodePage.js @@ -0,0 +1,59 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import { Link } from 'react-router-dom'; +import './App.css'; + +const NodePage = () => { + const [ip, setIp] = useState(''); + const [node, setNode] = useState(null); + + const fetchNode = async (ip) => { + try { + const response = await axios.get('/server/get_node', { params: { ip } }); + const nodeData = response.data; + const date = new Date(nodeData.current_time * 1000); + nodeData.current_time = date.toLocaleString(); + setNode(nodeData); + } catch (error) { + console.error('Error fetching node:', error); + } + }; + + const handleFetchNode = () => { + fetchNode(ip); + }; + + return ( +
+ +

Get Node

+

请输入节点 IP 地址以获取节点信息:

+ setIp(e.target.value)} + placeholder="Enter node IP" + /> + + {node ? ( +
+

Node Information:

+
ID: {node.id}
+
Current Time: {node.current_time}
+
+ ) : ( +

这里没有任何信息!

+ )} +
+ ); +}; + +export default NodePage; \ No newline at end of file diff --git a/frontend/src/NodesListPage.js b/frontend/src/NodesListPage.js new file mode 100644 index 0000000..ac40cba --- /dev/null +++ b/frontend/src/NodesListPage.js @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import axios from 'axios'; +import { Link } from 'react-router-dom'; +import './App.css'; + +const NodesListPage = () => { + const [count, setCount] = useState(''); + const [nodesList, setNodesList] = useState([]); + + const fetchNodesList = async (count) => { + try { + const response = await axios.get('/server/send_nodes_list', { params: { count } }); + setNodesList(response.data); + } catch (error) { + console.error('Error fetching nodes list:', error); + } + }; + + const handleFetchNodesList = () => { + fetchNodesList(count); + }; + + return ( +
+ +

Nodes List

+

请输入要获取的节点数量:

+ setCount(e.target.value)} + placeholder="Enter the number of nodes" + /> + + {nodesList.length > 0 ? ( +
+

Nodes Information:

+ +
+ ) : ( +

这里没有任何信息!

+ )} +
+ ); +}; + +export default NodesListPage; \ No newline at end of file diff --git a/frontend/src/WebSocketComponent.js b/frontend/src/WebSocketComponent.js index ee879d5..eaf146f 100644 --- a/frontend/src/WebSocketComponent.js +++ b/frontend/src/WebSocketComponent.js @@ -1,4 +1,5 @@ import React, { useEffect, useState, useRef, useCallback } from 'react'; +import './App.css'; // 确保引入了样式文件 const WebSocketComponent = () => { const [logs, setLogs] = useState([]); @@ -16,7 +17,7 @@ const WebSocketComponent = () => { }; wsRef.current.onmessage = (event) => { - setLogs((prevLogs) => [...prevLogs, event.data]); // 直接加入收到的消息 + setLogs((prevLogs) => [...prevLogs, JSON.parse(event.data)]); // 解析 JSON 数据 }; wsRef.current.onerror = (error) => { @@ -50,14 +51,18 @@ const WebSocketComponent = () => { return (
-

The logs

-
- {logs.map((log, index) => ( -

{log}

- ))} +

The logs

+

这里显示服务器的日志信息:

+
+ {logs.map((log, index) => { + const [timestamp, message] = log.message.split(' - ', 2); + return ( +
+ {timestamp} + - {message} +
+ ); + })}
); diff --git a/frontend/src/index.css b/frontend/src/index.css index 89f20df..9087f42 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,11 +1,18 @@ -/* src/index.css */ -body { +html, body { margin: 0; + padding: 0; + height: 100%; /* Ensures the body takes the full height */ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +#root { + height: 100%; /* Makes sure the root element takes the full height of the viewport */ + display: flex; + flex-direction: column; +} + code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } diff --git a/frontend/src/index.js b/frontend/src/index.js index b5b05a4..c15f402 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,13 +1,11 @@ import React from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App'; +import ReactDOM from 'react-dom'; import './index.css'; +import App from './App'; -const container = document.getElementById('root'); -const root = createRoot(container); - -root.render( +ReactDOM.render( - + , + document.getElementById('root') ); \ No newline at end of file diff --git a/src/server.py b/src/server.py index 3716404..341b852 100644 --- a/src/server.py +++ b/src/server.py @@ -28,9 +28,10 @@ app.add_middleware( ) # 配置日志文件路径 -log_dir = "logs" -if not os.path.exists(log_dir): - os.makedirs(log_dir) + +log_dir = os.path.expanduser("~/tpre-python-logs") +os.makedirs(log_dir, exist_ok=True) + log_file = os.path.join(log_dir, "server_logs.log") # 全局日志配置 @@ -58,10 +59,10 @@ async def lifespan(_: FastAPI): current_dir = os.path.dirname(os.path.abspath(__file__)) # 定义 frontend 的绝对路径 -frontend_dir = os.path.join(current_dir, "..", "frontend") +frontend_dir = os.path.join(current_dir, "..", "frontend","build") app = FastAPI(lifespan=lifespan) -app.mount("/frontend", StaticFiles(directory="frontend/build"), name="frontend") +app.mount("/frontend", StaticFiles(directory=frontend_dir), name="frontend") def init():