mirror of
https://github.com/dpup/meshstream.git
synced 2026-03-28 17:42:37 +01:00
Working on UI
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Meshstream - A web interface for viewing Meshtastic network traffic" />
|
||||
<title>Meshstream</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
<html lang="en" class="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="ERSN Mesh :: Meshtastic activity in the Ebbett's Pass region of Highway 4, CA." />
|
||||
<title>ERSN Mesh</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -29,12 +29,14 @@
|
||||
"@tanstack/router-devtools": "^1.116.0",
|
||||
"@tanstack/router-vite-plugin": "^1.116.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-redux": "^9.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
@@ -51,6 +53,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.31.0",
|
||||
"vite": "^6.3.2",
|
||||
|
||||
59
web/pnpm-lock.yaml
generated
59
web/pnpm-lock.yaml
generated
@@ -29,6 +29,9 @@ dependencies:
|
||||
leaflet:
|
||||
specifier: ^1.9.4
|
||||
version: 1.9.4
|
||||
lucide-react:
|
||||
specifier: ^0.503.0
|
||||
version: 0.503.0(react@19.1.0)
|
||||
react:
|
||||
specifier: ^19.1.0
|
||||
version: 19.1.0
|
||||
@@ -43,6 +46,9 @@ dependencies:
|
||||
version: 9.2.0(@types/react@19.1.2)(react@19.1.0)(redux@5.0.1)
|
||||
|
||||
devDependencies:
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4(vite@6.3.2)
|
||||
'@testing-library/jest-dom':
|
||||
specifier: ^6.6.3
|
||||
version: 6.6.3
|
||||
@@ -91,6 +97,9 @@ devDependencies:
|
||||
postcss:
|
||||
specifier: ^8.5.3
|
||||
version: 8.5.3
|
||||
tailwindcss:
|
||||
specifier: ^4.1.4
|
||||
version: 4.1.4
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
@@ -892,7 +901,6 @@ packages:
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.29.2
|
||||
tailwindcss: 4.1.4
|
||||
dev: false
|
||||
|
||||
/@tailwindcss/oxide-android-arm64@4.1.4:
|
||||
resolution: {integrity: sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA==}
|
||||
@@ -900,7 +908,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-darwin-arm64@4.1.4:
|
||||
@@ -909,7 +916,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-darwin-x64@4.1.4:
|
||||
@@ -918,7 +924,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-freebsd-x64@4.1.4:
|
||||
@@ -927,7 +932,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4:
|
||||
@@ -936,7 +940,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-linux-arm64-gnu@4.1.4:
|
||||
@@ -945,7 +948,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-linux-arm64-musl@4.1.4:
|
||||
@@ -954,7 +956,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-linux-x64-gnu@4.1.4:
|
||||
@@ -963,7 +964,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-linux-x64-musl@4.1.4:
|
||||
@@ -972,7 +972,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-wasm32-wasi@4.1.4:
|
||||
@@ -980,7 +979,6 @@ packages:
|
||||
engines: {node: '>=14.0.0'}
|
||||
cpu: [wasm32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
bundledDependencies:
|
||||
- '@napi-rs/wasm-runtime'
|
||||
@@ -996,7 +994,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide-win32-x64-msvc@4.1.4:
|
||||
@@ -1005,7 +1002,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/@tailwindcss/oxide@4.1.4:
|
||||
@@ -1024,7 +1020,6 @@ packages:
|
||||
'@tailwindcss/oxide-wasm32-wasi': 4.1.4
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.1.4
|
||||
dev: false
|
||||
|
||||
/@tailwindcss/postcss@4.1.4:
|
||||
resolution: {integrity: sha512-bjV6sqycCEa+AQSt2Kr7wpGF1bOZJ5wsqnLEkqSbM/JEHxx/yhMH8wHmdkPyApF9xhHeMSwnnkDUUMMM/hYnXw==}
|
||||
@@ -1036,6 +1031,17 @@ packages:
|
||||
tailwindcss: 4.1.4
|
||||
dev: false
|
||||
|
||||
/@tailwindcss/vite@4.1.4(vite@6.3.2):
|
||||
resolution: {integrity: sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A==}
|
||||
peerDependencies:
|
||||
vite: ^5.2.0 || ^6
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.1.4
|
||||
'@tailwindcss/oxide': 4.1.4
|
||||
tailwindcss: 4.1.4
|
||||
vite: 6.3.2
|
||||
dev: true
|
||||
|
||||
/@tanstack/history@1.115.0:
|
||||
resolution: {integrity: sha512-K7JJNrRVvyjAVnbXOH2XLRhFXDkeP54Kt2P4FR1Kl2KDGlIbkua5VqZQD2rot3qaDrpufyUa63nuLai1kOLTsQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2026,7 +2032,6 @@ packages:
|
||||
/detect-libc@2.0.4:
|
||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/diff@7.0.0:
|
||||
resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==}
|
||||
@@ -2066,7 +2071,6 @@ packages:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
dev: false
|
||||
|
||||
/entities@6.0.0:
|
||||
resolution: {integrity: sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==}
|
||||
@@ -2593,7 +2597,6 @@ packages:
|
||||
|
||||
/graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
dev: false
|
||||
|
||||
/graphemer@1.4.0:
|
||||
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
|
||||
@@ -2919,7 +2922,6 @@ packages:
|
||||
/jiti@2.4.2:
|
||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
@@ -3022,7 +3024,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-darwin-x64@1.29.2:
|
||||
@@ -3031,7 +3032,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-freebsd-x64@1.29.2:
|
||||
@@ -3040,7 +3040,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-linux-arm-gnueabihf@1.29.2:
|
||||
@@ -3049,7 +3048,6 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-linux-arm64-gnu@1.29.2:
|
||||
@@ -3058,7 +3056,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-linux-arm64-musl@1.29.2:
|
||||
@@ -3067,7 +3064,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-linux-x64-gnu@1.29.2:
|
||||
@@ -3076,7 +3072,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-linux-x64-musl@1.29.2:
|
||||
@@ -3085,7 +3080,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-win32-arm64-msvc@1.29.2:
|
||||
@@ -3094,7 +3088,6 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss-win32-x64-msvc@1.29.2:
|
||||
@@ -3103,7 +3096,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/lightningcss@1.29.2:
|
||||
@@ -3122,7 +3114,6 @@ packages:
|
||||
lightningcss-linux-x64-musl: 1.29.2
|
||||
lightningcss-win32-arm64-msvc: 1.29.2
|
||||
lightningcss-win32-x64-msvc: 1.29.2
|
||||
dev: false
|
||||
|
||||
/locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
@@ -3159,6 +3150,14 @@ packages:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
/lucide-react@0.503.0(react@19.1.0):
|
||||
resolution: {integrity: sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
dependencies:
|
||||
react: 19.1.0
|
||||
dev: false
|
||||
|
||||
/lz-string@1.5.0:
|
||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||
hasBin: true
|
||||
@@ -3869,12 +3868,10 @@ packages:
|
||||
|
||||
/tailwindcss@4.1.4:
|
||||
resolution: {integrity: sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A==}
|
||||
dev: false
|
||||
|
||||
/tapable@2.2.1:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/tiny-invariant@1.3.3:
|
||||
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
|
||||
|
||||
@@ -3,4 +3,4 @@ export default {
|
||||
'@tailwindcss/postcss': {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
};
|
||||
55
web/src/components/ConnectionStatus.tsx
Normal file
55
web/src/components/ConnectionStatus.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { WifiOff, Wifi, WifiLow, Activity, AlertCircle } from "lucide-react";
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
status: string;
|
||||
}
|
||||
|
||||
export const ConnectionStatus: React.FC<ConnectionStatusProps> = ({
|
||||
status,
|
||||
}) => {
|
||||
// Determine connection status type
|
||||
const getStatusInfo = () => {
|
||||
if (status.includes("error") || status.includes("Error")) {
|
||||
return {
|
||||
icon: <AlertCircle className="h-5 w-5 text-red-400" />,
|
||||
text: "Connection Error",
|
||||
colorClass: "text-red-400",
|
||||
};
|
||||
} else if (status.includes("Reconnecting")) {
|
||||
return {
|
||||
icon: <WifiLow className="h-5 w-5 text-amber-400" />,
|
||||
text: "Reconnecting",
|
||||
colorClass: "text-amber-400",
|
||||
};
|
||||
} else if (status.includes("Connecting")) {
|
||||
return {
|
||||
icon: <Activity className="h-5 w-5 text-blue-400" />,
|
||||
text: "Connecting",
|
||||
colorClass: "text-blue-400",
|
||||
};
|
||||
} else if (status.includes("Connected")) {
|
||||
return {
|
||||
icon: <Wifi className="h-5 w-5 text-green-400" />,
|
||||
text: "Connected",
|
||||
colorClass: "text-green-400",
|
||||
};
|
||||
} else {
|
||||
// Default state or unknown status
|
||||
return {
|
||||
icon: <WifiOff className="h-5 w-5 text-neutral-400" />,
|
||||
text: status || "Unknown",
|
||||
colorClass: "text-neutral-400",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { icon, text, colorClass } = getStatusInfo();
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2 bg-neutral-800 p-4 border-1 border-b-neutral-700 border-r-neutral-700 border-t-neutral-950 border-l-neutral-950 rounded-sm">
|
||||
{icon}
|
||||
<span className={`text-sm font-medium ${colorClass}`}>{text}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface FilterProps {
|
||||
onChange: (filter: string) => void;
|
||||
@@ -8,13 +8,16 @@ interface FilterProps {
|
||||
export const Filter: React.FC<FilterProps> = ({ onChange, value }) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="filter" className="block text-sm font-medium text-gray-700 mb-1">
|
||||
<label
|
||||
htmlFor="filter"
|
||||
className="block text-sm font-medium text-neutral-700 mb-1"
|
||||
>
|
||||
Filter Packets
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="filter"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
className="w-full px-3 py-2 border border-neutral-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="Filter by type, sender, etc."
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
|
||||
@@ -1,41 +1,57 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Info, AlertTriangle, AlertCircle } from "lucide-react";
|
||||
|
||||
interface InfoMessageProps {
|
||||
message: string;
|
||||
type?: 'info' | 'warning' | 'error';
|
||||
type?: "info" | "warning" | "error";
|
||||
}
|
||||
|
||||
export const InfoMessage: React.FC<InfoMessageProps> = ({
|
||||
message,
|
||||
type = 'info'
|
||||
export const InfoMessage: React.FC<InfoMessageProps> = ({
|
||||
message,
|
||||
type = "info",
|
||||
}) => {
|
||||
const getBgColor = () => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'bg-yellow-50 border-yellow-200';
|
||||
case 'error':
|
||||
return 'bg-red-50 border-red-200';
|
||||
case 'info':
|
||||
case "warning":
|
||||
return "bg-neutral-700 border-neutral-600";
|
||||
case "error":
|
||||
return "bg-neutral-800 border-neutral-700";
|
||||
case "info":
|
||||
default:
|
||||
return 'bg-blue-50 border-blue-200';
|
||||
return "bg-neutral-800 border-neutral-700";
|
||||
}
|
||||
};
|
||||
|
||||
const getTextColor = () => {
|
||||
switch (type) {
|
||||
case 'warning':
|
||||
return 'text-yellow-800';
|
||||
case 'error':
|
||||
return 'text-red-800';
|
||||
case 'info':
|
||||
case "warning":
|
||||
return "text-amber-400";
|
||||
case "error":
|
||||
return "text-red-400";
|
||||
case "info":
|
||||
default:
|
||||
return 'text-blue-800';
|
||||
return "text-neutral-300";
|
||||
}
|
||||
};
|
||||
|
||||
const getIcon = () => {
|
||||
switch (type) {
|
||||
case "warning":
|
||||
return <AlertTriangle className="h-5 w-5 text-amber-400" />;
|
||||
case "error":
|
||||
return <AlertCircle className="h-5 w-5 text-red-400" />;
|
||||
case "info":
|
||||
default:
|
||||
return <Info className="h-5 w-5 text-neutral-400" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`p-3 mb-4 rounded border ${getBgColor()}`}>
|
||||
<p className={`text-sm ${getTextColor()}`}>{message}</p>
|
||||
<div className={`p-3 mb-4 rounded border ${getBgColor()} shadow-inner`}>
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0 mt-0.5 mr-3">{getIcon()}</div>
|
||||
<p className={`text-sm ${getTextColor()}`}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,24 +1,44 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
import { Packet } from "../lib/types";
|
||||
|
||||
interface MessageDisplayProps {
|
||||
message: any; // Will be properly typed once we have the protobuf structures
|
||||
message: Packet;
|
||||
}
|
||||
|
||||
export const MessageDisplay: React.FC<MessageDisplayProps> = ({ message }) => {
|
||||
// The data structure will be refined as we integrate with the protobuf definitions
|
||||
const { data } = message;
|
||||
|
||||
const getMessageContent = () => {
|
||||
if (data.text_message) {
|
||||
return data.text_message;
|
||||
} else if (data.position) {
|
||||
return `Position: ${data.position.latitude}, ${data.position.longitude}`;
|
||||
} else if (data.node_info) {
|
||||
return `Node Info: ${data.node_info.longName || data.node_info.shortName}`;
|
||||
} else if (data.telemetry) {
|
||||
return "Telemetry data";
|
||||
} else if (data.decode_error) {
|
||||
return `Error: ${data.decode_error}`;
|
||||
}
|
||||
return "Unknown message type";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 border rounded shadow-sm bg-white">
|
||||
<div className="p-4 border border-neutral-700 rounded bg-neutral-800 shadow-inner">
|
||||
<div className="flex justify-between mb-2">
|
||||
<span className="font-medium">{message.from || 'Unknown'}</span>
|
||||
<span className="text-gray-500 text-sm">
|
||||
{message.timestamp ? new Date(message.timestamp).toLocaleString() : 'No timestamp'}
|
||||
<span className="font-medium text-neutral-200">
|
||||
From: {data.from || "Unknown"}
|
||||
</span>
|
||||
<span className="text-neutral-400 text-sm">
|
||||
ID: {data.id || "No ID"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
{message.text || 'No content'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
ID: {message.id || 'No ID'}
|
||||
<div className="mb-2 text-neutral-300">{getMessageContent()}</div>
|
||||
<div className="mt-3 flex justify-between items-center">
|
||||
<span className="text-xs text-neutral-500">
|
||||
Channel: {message.info.channel}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500">Type: {data.port_num}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
57
web/src/components/Nav.tsx
Normal file
57
web/src/components/Nav.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from "react";
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ConnectionStatus } from "./ConnectionStatus";
|
||||
import { Separator } from "./Separator";
|
||||
import { SITE_TITLE } from "../lib/config";
|
||||
|
||||
interface NavProps {
|
||||
connectionStatus: string;
|
||||
}
|
||||
|
||||
export const Nav: React.FC<NavProps> = ({ connectionStatus }) => {
|
||||
return (
|
||||
<aside className="w-64 text-neutral-100 h-screen fixed left-0 top-0 flex flex-col">
|
||||
{/* Logo section */}
|
||||
<div className="p-4 mb-2">
|
||||
<h1 className="text-xl font-bold">{SITE_TITLE}</h1>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<nav className="flex-1 pt-4">
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<Link
|
||||
to="/"
|
||||
className="block px-4 py-2 hover:bg-neutral-800 transition-colors"
|
||||
activeProps={{
|
||||
className:
|
||||
"block px-4 py-2 bg-neutral-800 border-l-4 border-neutral-500",
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
to="/packets"
|
||||
className="block px-4 py-2 hover:bg-neutral-800 transition-colors"
|
||||
activeProps={{
|
||||
className:
|
||||
"block px-4 py-2 bg-neutral-800 border-l-4 border-neutral-500",
|
||||
}}
|
||||
>
|
||||
Packets
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="px-4 py-2 pb-6">
|
||||
<ConnectionStatus status={connectionStatus} />
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
interface PacketDetailsProps {
|
||||
packet: any; // Will be properly typed once we have the protobuf structures
|
||||
@@ -6,9 +6,9 @@ interface PacketDetailsProps {
|
||||
|
||||
export const PacketDetails: React.FC<PacketDetailsProps> = ({ packet }) => {
|
||||
return (
|
||||
<div className="p-4 border rounded bg-gray-50">
|
||||
<div className="p-4 border rounded bg-neutral-50">
|
||||
<h3 className="text-lg font-semibold mb-2">Packet Details</h3>
|
||||
<pre className="text-xs overflow-auto p-2 bg-gray-100 rounded">
|
||||
<pre className="text-xs overflow-auto p-2 bg-neutral-100 rounded">
|
||||
{JSON.stringify(packet, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
import React from 'react';
|
||||
import { useAppSelector } from '../hooks';
|
||||
import React from "react";
|
||||
import { useAppSelector } from "../hooks";
|
||||
|
||||
export const PacketList: React.FC = () => {
|
||||
const { packets, loading, error } = useAppSelector(state => state.packets);
|
||||
const { packets, loading, error } = useAppSelector((state) => state.packets);
|
||||
|
||||
if (loading) {
|
||||
return <div className="p-4">Loading...</div>;
|
||||
return <div className="p-4 text-neutral-300">Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="p-4 text-red-500">Error: {error}</div>;
|
||||
return <div className="p-4 text-red-400">Error: {error}</div>;
|
||||
}
|
||||
|
||||
if (packets.length === 0) {
|
||||
return <div className="p-4">No packets received yet</div>;
|
||||
return <div className="p-4 text-neutral-400">No packets received yet</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="text-xl font-bold mb-4">Received Packets</h2>
|
||||
<h2 className="text-xl font-bold mb-4 text-neutral-200">
|
||||
Received Packets
|
||||
</h2>
|
||||
<ul className="space-y-2">
|
||||
{packets.map(packet => (
|
||||
<li key={packet.id} className="p-2 border rounded">
|
||||
{packets.map((packet) => (
|
||||
<li
|
||||
key={packet.id}
|
||||
className="p-3 border border-neutral-700 rounded bg-neutral-900 shadow-inner text-neutral-300 hover:bg-neutral-800 transition-colors"
|
||||
>
|
||||
{packet.id}
|
||||
</li>
|
||||
))}
|
||||
|
||||
13
web/src/components/Separator.tsx
Normal file
13
web/src/components/Separator.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
interface SeparatorProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Separator: React.FC<SeparatorProps> = ({ className = "" }) => {
|
||||
return (
|
||||
<div
|
||||
className={`mx-4 h-px border-t-1 border-t-neutral-900 border-b-1 border-b-neutral-700/80 my-3 ${className}`}
|
||||
></div>
|
||||
);
|
||||
};
|
||||
@@ -3,3 +3,6 @@ export * from './MessageDisplay';
|
||||
export * from './PacketDetails';
|
||||
export * from './Filter';
|
||||
export * from './InfoMessage';
|
||||
export * from './ConnectionStatus';
|
||||
export * from './Nav';
|
||||
export * from './Separator';
|
||||
@@ -7,6 +7,9 @@ export const IS_DEV = import.meta.env.DEV;
|
||||
export const IS_PROD = import.meta.env.PROD;
|
||||
export const APP_ENV = import.meta.env.VITE_APP_ENV || 'development';
|
||||
|
||||
// Site configuration
|
||||
export const SITE_TITLE = import.meta.env.VITE_SITE_TITLE || 'ERSN Mesh';
|
||||
|
||||
// API URL configuration
|
||||
const getApiBaseUrl = (): string => {
|
||||
// In production, use the same domain (empty string base URL)
|
||||
@@ -24,4 +27,4 @@ export const API_BASE_URL = getApiBaseUrl();
|
||||
export const API_ENDPOINTS = {
|
||||
STREAM: `${API_BASE_URL}/api/stream`,
|
||||
RECENT_PACKETS: `${API_BASE_URL}/api/packets/recent`,
|
||||
};
|
||||
};
|
||||
@@ -1,36 +1,39 @@
|
||||
import { Link, Outlet } from '@tanstack/react-router';
|
||||
import { Outlet } from "@tanstack/react-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Nav } from "../components";
|
||||
import { streamPackets, StreamEvent } from "../lib/api";
|
||||
|
||||
export default function Root() {
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<string>("Connecting...");
|
||||
|
||||
useEffect(() => {
|
||||
// Set up Server-Sent Events connection
|
||||
const cleanup = streamPackets(
|
||||
// Event handler for all event types
|
||||
(event: StreamEvent) => {
|
||||
if (event.type === "info") {
|
||||
// Handle info events (connection status, etc.)
|
||||
setConnectionStatus(event.data);
|
||||
}
|
||||
},
|
||||
// On error
|
||||
() => {
|
||||
setConnectionStatus("Connection error. Reconnecting...");
|
||||
}
|
||||
);
|
||||
|
||||
// Clean up connection when component unmounts
|
||||
return cleanup;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<header className="bg-blue-600 text-white shadow-md">
|
||||
<div className="container mx-auto px-4 py-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">Meshstream</h1>
|
||||
<nav className="space-x-4">
|
||||
<Link
|
||||
to="/"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
}}
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/packets"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
}}
|
||||
>
|
||||
Packets
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="container mx-auto px-4 py-6">
|
||||
<div className="flex min-h-screen bg-neutral-800">
|
||||
{/* Sidebar Navigation */}
|
||||
<Nav connectionStatus={connectionStatus} />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<main className="ml-64 flex-1 p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,48 @@
|
||||
import { InfoMessage, Separator } from "../components";
|
||||
import { SITE_TITLE } from "../lib/config";
|
||||
|
||||
export function IndexPage() {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Welcome to Meshstream</h2>
|
||||
<p className="mb-4">
|
||||
This application provides a real-time view of Meshtastic network traffic.
|
||||
</p>
|
||||
<div className="bg-blue-100 p-4 rounded">
|
||||
<h3 className="text-lg font-semibold mb-2">Getting Started</h3>
|
||||
<p>
|
||||
Click on the <strong>Packets</strong> link in the navigation to view incoming
|
||||
messages from the Meshtastic network.
|
||||
<div className="bg-neutral-700 rounded-lg shadow-inner">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-neutral-100">
|
||||
Welcome to {SITE_TITLE}
|
||||
</h2>
|
||||
<p className="mb-4 text-neutral-200">
|
||||
This application provides a real-time view of Meshtastic network
|
||||
traffic.
|
||||
</p>
|
||||
|
||||
<InfoMessage
|
||||
message="Click on the Packets link in the navigation to view incoming messages from the Meshtastic network."
|
||||
type="info"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-6" />
|
||||
|
||||
<div className="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-neutral-800 border border-neutral-700 p-5 rounded shadow-inner">
|
||||
<h3 className="text-lg font-semibold mb-3 text-neutral-200">
|
||||
About Meshtastic
|
||||
</h3>
|
||||
<p className="text-neutral-300">
|
||||
Meshtastic is an open source, off-grid, decentralized mesh
|
||||
communication platform. It allows devices to communicate without
|
||||
cellular service or internet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-800 border border-neutral-700 p-5 rounded shadow-inner">
|
||||
<h3 className="text-lg font-semibold mb-3 text-neutral-200">
|
||||
Data Privacy
|
||||
</h3>
|
||||
<p className="text-neutral-300">
|
||||
All data is processed locally. Position data on public servers has
|
||||
reduced precision for privacy protection.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,47 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAppDispatch } from '../hooks';
|
||||
import { PacketList } from '../components/PacketList';
|
||||
import { InfoMessage } from '../components/InfoMessage';
|
||||
import { addPacket } from '../store/slices/packetSlice';
|
||||
import { streamPackets, StreamEvent } from '../lib/api';
|
||||
import { useEffect } from "react";
|
||||
import { useAppDispatch } from "../hooks";
|
||||
import { PacketList } from "../components/PacketList";
|
||||
import { InfoMessage, Separator } from "../components";
|
||||
import { addPacket } from "../store/slices/packetSlice";
|
||||
import { streamPackets, StreamEvent } from "../lib/api";
|
||||
|
||||
export function PacketsRoute() {
|
||||
const dispatch = useAppDispatch();
|
||||
const [connectionStatus, setConnectionStatus] = useState<string>('Connecting...');
|
||||
|
||||
useEffect(() => {
|
||||
// Set up Server-Sent Events connection using our API utility
|
||||
const cleanup = streamPackets(
|
||||
// Event handler for all event types
|
||||
(event: StreamEvent) => {
|
||||
if (event.type === 'info') {
|
||||
// Handle info events (connection status, etc.)
|
||||
setConnectionStatus(event.data);
|
||||
} else if (event.type === 'message') {
|
||||
if (event.type === "message") {
|
||||
// Handle message events (actual packet data)
|
||||
dispatch(addPacket(event.data));
|
||||
}
|
||||
},
|
||||
// On error
|
||||
() => {
|
||||
setConnectionStatus('Connection error. Reconnecting...');
|
||||
console.error('EventSource failed, reconnecting...');
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
// Clean up connection when component unmounts
|
||||
return cleanup;
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4">Mesh Network Packets</h2>
|
||||
|
||||
{/* Connection status indicator */}
|
||||
<InfoMessage
|
||||
message={`Status: ${connectionStatus}`}
|
||||
type={connectionStatus.includes('error') ? 'error' : 'info'}
|
||||
/>
|
||||
|
||||
<p className="mb-4">
|
||||
This page displays real-time packets from the Meshtastic mesh network.
|
||||
</p>
|
||||
|
||||
<PacketList />
|
||||
<div className="bg-neutral-700 rounded-lg shadow-inner">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4 text-neutral-100">
|
||||
Mesh Network Packets
|
||||
</h2>
|
||||
|
||||
<InfoMessage
|
||||
message="This page displays real-time packets from the Meshtastic mesh network."
|
||||
type="info"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<div className="p-6">
|
||||
<PacketList />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Link, Outlet } from '@tanstack/react-router';
|
||||
import { Link, Outlet } from "@tanstack/react-router";
|
||||
|
||||
export function Root() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
<div className="min-h-screen bg-neutral-100">
|
||||
<header className="bg-blue-600 text-white shadow-md">
|
||||
<div className="container mx-auto px-4 py-3">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -12,7 +12,7 @@ export function Root() {
|
||||
to="/"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
className: "font-bold underline",
|
||||
}}
|
||||
>
|
||||
Home
|
||||
@@ -21,7 +21,7 @@ export function Root() {
|
||||
to="/packets"
|
||||
className="hover:underline"
|
||||
activeProps={{
|
||||
className: 'font-bold underline',
|
||||
className: "font-bold underline",
|
||||
}}
|
||||
>
|
||||
Packets
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Additional custom styles can be added here */
|
||||
/* Set default background and text colors */
|
||||
@layer base {
|
||||
html, body {
|
||||
@apply bg-neutral-800;
|
||||
@apply text-neutral-200;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const colors = require("tailwindcss/colors");
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: { colors: { ...colors } },
|
||||
},
|
||||
darkMode: "class",
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user