mirror of
https://github.com/liamcottle/reticulum-meshchat.git
synced 2026-04-28 09:43:13 +00:00
381 lines
No EOL
15 KiB
HTML
381 lines
No EOL
15 KiB
HTML
<html>
|
|
<head>
|
|
|
|
<meta charset="UTF-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
<title>Network | Reticulum WebChat</title>
|
|
|
|
<!-- scripts -->
|
|
<script src="assets/js/tailwindcss/tailwind-v3.4.3-forms-v0.5.7.js"></script>
|
|
<script src="assets/js/axios@1.6.8/dist/axios.min.js"></script>
|
|
<script src="assets/js/vue@3.4.26/dist/vue.global.js"></script>
|
|
|
|
<!-- anychart -->
|
|
<script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-core.min.js"></script>
|
|
<script src="https://cdn.anychart.com/releases/8.12.1/js/anychart-graph.min.js"></script>
|
|
|
|
</head>
|
|
<body class="bg-gray-100">
|
|
<div id="app">
|
|
<div id="chart" class="w-full h-full"></div>
|
|
</div>
|
|
<script>
|
|
Vue.createApp({
|
|
data() {
|
|
return {
|
|
config: null,
|
|
interfaces: [],
|
|
pathTable: [],
|
|
announces: {},
|
|
};
|
|
},
|
|
mounted: function() {
|
|
this.update();
|
|
},
|
|
methods: {
|
|
async getInterfaceStats() {
|
|
try {
|
|
const response = await axios.get(`/api/v1/interface-stats`);
|
|
this.interfaces = response.data.interface_stats?.interfaces ?? [];
|
|
} catch(e) {
|
|
alert("failed to load interface stats");
|
|
console.log(e);
|
|
}
|
|
},
|
|
async getPathTable() {
|
|
try {
|
|
const response = await axios.get(`/api/v1/path-table`);
|
|
this.pathTable = response.data.path_table;
|
|
} catch(e) {
|
|
alert("failed to load path table");
|
|
console.log(e);
|
|
}
|
|
},
|
|
async getConfig() {
|
|
try {
|
|
const response = await axios.get("/api/v1/config");
|
|
this.config = response.data.config;
|
|
} catch(e) {
|
|
alert("failed to load config");
|
|
console.error(e);
|
|
}
|
|
},
|
|
async getAnnounces() {
|
|
try {
|
|
|
|
// fetch announces
|
|
const response = await window.axios.get(`/api/v1/announces`);
|
|
|
|
// cache announces
|
|
this.announces = {};
|
|
for(const announce of response.data.announces){
|
|
this.announces[announce.destination_hash] = announce;
|
|
}
|
|
|
|
} catch(e) {
|
|
// do nothing if failed to load announces
|
|
console.log(e);
|
|
}
|
|
},
|
|
async update() {
|
|
|
|
await this.getConfig();
|
|
await this.getInterfaceStats();
|
|
await this.getPathTable();
|
|
await this.getAnnounces();
|
|
|
|
const nodes = [];
|
|
const edges = [];
|
|
|
|
// add me
|
|
nodes.push({
|
|
id: "me",
|
|
group: "me",
|
|
});
|
|
|
|
// add interfaces
|
|
for(const entry of this.interfaces){
|
|
|
|
// add interface node
|
|
nodes.push({
|
|
id: entry.name,
|
|
group: "interface",
|
|
meta: {
|
|
interface: entry,
|
|
},
|
|
fill: {
|
|
// interface colour by status
|
|
color: entry.status ? '#22c55e' : '#ef4444',
|
|
},
|
|
});
|
|
|
|
// add edge from me to interface
|
|
edges.push({
|
|
"from": "me",
|
|
"to": entry.name,
|
|
});
|
|
|
|
}
|
|
|
|
// add paths for announces
|
|
for(const entry of this.pathTable){
|
|
|
|
// find what announced this path, or skip showing it for now
|
|
const announce = this.announces[entry.hash];
|
|
if(!announce){
|
|
continue;
|
|
}
|
|
|
|
// add node
|
|
nodes.push({
|
|
id: entry.hash,
|
|
group: `aspect:${announce.aspect}`,
|
|
meta: {
|
|
announce: announce,
|
|
path: entry,
|
|
},
|
|
});
|
|
|
|
// hops == 1 -> show node on the interface itself
|
|
// hops == 2 -> show node on first intermediate hop
|
|
// hops >= 3 -> show node on next intermediate hop
|
|
|
|
// add edge from interface to announced aspect
|
|
// if(entry.hops === 1){
|
|
if(true){
|
|
edges.push({
|
|
"from": entry.interface,
|
|
"to": entry.hash,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// this path entry has multiple hops, create intermediate hop nodes so we can visualise each hop
|
|
var intermediateHopNodeId = null;
|
|
for(var i = 1; i < entry.hops; i++){
|
|
|
|
const hopNumber = i + 1;
|
|
const previousIntermediateHopNodeId = intermediateHopNodeId;
|
|
intermediateHopNodeId = `${entry.interface}_hops_${hopNumber}`;
|
|
|
|
// find existing intermediate hop node, or create it if it doesn't exist
|
|
const existingIntermediateHopNode = nodes.find((node) => node.id === intermediateHopNodeId);
|
|
if(!existingIntermediateHopNode){
|
|
|
|
// add intermediate hop node
|
|
nodes.push({
|
|
id: intermediateHopNodeId,
|
|
group: `intermediate-hop-node`,
|
|
meta: {
|
|
interface_name: entry.interface,
|
|
hop_number: hopNumber,
|
|
},
|
|
});
|
|
|
|
// if there is no previous intermediate hop node, we should add an edge to the actual interface
|
|
if(previousIntermediateHopNodeId == null){
|
|
edges.push({
|
|
"from": entry.interface,
|
|
"to": intermediateHopNodeId,
|
|
});
|
|
} else {
|
|
edges.push({
|
|
"from": previousIntermediateHopNodeId,
|
|
"to": intermediateHopNodeId,
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// add edge from intermediate hop node to announced aspect
|
|
if(intermediateHopNodeId){
|
|
edges.push({
|
|
"from": intermediateHopNodeId,
|
|
"to": entry.hash,
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
// create a network graph chart
|
|
var chart = anychart.graph({
|
|
"nodes": nodes,
|
|
"edges": edges,
|
|
});
|
|
|
|
// set the title and enable tooltips
|
|
chart.title("Reticulum Mesh Network");
|
|
chart.nodes().tooltip().enabled(true);
|
|
|
|
// set default node style
|
|
chart.nodes()
|
|
.height(10)
|
|
.stroke('black')
|
|
.fill('#6b7280');
|
|
|
|
// set styles for group
|
|
const meGroup = chart.group('me');
|
|
if(meGroup){
|
|
meGroup.height(50)
|
|
.shape('circle')
|
|
.stroke('black')
|
|
.fill('#EAB308');
|
|
}
|
|
|
|
// set styles for group
|
|
const interfaceGroup = chart.group('interface');
|
|
if(interfaceGroup){
|
|
interfaceGroup
|
|
.height(25)
|
|
.shape('circle')
|
|
.stroke('black')
|
|
.fill('#22C55E');
|
|
}
|
|
|
|
// set styles for group
|
|
const intermediateHopNodeGroup = chart.group('intermediate-hop-node');
|
|
if(intermediateHopNodeGroup){
|
|
intermediateHopNodeGroup
|
|
.height(25)
|
|
.shape('circle')
|
|
.stroke('black')
|
|
.fill('#6b7280');
|
|
}
|
|
|
|
// set styles for group
|
|
const lxmfDeliveryGroup = chart.group('aspect:lxmf.delivery');
|
|
if(lxmfDeliveryGroup){
|
|
lxmfDeliveryGroup
|
|
.height(10)
|
|
.shape('circle')
|
|
.stroke('black')
|
|
.fill('#3b82f6');
|
|
}
|
|
|
|
// set styles for group
|
|
const nomadNetworkNodeGroup = chart.group('aspect:nomadnetwork.node');
|
|
if(nomadNetworkNodeGroup){
|
|
nomadNetworkNodeGroup
|
|
.height(10)
|
|
.shape('circle')
|
|
.stroke('black')
|
|
.fill('#3b82f6');
|
|
}
|
|
|
|
// configure tooltips for nodes
|
|
chart.nodes().tooltip().format((context) => {
|
|
|
|
// node id we want to make a tooltip for
|
|
const nodeId = context.id;
|
|
|
|
// find existing node by id
|
|
const existingNode = nodes.find((node) => node.id === nodeId);
|
|
if(existingNode){
|
|
|
|
// format tooltip for me
|
|
if(existingNode.group === "me"){
|
|
return [
|
|
`${this.config?.display_name ?? 'This Device'}`,
|
|
`Identity: ${this.config?.identity_hash ?? 'Unknown'}`,
|
|
].join("\n");
|
|
}
|
|
|
|
// format tooltip for interface
|
|
if(existingNode.group === "interface"){
|
|
const _interface = existingNode.meta.interface;
|
|
return [
|
|
`Interface`,
|
|
`Name: ${_interface.name}`,
|
|
`State: ${_interface.status ? 'Online' : 'Offline'}`,
|
|
`Bitrate: ${this.formatBitsPerSecond(_interface.bitrate)}`,
|
|
`TX: ${this.formatBytes(_interface.txb)}`,
|
|
`RX: ${this.formatBytes(_interface.rxb)}`,
|
|
].join("\n");
|
|
}
|
|
|
|
// format tooltip for intermediate-hop-node
|
|
if(existingNode.group === "intermediate-hop-node"){
|
|
const iterfaceName = existingNode.meta.interface_name;
|
|
const hopNumber = existingNode.meta.hop_number;
|
|
return [
|
|
`${hopNumber} ${hopNumber === 1 ? 'Hop' : 'Hops'} via ${iterfaceName}`,
|
|
].join("\n");
|
|
}
|
|
|
|
// format tooltip for lxmf.delivery
|
|
if(existingNode.group === "aspect:lxmf.delivery"){
|
|
const _announce = existingNode.meta.announce;
|
|
const _path = existingNode.meta.path;
|
|
const name = _announce.app_data ? atob(_announce.app_data) : "Anonymous Peer";
|
|
return [
|
|
`Name: ${name}`,
|
|
`Aspect: ${_announce.aspect}`,
|
|
`Identity: ${_announce.identity_hash}`,
|
|
`Destination: ${_announce.destination_hash}`,
|
|
`Path: ${_path.hops} ${_path.hops === 1 ? 'Hop' : 'Hops'} via ${_path.interface}`,
|
|
`Announced At: ${_announce.updated_at}`,
|
|
].join("\n");
|
|
}
|
|
|
|
// format tooltip for nomadnetwork.node
|
|
if(existingNode.group === "aspect:nomadnetwork.node"){
|
|
const _announce = existingNode.meta.announce;
|
|
const _path = existingNode.meta.path;
|
|
const name = _announce.app_data ? atob(_announce.app_data) : "Anonymous Node";
|
|
return [
|
|
`Name: ${name}`,
|
|
`Aspect: ${_announce.aspect}`,
|
|
`Identity: ${_announce.identity_hash}`,
|
|
`Destination: ${_announce.destination_hash}`,
|
|
`Path: ${_path.hops} ${_path.hops === 1 ? 'Hop' : 'Hops'} via ${_path.interface}`,
|
|
`Announced At: ${_announce.updated_at}`,
|
|
].join("\n");
|
|
}
|
|
|
|
}
|
|
|
|
// fallback to showing id
|
|
return nodeId;
|
|
|
|
});
|
|
|
|
// draw the chart
|
|
chart.container("chart").draw();
|
|
|
|
},
|
|
formatBytes: function(bytes) {
|
|
|
|
if(bytes === 0){
|
|
return '0 Bytes';
|
|
}
|
|
|
|
const k = 1024;
|
|
const decimals = 0;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
|
|
|
},
|
|
formatBitsPerSecond: function(bits) {
|
|
if (bits === 0) {
|
|
return '0 bps';
|
|
}
|
|
|
|
const k = 1000; // Use 1000 instead of 1024 for network speeds
|
|
const decimals = 0;
|
|
const sizes = ['bps', 'kbps', 'Mbps', 'Gbps', 'Tbps', 'Pbps', 'Ebps', 'Zbps', 'Ybps'];
|
|
|
|
const i = Math.floor(Math.log(bits) / Math.log(k));
|
|
|
|
return parseFloat((bits / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
|
},
|
|
},
|
|
}).mount('#app');
|
|
</script>
|
|
</body>
|
|
</html> |