mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-03 07:22:31 +02:00
Draft 1 of the homebaked visualizer
This commit is contained in:
+542
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-542
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -13,8 +13,8 @@
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<script type="module" crossorigin src="/assets/index-D6IU3K0A.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CcK6B7WY.css">
|
||||
<script type="module" crossorigin src="/assets/index-B7TRWRoH.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-tIXvIK_l.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Generated
+432
-2
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "remoteterm-meshcore-frontend",
|
||||
"version": "1.2.0",
|
||||
"version": "1.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "remoteterm-meshcore-frontend",
|
||||
"version": "1.2.0",
|
||||
"version": "1.4.1",
|
||||
"dependencies": {
|
||||
"@michaelhart/meshcore-decoder": "^0.2.7",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
@@ -16,12 +17,14 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"meshcore-hashtag-cracker": "^1.6.0",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-force-graph-2d": "^1.29.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
@@ -31,6 +34,7 @@
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@testing-library/jest-dom": "^6.6.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^18.3.12",
|
||||
@@ -687,6 +691,8 @@
|
||||
},
|
||||
"node_modules/@michaelhart/meshcore-decoder": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@michaelhart/meshcore-decoder/-/meshcore-decoder-0.2.7.tgz",
|
||||
"integrity": "sha512-a3zNbqeACibYy7XlMx6F2fMfg8FT+mP7lcYCmr8EvQk2MvBw7Qs7+bV2DAnwTf+51IRZqFEPCq55GB0scsw1WA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@noble/ed25519": "^2.3.0",
|
||||
@@ -1429,6 +1435,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@tweenjs/tween.js": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz",
|
||||
"integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "5.0.4",
|
||||
"dev": true,
|
||||
@@ -1472,6 +1484,13 @@
|
||||
"@babel/types": "^7.28.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-force": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"dev": true,
|
||||
@@ -1893,6 +1912,15 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/accessor-fn": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz",
|
||||
"integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
@@ -2069,6 +2097,16 @@
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/bezier-js": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz",
|
||||
"integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"license": "MIT",
|
||||
@@ -2188,6 +2226,18 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvas-color-tracker": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz",
|
||||
"integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tinycolor2": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/chai": {
|
||||
"version": "5.3.3",
|
||||
"dev": true,
|
||||
@@ -2387,6 +2437,236 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-binarytree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-drag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-selection": "3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
||||
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force-3d": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
||||
"integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-binarytree": "1",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-octree": "1",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
|
||||
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-octree": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz",
|
||||
"integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-selection": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-transition": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-ease": "1 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2 - 3"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-zoom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-interpolate": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-transition": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"dev": true,
|
||||
@@ -2901,6 +3181,46 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/float-tooltip": {
|
||||
"version": "1.7.5",
|
||||
"resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz",
|
||||
"integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-selection": "2 - 3",
|
||||
"kapsule": "^1.16",
|
||||
"preact": "10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/force-graph": {
|
||||
"version": "1.51.0",
|
||||
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.51.0.tgz",
|
||||
"integrity": "sha512-aTnihCmiMA0ItLJLCbrQYS9mzriopW24goFPgUnKAAmAlPogTSmFWqoBPMXzIfPb7bs04Hur5zEI4WYgLW3Sig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tweenjs/tween.js": "18 - 25",
|
||||
"accessor-fn": "1",
|
||||
"bezier-js": "3 - 6",
|
||||
"canvas-color-tracker": "^1.3",
|
||||
"d3-array": "1 - 3",
|
||||
"d3-drag": "2 - 3",
|
||||
"d3-force-3d": "2 - 3",
|
||||
"d3-scale": "1 - 4",
|
||||
"d3-scale-chromatic": "1 - 3",
|
||||
"d3-selection": "2 - 3",
|
||||
"d3-zoom": "2 - 3",
|
||||
"float-tooltip": "^1.7",
|
||||
"index-array-by": "1",
|
||||
"kapsule": "^1.16",
|
||||
"lodash-es": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"dev": true,
|
||||
@@ -3152,6 +3472,24 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/index-array-by": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz",
|
||||
"integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"license": "MIT",
|
||||
@@ -3211,6 +3549,15 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jerrypick": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz",
|
||||
"integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/jiti": {
|
||||
"version": "1.21.7",
|
||||
"license": "MIT",
|
||||
@@ -3317,6 +3664,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/kapsule": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz",
|
||||
"integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash-es": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3377,6 +3736,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.22",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz",
|
||||
"integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -3860,6 +4225,16 @@
|
||||
"version": "4.2.0",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.28.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.2.tgz",
|
||||
"integrity": "sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -3900,6 +4275,23 @@
|
||||
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types/node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"dev": true,
|
||||
@@ -3947,12 +4339,44 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-force-graph-2d": {
|
||||
"version": "1.29.0",
|
||||
"resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.29.0.tgz",
|
||||
"integrity": "sha512-Xv5IIk+hsZmB3F2ibja/t6j/b0/1T9dtFOQacTUoLpgzRHrO6wPu1GtQ2LfRqI/imgtaapnXUgQaE8g8enPo5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"force-graph": "^1.51",
|
||||
"prop-types": "15",
|
||||
"react-kapsule": "^2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/react-kapsule": {
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz",
|
||||
"integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jerrypick": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-leaflet": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz",
|
||||
@@ -4397,6 +4821,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinycolor2": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyexec": {
|
||||
"version": "0.3.2",
|
||||
"dev": true,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"format:check": "prettier --check src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@michaelhart/meshcore-decoder": "^0.2.7",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-label": "^2.1.8",
|
||||
@@ -23,12 +24,14 @@
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.562.0",
|
||||
"meshcore-hashtag-cracker": "^1.6.0",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-force-graph-2d": "^1.29.0",
|
||||
"react-leaflet": "^4.2.1",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
@@ -38,6 +41,7 @@
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@testing-library/jest-dom": "^6.6.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/react": "^18.3.12",
|
||||
|
||||
@@ -15,6 +15,7 @@ import { NewMessageModal } from './components/NewMessageModal';
|
||||
import { SettingsModal } from './components/SettingsModal';
|
||||
import { RawPacketList } from './components/RawPacketList';
|
||||
import { MapView } from './components/MapView';
|
||||
import { VisualizerView } from './components/VisualizerView';
|
||||
import { CrackerPanel } from './components/CrackerPanel';
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from './components/ui/sheet';
|
||||
import { Toaster, toast } from './components/ui/sonner';
|
||||
@@ -370,6 +371,9 @@ export function App() {
|
||||
mapFocusKey: hashConv.mapFocusKey,
|
||||
};
|
||||
}
|
||||
if (hashConv.type === 'visualizer') {
|
||||
return { type: 'visualizer', id: 'visualizer', name: 'Mesh Visualizer' };
|
||||
}
|
||||
if (hashConv.type === 'channel') {
|
||||
const channel = channels.find(
|
||||
(c) => c.name === hashConv.name || c.name === `#${hashConv.name}`
|
||||
@@ -716,6 +720,13 @@ export function App() {
|
||||
<MapView contacts={contacts} focusedKey={activeConversation.mapFocusKey} />
|
||||
</div>
|
||||
</>
|
||||
) : activeConversation.type === 'visualizer' ? (
|
||||
<VisualizerView
|
||||
packets={rawPackets}
|
||||
contacts={contacts}
|
||||
channels={channels}
|
||||
config={config}
|
||||
/>
|
||||
) : activeConversation.type === 'raw' ? (
|
||||
<>
|
||||
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium text-lg">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useMemo } from 'react';
|
||||
import { MeshCoreDecoder, PayloadType, Utils } from '@michaelhart/meshcore-decoder';
|
||||
import type { RawPacket } from '../types';
|
||||
|
||||
interface RawPacketListProps {
|
||||
@@ -10,30 +11,6 @@ function formatTime(timestamp: number): string {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
function formatPayloadType(type: string): string {
|
||||
// Convert SNAKE_CASE to Title Case
|
||||
return type
|
||||
.split('_')
|
||||
.map((word) => word.charAt(0) + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function getDecryptedLabel(packet: RawPacket): string {
|
||||
if (!packet.decrypted || !packet.decrypted_info) {
|
||||
return formatPayloadType(packet.payload_type);
|
||||
}
|
||||
|
||||
const info = packet.decrypted_info;
|
||||
if (packet.payload_type === 'GROUP_TEXT' && info.channel_name) {
|
||||
return `GroupText to ${info.channel_name}`;
|
||||
}
|
||||
if (packet.payload_type === 'TEXT_MESSAGE' && info.sender) {
|
||||
return `TextMessage from ${info.sender}`;
|
||||
}
|
||||
|
||||
return formatPayloadType(packet.payload_type);
|
||||
}
|
||||
|
||||
function formatSignalInfo(packet: RawPacket): string {
|
||||
const parts: string[] = [];
|
||||
if (packet.snr !== null && packet.snr !== undefined) {
|
||||
@@ -45,9 +22,170 @@ function formatSignalInfo(packet: RawPacket): string {
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
// Decrypted info from the packet (validated by backend)
|
||||
interface DecryptedInfo {
|
||||
channel_name: string | null;
|
||||
sender: string | null;
|
||||
}
|
||||
|
||||
// Decode a packet and generate a human-readable summary
|
||||
// Uses backend's decrypted_info when available (validated), falls back to decoder
|
||||
function decodePacketSummary(
|
||||
hexData: string,
|
||||
decryptedInfo: DecryptedInfo | null
|
||||
): {
|
||||
summary: string;
|
||||
routeType: string;
|
||||
details?: string;
|
||||
} {
|
||||
try {
|
||||
const decoded = MeshCoreDecoder.decode(hexData);
|
||||
|
||||
if (!decoded.isValid) {
|
||||
return { summary: 'Invalid packet', routeType: 'Unknown' };
|
||||
}
|
||||
|
||||
const routeType = Utils.getRouteTypeName(decoded.routeType);
|
||||
const payloadTypeName = Utils.getPayloadTypeName(decoded.payloadType);
|
||||
|
||||
// Build path string if available
|
||||
const pathStr = decoded.path && decoded.path.length > 0 ? ` via ${decoded.path.join('-')}` : '';
|
||||
|
||||
// Generate summary based on payload type
|
||||
let summary = payloadTypeName;
|
||||
let details: string | undefined;
|
||||
|
||||
switch (decoded.payloadType) {
|
||||
case PayloadType.TextMessage: {
|
||||
const payload = decoded.payload.decoded as {
|
||||
destinationHash?: string;
|
||||
sourceHash?: string;
|
||||
} | null;
|
||||
if (payload?.sourceHash && payload?.destinationHash) {
|
||||
summary = `DM from ${payload.sourceHash} to ${payload.destinationHash}${pathStr}`;
|
||||
} else {
|
||||
summary = `DM${pathStr}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PayloadType.GroupText: {
|
||||
const payload = decoded.payload.decoded as {
|
||||
channelHash?: string;
|
||||
} | null;
|
||||
// Use backend's validated decrypted_info when available
|
||||
if (decryptedInfo?.channel_name) {
|
||||
if (decryptedInfo.sender) {
|
||||
summary = `GT from ${decryptedInfo.sender} in ${decryptedInfo.channel_name}${pathStr}`;
|
||||
} else {
|
||||
summary = `GT in ${decryptedInfo.channel_name}${pathStr}`;
|
||||
}
|
||||
} else if (payload?.channelHash) {
|
||||
// Fallback to showing channel hash when not decrypted
|
||||
summary = `GT ch:${payload.channelHash}${pathStr}`;
|
||||
} else {
|
||||
summary = `GroupText${pathStr}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PayloadType.Advert: {
|
||||
const payload = decoded.payload.decoded as {
|
||||
publicKey?: string;
|
||||
appData?: { name?: string; deviceRole?: number };
|
||||
} | null;
|
||||
if (payload?.appData?.name) {
|
||||
const role =
|
||||
payload.appData.deviceRole !== undefined
|
||||
? Utils.getDeviceRoleName(payload.appData.deviceRole)
|
||||
: '';
|
||||
summary = `Advert: ${payload.appData.name}${role ? ` (${role})` : ''}${pathStr}`;
|
||||
} else if (payload?.publicKey) {
|
||||
summary = `Advert: ${payload.publicKey.slice(0, 8)}...${pathStr}`;
|
||||
} else {
|
||||
summary = `Advert${pathStr}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case PayloadType.Ack: {
|
||||
summary = `ACK${pathStr}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case PayloadType.Request: {
|
||||
summary = `Request${pathStr}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case PayloadType.Response: {
|
||||
summary = `Response${pathStr}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case PayloadType.Trace: {
|
||||
summary = `Trace${pathStr}`;
|
||||
break;
|
||||
}
|
||||
|
||||
case PayloadType.Path: {
|
||||
summary = `Path${pathStr}`;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
summary = `${payloadTypeName}${pathStr}`;
|
||||
}
|
||||
|
||||
return { summary, routeType, details };
|
||||
} catch {
|
||||
return { summary: 'Decode error', routeType: 'Unknown' };
|
||||
}
|
||||
}
|
||||
|
||||
// Get route type badge color
|
||||
function getRouteTypeColor(routeType: string): string {
|
||||
switch (routeType) {
|
||||
case 'Flood':
|
||||
return 'bg-blue-500/20 text-blue-400';
|
||||
case 'Direct':
|
||||
return 'bg-green-500/20 text-green-400';
|
||||
case 'Transport Flood':
|
||||
return 'bg-purple-500/20 text-purple-400';
|
||||
case 'Transport Direct':
|
||||
return 'bg-orange-500/20 text-orange-400';
|
||||
default:
|
||||
return 'bg-gray-500/20 text-gray-400';
|
||||
}
|
||||
}
|
||||
|
||||
// Get short route type label
|
||||
function getRouteTypeLabel(routeType: string): string {
|
||||
switch (routeType) {
|
||||
case 'Flood':
|
||||
return 'F';
|
||||
case 'Direct':
|
||||
return 'D';
|
||||
case 'Transport Flood':
|
||||
return 'TF';
|
||||
case 'Transport Direct':
|
||||
return 'TD';
|
||||
default:
|
||||
return '?';
|
||||
}
|
||||
}
|
||||
|
||||
export function RawPacketList({ packets }: RawPacketListProps) {
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Decode all packets (memoized to avoid re-decoding on every render)
|
||||
const decodedPackets = useMemo(() => {
|
||||
return packets.map((packet) => ({
|
||||
packet,
|
||||
decoded: decodePacketSummary(packet.data, packet.decrypted_info),
|
||||
}));
|
||||
}, [packets]);
|
||||
|
||||
useEffect(() => {
|
||||
if (listRef.current) {
|
||||
listRef.current.scrollTop = listRef.current.scrollHeight;
|
||||
@@ -63,24 +201,44 @@ export function RawPacketList({ packets }: RawPacketListProps) {
|
||||
}
|
||||
|
||||
// Sort packets by timestamp ascending (oldest first)
|
||||
const sortedPackets = [...packets].sort((a, b) => a.timestamp - b.timestamp);
|
||||
const sortedPackets = [...decodedPackets].sort((a, b) => a.packet.timestamp - b.packet.timestamp);
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-y-auto p-4 flex flex-col gap-3" ref={listRef}>
|
||||
{sortedPackets.map((packet) => (
|
||||
{sortedPackets.map(({ packet, decoded }) => (
|
||||
<div key={packet.id} className="py-2 px-3 bg-muted rounded">
|
||||
<div className={packet.decrypted ? 'text-primary' : 'text-destructive'}>
|
||||
{!packet.decrypted && <span className="mr-1">🔒</span>}
|
||||
{getDecryptedLabel(packet)}
|
||||
{' • '}
|
||||
{formatTime(packet.timestamp)}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Route type badge */}
|
||||
<span
|
||||
className={`text-[10px] font-mono px-1.5 py-0.5 rounded ${getRouteTypeColor(decoded.routeType)}`}
|
||||
title={decoded.routeType}
|
||||
>
|
||||
{getRouteTypeLabel(decoded.routeType)}
|
||||
</span>
|
||||
|
||||
{/* Encryption status */}
|
||||
{!packet.decrypted && <span title="Encrypted">🔒</span>}
|
||||
|
||||
{/* Summary */}
|
||||
<span className={packet.decrypted ? 'text-primary' : 'text-foreground'}>
|
||||
{decoded.summary}
|
||||
</span>
|
||||
|
||||
{/* Time */}
|
||||
<span className="text-muted-foreground ml-auto text-sm">
|
||||
{formatTime(packet.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Signal info */}
|
||||
{(packet.snr !== null || packet.rssi !== null) && (
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">
|
||||
{formatSignalInfo(packet)}
|
||||
</div>
|
||||
)}
|
||||
<div className="font-mono text-[11px] break-all text-muted-foreground/70 mt-1">
|
||||
|
||||
{/* Raw hex data (always visible) */}
|
||||
<div className="font-mono text-[10px] break-all text-muted-foreground/70 mt-1 p-1 bg-background/50 rounded">
|
||||
{packet.data.toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ export function Sidebar({
|
||||
onSelectConversation(conversation);
|
||||
};
|
||||
|
||||
const isActive = (type: 'contact' | 'channel' | 'raw' | 'map', id: string) =>
|
||||
const isActive = (type: 'contact' | 'channel' | 'raw' | 'map' | 'visualizer', id: string) =>
|
||||
activeConversation?.type === type && activeConversation?.id === id;
|
||||
|
||||
// Get unread count for a conversation
|
||||
@@ -288,6 +288,26 @@ export function Sidebar({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mesh Visualizer */}
|
||||
{!query && (
|
||||
<div
|
||||
className={cn(
|
||||
'px-3 py-2.5 cursor-pointer flex items-center gap-2 border-l-2 border-transparent hover:bg-accent',
|
||||
isActive('visualizer', 'visualizer') && 'bg-accent border-l-primary'
|
||||
)}
|
||||
onClick={() =>
|
||||
handleSelectConversation({
|
||||
type: 'visualizer',
|
||||
id: 'visualizer',
|
||||
name: 'Mesh Visualizer',
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="text-muted-foreground text-xs">✨</span>
|
||||
<span className="flex-1 truncate">Mesh Visualizer</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cracker Toggle */}
|
||||
{!query && (
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { useState } from 'react';
|
||||
import type { Contact, RawPacket, Channel, RadioConfig } from '../types';
|
||||
import { PacketVisualizer } from './PacketVisualizer';
|
||||
import { RawPacketList } from './RawPacketList';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface VisualizerViewProps {
|
||||
packets: RawPacket[];
|
||||
contacts: Contact[];
|
||||
channels: Channel[];
|
||||
config: RadioConfig | null;
|
||||
}
|
||||
|
||||
export function VisualizerView({ packets, contacts, channels, config }: VisualizerViewProps) {
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center px-4 py-3 border-b border-border font-medium text-lg">
|
||||
<span>Mesh Visualizer</span>
|
||||
{/* Full screen toggle - only show on larger screens */}
|
||||
<label className="hidden md:flex items-center gap-2 text-sm font-normal cursor-pointer">
|
||||
<Checkbox
|
||||
checked={fullScreen}
|
||||
onCheckedChange={(checked) => setFullScreen(checked === true)}
|
||||
/>
|
||||
<span>Full screen</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Mobile: Tabbed interface */}
|
||||
<div className="flex-1 overflow-hidden md:hidden">
|
||||
<Tabs defaultValue="visualizer" className="h-full flex flex-col">
|
||||
<TabsList className="mx-4 mt-2 grid grid-cols-2">
|
||||
<TabsTrigger value="visualizer">Visualizer</TabsTrigger>
|
||||
<TabsTrigger value="packets">Packet Feed</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="visualizer" className="flex-1 m-0 overflow-hidden">
|
||||
<PacketVisualizer
|
||||
packets={packets}
|
||||
contacts={contacts}
|
||||
channels={channels}
|
||||
config={config}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="packets" className="flex-1 m-0 overflow-hidden">
|
||||
<RawPacketList packets={packets} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* Desktop: Split screen (or full screen if toggled) */}
|
||||
<div className="hidden md:flex flex-1 overflow-hidden">
|
||||
{/* Visualizer panel */}
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden transition-all duration-200',
|
||||
fullScreen ? 'flex-1' : 'flex-1 border-r border-border'
|
||||
)}
|
||||
>
|
||||
<PacketVisualizer
|
||||
packets={packets}
|
||||
contacts={contacts}
|
||||
channels={channels}
|
||||
config={config}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Packet feed panel - hidden when full screen */}
|
||||
<div
|
||||
className={cn(
|
||||
'overflow-hidden transition-all duration-200',
|
||||
fullScreen ? 'w-0' : 'w-[45rem] lg:w-[54rem]'
|
||||
)}
|
||||
>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="px-3 py-2 border-b border-border text-sm font-medium text-muted-foreground">
|
||||
Packet Feed
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<RawPacketList packets={packets} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export interface Message {
|
||||
acked: number;
|
||||
}
|
||||
|
||||
export type ConversationType = 'contact' | 'channel' | 'raw' | 'map';
|
||||
export type ConversationType = 'contact' | 'channel' | 'raw' | 'map' | 'visualizer';
|
||||
|
||||
export interface Conversation {
|
||||
type: ConversationType;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Conversation } from '../types';
|
||||
|
||||
export interface ParsedHashConversation {
|
||||
type: 'channel' | 'contact' | 'raw' | 'map';
|
||||
type: 'channel' | 'contact' | 'raw' | 'map' | 'visualizer';
|
||||
name: string;
|
||||
/** For map view: public key prefix to focus on */
|
||||
mapFocusKey?: string;
|
||||
@@ -20,6 +20,10 @@ export function parseHashConversation(): ParsedHashConversation | null {
|
||||
return { type: 'map', name: 'map' };
|
||||
}
|
||||
|
||||
if (hash === 'visualizer') {
|
||||
return { type: 'visualizer', name: 'visualizer' };
|
||||
}
|
||||
|
||||
// Check for map with focus: #map/focus/{pubkey_prefix}
|
||||
if (hash.startsWith('map/focus/')) {
|
||||
const focusKey = hash.slice('map/focus/'.length);
|
||||
@@ -54,6 +58,7 @@ export function getConversationHash(conv: Conversation | null): string {
|
||||
if (!conv) return '';
|
||||
if (conv.type === 'raw') return '#raw';
|
||||
if (conv.type === 'map') return '#map';
|
||||
if (conv.type === 'visualizer') return '#visualizer';
|
||||
// Strip leading # from channel names for cleaner URLs
|
||||
const name =
|
||||
conv.type === 'channel' && conv.name.startsWith('#') ? conv.name.slice(1) : conv.name;
|
||||
|
||||
Reference in New Issue
Block a user