270 lines
12 KiB
Vue
270 lines
12 KiB
Vue
<template>
|
|
<div class="flex flex-col flex-1 overflow-hidden min-w-full sm:min-w-[500px] bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900">
|
|
<div class="flex-1 overflow-y-auto w-full px-4 md:px-8 py-6">
|
|
<div class="space-y-4 w-full max-w-4xl mx-auto">
|
|
|
|
<div class="glass-card space-y-5">
|
|
<div class="space-y-2">
|
|
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">Diagnostics</div>
|
|
<div class="text-2xl font-semibold text-gray-900 dark:text-white">Ping Mesh Peers</div>
|
|
<div class="text-sm text-gray-600 dark:text-gray-300">Only <code class="font-mono text-xs">lxmf.delivery</code> destinations respond to ping.</div>
|
|
</div>
|
|
|
|
<div class="grid md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="glass-label">Destination Hash</label>
|
|
<input v-model="destinationHash" type="text" placeholder="e.g. 7b746057a7294469799cd8d7d429676a" class="input-field font-mono"/>
|
|
</div>
|
|
<div>
|
|
<label class="glass-label">Ping Timeout (seconds)</label>
|
|
<input v-model="timeout" type="number" min="1" class="input-field"/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
<button v-if="!isRunning" @click="start" type="button" class="primary-chip px-4 py-2 text-sm">
|
|
<MaterialDesignIcon icon-name="play" class="w-4 h-4"/>
|
|
Start Ping
|
|
</button>
|
|
<button v-else @click="stop" type="button" class="secondary-chip px-4 py-2 text-sm text-red-600 dark:text-red-300 border-red-200 dark:border-red-500/50">
|
|
<MaterialDesignIcon icon-name="pause" class="w-4 h-4"/>
|
|
Stop
|
|
</button>
|
|
<button @click="clear" type="button" class="secondary-chip px-4 py-2 text-sm">
|
|
<MaterialDesignIcon icon-name="broom" class="w-4 h-4"/>
|
|
Clear Results
|
|
</button>
|
|
<button @click="dropPath" type="button" class="inline-flex items-center gap-2 rounded-full bg-red-600/90 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-red-500 transition">
|
|
<MaterialDesignIcon icon-name="link-variant-remove" class="w-4 h-4"/>
|
|
Drop Path
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2 text-xs font-semibold">
|
|
<span :class="[isRunning ? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200' : 'bg-gray-200 text-gray-700 dark:bg-zinc-800 dark:text-gray-200', 'rounded-full px-3 py-1']">
|
|
Status: {{ isRunning ? 'Running' : 'Idle' }}
|
|
</span>
|
|
<span v-if="lastPingSummary?.duration" class="rounded-full px-3 py-1 bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-200">
|
|
Last RTT: {{ lastPingSummary.duration }}
|
|
</span>
|
|
<span v-if="lastPingSummary?.error" class="rounded-full px-3 py-1 bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-200">
|
|
Last Error: {{ lastPingSummary.error }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="glass-card flex flex-col min-h-[320px] space-y-3">
|
|
<div class="flex items-center justify-between gap-4">
|
|
<div>
|
|
<div class="text-sm font-semibold text-gray-900 dark:text-white">Console Output</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">Streaming seq responses in real time</div>
|
|
</div>
|
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
seq #{{ seq }}
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="lastPingSummary && !lastPingSummary.error" class="flex flex-wrap gap-2 text-xs text-gray-700 dark:text-gray-200">
|
|
<span v-if="lastPingSummary.hopsThere != null" class="stat-chip">Hops there: {{ lastPingSummary.hopsThere }}</span>
|
|
<span v-if="lastPingSummary.hopsBack != null" class="stat-chip">Hops back: {{ lastPingSummary.hopsBack }}</span>
|
|
<span v-if="lastPingSummary.rssi != null" class="stat-chip">RSSI {{ lastPingSummary.rssi }} dBm</span>
|
|
<span v-if="lastPingSummary.snr != null" class="stat-chip">SNR {{ lastPingSummary.snr }} dB</span>
|
|
<span v-if="lastPingSummary.quality != null" class="stat-chip">Quality {{ lastPingSummary.quality }}%</span>
|
|
<span v-if="lastPingSummary.via" class="stat-chip">Interface {{ lastPingSummary.via }}</span>
|
|
</div>
|
|
|
|
<div id="results" class="flex-1 overflow-y-auto rounded-2xl bg-black/80 text-emerald-300 font-mono text-xs p-3 space-y-1 shadow-inner border border-zinc-900">
|
|
<div v-if="pingResults.length === 0" class="text-emerald-500/80">No pings yet. Start a run to collect RTT data.</div>
|
|
<div v-for="(pingResult, index) in pingResults" :key="`${index}-${pingResult}`" class="whitespace-pre-wrap">{{ pingResult }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import {CanceledError} from "axios";
|
|
import DialogUtils from "../../js/DialogUtils";
|
|
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
|
|
|
export default {
|
|
name: 'PingPage',
|
|
components: {
|
|
MaterialDesignIcon,
|
|
},
|
|
data() {
|
|
return {
|
|
isRunning: false,
|
|
destinationHash: null,
|
|
timeout: 10,
|
|
seq: 0,
|
|
pingResults: [],
|
|
abortController: null,
|
|
lastPingSummary: null,
|
|
};
|
|
},
|
|
beforeUnmount() {
|
|
this.stop();
|
|
},
|
|
methods: {
|
|
async start() {
|
|
|
|
// do nothing if already running
|
|
if(this.isRunning){
|
|
return;
|
|
}
|
|
|
|
// simple check to ensure destination hash is valid
|
|
if(this.destinationHash == null || this.destinationHash.length !== 32){
|
|
DialogUtils.alert("Invalid Destination Hash!");
|
|
return;
|
|
}
|
|
|
|
// simple check to ensure destination hash is valid
|
|
if(this.timeout == null || this.timeout < 0){
|
|
DialogUtils.alert("Timeout must be a number!");
|
|
return;
|
|
}
|
|
|
|
// we are now running ping
|
|
this.seq = 0;
|
|
this.isRunning = true;
|
|
this.abortController = new AbortController();
|
|
|
|
// run ping until stopped
|
|
while(this.isRunning){
|
|
|
|
// run ping
|
|
await this.ping();
|
|
|
|
// wait a bit before running next ping
|
|
await this.sleep(1000);
|
|
|
|
}
|
|
|
|
},
|
|
async stop() {
|
|
this.isRunning = false;
|
|
if(this.abortController){
|
|
this.abortController.abort();
|
|
}
|
|
},
|
|
async clear() {
|
|
this.pingResults = [];
|
|
this.lastPingSummary = null;
|
|
},
|
|
async sleep(millis) {
|
|
return new Promise((resolve, reject) => setTimeout(resolve, millis));
|
|
},
|
|
async ping() {
|
|
try {
|
|
|
|
this.seq++;
|
|
|
|
// ping destination
|
|
const response = await window.axios.get(`/api/v1/ping/${this.destinationHash}/lxmf.delivery`, {
|
|
signal: this.abortController.signal,
|
|
params: {
|
|
timeout: this.timeout,
|
|
},
|
|
});
|
|
|
|
const pingResult = response.data.ping_result;
|
|
const rttMilliseconds = (pingResult.rtt * 1000).toFixed(3);
|
|
const rttDurationString = `${rttMilliseconds}ms`;
|
|
|
|
const info = [
|
|
`seq=${this.seq}`,
|
|
`duration=${rttDurationString}`,
|
|
`hops_there=${pingResult.hops_there}`,
|
|
`hops_back=${pingResult.hops_back}`,
|
|
];
|
|
|
|
// add rssi if available
|
|
if(pingResult.rssi != null){
|
|
info.push(`rssi=${pingResult.rssi}dBm`);
|
|
}
|
|
|
|
// add snr if available
|
|
if(pingResult.snr != null){
|
|
info.push(`snr=${pingResult.snr}dB`);
|
|
}
|
|
|
|
// add signal quality if available
|
|
if(pingResult.quality != null){
|
|
info.push(`quality=${pingResult.quality}%`);
|
|
}
|
|
|
|
// add receiving interface
|
|
info.push(`via=${pingResult.receiving_interface}`);
|
|
|
|
// update ui
|
|
this.addPingResult(info.join(" "));
|
|
this.lastPingSummary = {
|
|
duration: rttDurationString,
|
|
hopsThere: pingResult.hops_there,
|
|
hopsBack: pingResult.hops_back,
|
|
rssi: pingResult.rssi,
|
|
snr: pingResult.snr,
|
|
quality: pingResult.quality,
|
|
via: pingResult.receiving_interface,
|
|
};
|
|
|
|
} catch(e) {
|
|
|
|
// ignore cancelled error
|
|
if(e instanceof CanceledError){
|
|
return;
|
|
}
|
|
|
|
console.log(e);
|
|
|
|
// add ping error to results
|
|
const message = e.response?.data?.message ?? e;
|
|
this.addPingResult(`seq=${this.seq} error=${message}`);
|
|
this.lastPingSummary = {
|
|
error: typeof message === "string" ? message : JSON.stringify(message),
|
|
};
|
|
|
|
}
|
|
},
|
|
async dropPath() {
|
|
|
|
// simple check to ensure destination hash is valid
|
|
if(this.destinationHash == null || this.destinationHash.length !== 32){
|
|
DialogUtils.alert("Invalid Destination Hash!");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await window.axios.post(`/api/v1/destination/${this.destinationHash}/drop-path`);
|
|
DialogUtils.alert(response.data.message);
|
|
} catch(e) {
|
|
console.log(e);
|
|
const message = e.response?.data?.message ?? `Failed to drop path: ${e}`;
|
|
DialogUtils.alert(message);
|
|
}
|
|
|
|
},
|
|
addPingResult(result) {
|
|
this.pingResults.push(result);
|
|
this.scrollPingResultsToBottom();
|
|
},
|
|
scrollPingResultsToBottom: function() {
|
|
// next tick waits for the ui to have the new elements added
|
|
this.$nextTick(() => {
|
|
// set timeout with zero millis seems to fix issue where it doesn't scroll all the way to the bottom...
|
|
setTimeout(() => {
|
|
const container = document.getElementById("results");
|
|
if(container){
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
}, 0);
|
|
});
|
|
},
|
|
},
|
|
}
|
|
</script>
|