mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-06-04 10:31:21 +00:00
feat(propagation-nodes): propagation node management with new UI elements and API integration
- Added functionality to request paths for preferred propagation nodes during sync. - Introduced new Material Design icons for better visual representation of node states. - Updated settings to allow transfer limits in megabytes, improving user experience. - Enhanced localization support for new features in multiple languages. - Improved tests to cover new propagation node functionalities and UI interactions.
This commit is contained in:
@@ -1343,6 +1343,10 @@ export default {
|
||||
|
||||
// request sync
|
||||
try {
|
||||
const preferredHash = this.config?.lxmf_preferred_propagation_node_destination_hash;
|
||||
if (preferredHash) {
|
||||
await window.api.post(`/api/v1/destination/${preferredHash}/request-path`);
|
||||
}
|
||||
await window.api.get("/api/v1/lxmf/propagation-node/sync");
|
||||
} catch (e) {
|
||||
const errorMessage = e.response?.data?.message ?? this.$t("app.sync_error_generic");
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
<script>
|
||||
import * as mdi from "@mdi/js";
|
||||
|
||||
const MDI_ICON_ALIASES = {
|
||||
mdiRoute: "mdiRoutes",
|
||||
mdiEmailSendOutline: "mdiSendOutline",
|
||||
};
|
||||
|
||||
export default {
|
||||
name: "MaterialDesignIcon",
|
||||
props: {
|
||||
@@ -58,7 +63,8 @@ export default {
|
||||
}
|
||||
|
||||
const name = this.mdiIconName;
|
||||
const path = mdi[name];
|
||||
const aliasName = MDI_ICON_ALIASES[name] || name;
|
||||
const path = mdi[aliasName];
|
||||
|
||||
if (path) return path;
|
||||
|
||||
|
||||
@@ -4,101 +4,217 @@
|
||||
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gray-50 dark:bg-zinc-950">
|
||||
<div class="px-4 py-4 border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900">
|
||||
<div class="rounded-2xl border border-gray-200 dark:border-zinc-800 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="font-semibold text-gray-900 dark:text-zinc-100">Hosted Propagation Node</div>
|
||||
<span
|
||||
v-if="localPropagationNode"
|
||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold"
|
||||
:class="
|
||||
localPropagationNode.is_propagation_enabled
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-zinc-800 dark:text-zinc-300'
|
||||
"
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 text-left"
|
||||
@click="isLocalManagerCollapsed = !isLocalManagerCollapsed"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<MaterialDesignIcon
|
||||
:icon-name="isLocalManagerCollapsed ? 'chevron-right' : 'chevron-down'"
|
||||
class="size-5 text-gray-500 dark:text-zinc-400 shrink-0"
|
||||
/>
|
||||
<div class="font-semibold text-gray-900 dark:text-zinc-100 truncate">
|
||||
Hosted Propagation Node
|
||||
</div>
|
||||
<span
|
||||
v-if="localPropagationNode"
|
||||
class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold"
|
||||
:class="
|
||||
localNodeIsRunning
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
|
||||
: 'bg-gray-100 text-gray-700 dark:bg-zinc-800 dark:text-zinc-300'
|
||||
"
|
||||
>
|
||||
{{ localNodeIsRunning ? "Running" : "Stopped" }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
localPropagationNode &&
|
||||
config.lxmf_preferred_propagation_node_destination_hash ===
|
||||
localPropagationNode.destination_hash
|
||||
"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-100 dark:bg-blue-900/30 px-2 py-0.5 text-xs font-semibold text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
Preferred
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-40"
|
||||
title="Announce now"
|
||||
:disabled="!localPropagationNode"
|
||||
@click="announceNow"
|
||||
>
|
||||
{{ localPropagationNode.is_propagation_enabled ? "Running" : "Stopped" }}
|
||||
</span>
|
||||
<span
|
||||
v-if="
|
||||
localPropagationNode &&
|
||||
config.lxmf_preferred_propagation_node_destination_hash ===
|
||||
localPropagationNode.destination_hash
|
||||
"
|
||||
class="inline-flex items-center gap-1 rounded-full bg-blue-100 dark:bg-blue-900/30 px-2 py-0.5 text-xs font-semibold text-blue-700 dark:text-blue-300"
|
||||
<MaterialDesignIcon icon-name="bullhorn" class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!localNodeIsRunning"
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-emerald-600 dark:hover:text-emerald-400 disabled:opacity-40"
|
||||
title="Start node"
|
||||
:disabled="!localPropagationNode"
|
||||
@click="startLocalPropagationNode"
|
||||
>
|
||||
Preferred
|
||||
</span>
|
||||
<MaterialDesignIcon icon-name="play" class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
v-if="localNodeIsRunning"
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-amber-600 dark:hover:text-amber-400"
|
||||
title="Restart node"
|
||||
@click="restartLocalPropagationNode"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="refresh" class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
v-if="localNodeIsRunning"
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400"
|
||||
title="Stop node"
|
||||
@click="stopLocalPropagationNode"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="stop" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isLocalManagerCollapsed" class="mt-3 space-y-3">
|
||||
<div
|
||||
v-if="config.lxmf_local_propagation_node_address_hash"
|
||||
class="text-xs font-mono text-gray-600 dark:text-zinc-400 break-all"
|
||||
>
|
||||
<{{ config.lxmf_local_propagation_node_address_hash }}>
|
||||
</div>
|
||||
<div class="text-xs text-gray-600 dark:text-zinc-400 flex items-center gap-2">
|
||||
<template v-if="nodePathFor(config.lxmf_local_propagation_node_address_hash)">
|
||||
<span>{{
|
||||
formatPathLabel(nodePathFor(config.lxmf_local_propagation_node_address_hash))
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>No path yet</span>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-blue-600 dark:hover:text-blue-400 disabled:opacity-40"
|
||||
title="Find path now"
|
||||
:disabled="!config.lxmf_local_propagation_node_address_hash"
|
||||
@click="requestPathForNode(config.lxmf_local_propagation_node_address_hash)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="map-marker-path" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<label class="block text-xs text-gray-600 dark:text-zinc-400">
|
||||
Display name
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<input
|
||||
v-model.trim="localNodeDisplayNameDraft"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
class="w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl px-3 py-2"
|
||||
placeholder="Anonymous Peer"
|
||||
@keydown.enter.prevent="saveLocalNodeDisplayName"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-emerald-600 dark:hover:text-emerald-400"
|
||||
title="Save name"
|
||||
@click="saveLocalNodeDisplayName"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="check" class="size-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-gray-800 dark:hover:text-zinc-100"
|
||||
title="Reset to Anonymous"
|
||||
@click="resetLocalNodeDisplayName"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="restore" class="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<div
|
||||
v-if="localPropagationNode?.local_node_stats"
|
||||
v-if="localNodeStatsVisible"
|
||||
class="text-xs text-gray-600 dark:text-zinc-400 flex flex-wrap gap-x-3 gap-y-1"
|
||||
>
|
||||
<span>{{ formatSeconds(localPropagationNode.local_node_stats.uptime_seconds) }} uptime</span>
|
||||
<span>{{ localPropagationNode.local_node_stats.total_peers }} peers</span>
|
||||
<span>{{ localPropagationNode.local_node_stats.messagestore_count }} messages stored</span>
|
||||
<span>{{ localPropagationNode.local_node_stats.client_messages_received }} received</span>
|
||||
<span>{{ localPropagationNode.local_node_stats.client_messages_served }} served</span>
|
||||
<span>{{ formatStorageUsage(localPropagationNode.local_node_stats) }} storage</span>
|
||||
<span>RX {{ formatByteSize(localPropagationNode.local_node_stats.rx_bytes) }}</span>
|
||||
<span>TX {{ formatByteSize(localPropagationNode.local_node_stats.tx_bytes) }}</span>
|
||||
</div>
|
||||
<div v-else class="text-xs text-gray-500 dark:text-zinc-500">Node stats appear when running.</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<label class="text-xs text-gray-600 dark:text-zinc-400">
|
||||
Delivery transfer limit (bytes)
|
||||
Delivery transfer limit (MB)
|
||||
<input
|
||||
v-model.number="config.lxmf_delivery_transfer_limit_in_bytes"
|
||||
v-model.number="deliveryLimitInputMb"
|
||||
type="number"
|
||||
min="1000"
|
||||
min="0.001"
|
||||
step="0.01"
|
||||
class="mt-1 w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl px-3 py-2"
|
||||
@input="onDeliveryTransferLimitChange"
|
||||
/>
|
||||
<div class="mt-1 text-[11px] text-gray-500 dark:text-zinc-500">
|
||||
{{ formatByteSize(config.lxmf_delivery_transfer_limit_in_bytes) }}
|
||||
</div>
|
||||
</label>
|
||||
<label class="text-xs text-gray-600 dark:text-zinc-400">
|
||||
Propagation transfer limit (bytes)
|
||||
Propagation transfer limit (MB)
|
||||
<input
|
||||
v-model.number="config.lxmf_propagation_transfer_limit_in_bytes"
|
||||
v-model.number="propagationLimitInputMb"
|
||||
type="number"
|
||||
min="1000"
|
||||
min="0.001"
|
||||
step="0.01"
|
||||
class="mt-1 w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl px-3 py-2"
|
||||
@input="onPropagationTransferLimitChange"
|
||||
/>
|
||||
<div class="mt-1 text-[11px] text-gray-500 dark:text-zinc-500">
|
||||
{{ formatByteSize(config.lxmf_propagation_transfer_limit_in_bytes) }}
|
||||
</div>
|
||||
</label>
|
||||
<label class="text-xs text-gray-600 dark:text-zinc-400">
|
||||
Propagation sync limit (bytes)
|
||||
Propagation sync limit (MB)
|
||||
<input
|
||||
v-model.number="config.lxmf_propagation_sync_limit_in_bytes"
|
||||
v-model.number="propagationSyncLimitInputMb"
|
||||
type="number"
|
||||
min="1000"
|
||||
min="0.001"
|
||||
step="0.01"
|
||||
class="mt-1 w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl px-3 py-2"
|
||||
@input="onPropagationSyncLimitChange"
|
||||
/>
|
||||
<div class="mt-1 text-[11px] text-gray-500 dark:text-zinc-500">
|
||||
{{ formatByteSize(config.lxmf_propagation_sync_limit_in_bytes) }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<label class="block text-xs text-gray-600 dark:text-zinc-400">
|
||||
Propagation stamp cost
|
||||
<input
|
||||
v-model.number="config.lxmf_propagation_node_stamp_cost"
|
||||
type="number"
|
||||
min="13"
|
||||
max="254"
|
||||
class="mt-1 w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl px-3 py-2"
|
||||
@input="onPropagationStampCostChange"
|
||||
/>
|
||||
</label>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors disabled:opacity-40"
|
||||
:disabled="!localPropagationNode"
|
||||
@click="useLocalPropagationNode"
|
||||
>
|
||||
Use Our Node
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-xl bg-emerald-600 hover:bg-emerald-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors"
|
||||
@click="restartLocalPropagationNode"
|
||||
>
|
||||
Restart Node
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-x-1.5 rounded-xl bg-red-600 hover:bg-red-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors"
|
||||
@click="stopLocalPropagationNode"
|
||||
>
|
||||
Stop Node
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,6 +302,22 @@
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-500 mt-1">
|
||||
Announced {{ formatTimeAgo(propagationNode.updated_at) }}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-500 mt-1 flex items-center gap-2">
|
||||
<template v-if="nodePathFor(propagationNode.destination_hash)">
|
||||
<span>{{ formatPathLabel(nodePathFor(propagationNode.destination_hash)) }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>No path</span>
|
||||
</template>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-zinc-400 hover:text-blue-600 dark:hover:text-blue-400"
|
||||
title="Find path now"
|
||||
@click="requestPathForNode(propagationNode.destination_hash)"
|
||||
>
|
||||
<MaterialDesignIcon icon-name="map-marker-path" class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="propagationNode.local_node_stats"
|
||||
class="text-xs text-gray-500 dark:text-zinc-500 mt-1 flex flex-wrap gap-x-3 gap-y-1"
|
||||
@@ -193,7 +325,11 @@
|
||||
<span>{{ formatSeconds(propagationNode.local_node_stats.uptime_seconds) }} uptime</span>
|
||||
<span>{{ propagationNode.local_node_stats.total_peers }} peers</span>
|
||||
<span>{{ propagationNode.local_node_stats.messagestore_count }} stored</span>
|
||||
<span>{{ propagationNode.local_node_stats.client_messages_received }} received</span>
|
||||
<span>{{ propagationNode.local_node_stats.client_messages_served }} served</span>
|
||||
<span>{{ formatStorageUsage(propagationNode.local_node_stats) }} storage</span>
|
||||
<span>RX {{ formatByteSize(propagationNode.local_node_stats.rx_bytes) }}</span>
|
||||
<span>TX {{ formatByteSize(propagationNode.local_node_stats.tx_bytes) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
@@ -349,9 +485,13 @@
|
||||
import Utils from "../../js/Utils";
|
||||
import WebSocketConnection from "../../js/WebSocketConnection";
|
||||
import ToastUtils from "../../js/ToastUtils";
|
||||
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
|
||||
|
||||
export default {
|
||||
name: "PropagationNodesPage",
|
||||
components: {
|
||||
MaterialDesignIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
searchTerm: "",
|
||||
@@ -363,6 +503,7 @@ export default {
|
||||
lxmf_delivery_transfer_limit_in_bytes: 1000 * 1000 * 10,
|
||||
lxmf_propagation_transfer_limit_in_bytes: 1000 * 256,
|
||||
lxmf_propagation_sync_limit_in_bytes: 1000 * 10240,
|
||||
lxmf_propagation_node_stamp_cost: 16,
|
||||
},
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
@@ -370,13 +511,30 @@ export default {
|
||||
deliveryLimit: null,
|
||||
propagationLimit: null,
|
||||
propagationSyncLimit: null,
|
||||
propagationStampCost: null,
|
||||
},
|
||||
isLocalManagerCollapsed: false,
|
||||
localNodeDisplayNameDraft: "",
|
||||
deliveryLimitInputMb: 0,
|
||||
propagationLimitInputMb: 0,
|
||||
propagationSyncLimitInputMb: 0,
|
||||
nodePathsByHash: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
localPropagationNode() {
|
||||
return this.propagationNodes.find((node) => node.is_local_node) ?? null;
|
||||
},
|
||||
localNodeIsRunning() {
|
||||
const running = this.localPropagationNode?.local_node_stats?.is_running;
|
||||
if (typeof running === "boolean") {
|
||||
return running;
|
||||
}
|
||||
return Boolean(this.localPropagationNode?.is_propagation_enabled);
|
||||
},
|
||||
localNodeStatsVisible() {
|
||||
return Boolean(this.localPropagationNode?.local_node_stats && this.localNodeIsRunning);
|
||||
},
|
||||
searchedPropagationNodes() {
|
||||
return this.propagationNodes.filter((propagationNode) => {
|
||||
const search = this.searchTerm.toLowerCase();
|
||||
@@ -485,6 +643,9 @@ export default {
|
||||
// listen for websocket messages
|
||||
WebSocketConnection.on("message", this.onWebsocketMessage);
|
||||
|
||||
if (window.matchMedia && window.matchMedia("(max-width: 640px)").matches) {
|
||||
this.isLocalManagerCollapsed = true;
|
||||
}
|
||||
this.getConfig();
|
||||
this.loadPropagationNodes();
|
||||
},
|
||||
@@ -494,6 +655,7 @@ export default {
|
||||
switch (json.type) {
|
||||
case "config": {
|
||||
this.config = json.config;
|
||||
this.syncManagerInputsFromConfig();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -502,6 +664,7 @@ export default {
|
||||
try {
|
||||
const response = await window.api.get("/api/v1/config");
|
||||
this.config = response.data.config;
|
||||
this.syncManagerInputsFromConfig();
|
||||
} catch (e) {
|
||||
// do nothing if failed to load config
|
||||
console.log(e);
|
||||
@@ -511,9 +674,12 @@ export default {
|
||||
try {
|
||||
const response = await window.api.patch("/api/v1/config", config);
|
||||
this.config = response.data.config;
|
||||
this.syncManagerInputsFromConfig();
|
||||
return true;
|
||||
} catch (e) {
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
async loadPropagationNodes() {
|
||||
@@ -524,6 +690,7 @@ export default {
|
||||
},
|
||||
});
|
||||
this.propagationNodes = response.data.lxmf_propagation_nodes;
|
||||
await this.refreshPriorityNodePaths();
|
||||
} catch {
|
||||
// do nothing if failed to load
|
||||
}
|
||||
@@ -532,6 +699,7 @@ export default {
|
||||
await this.updateConfig({
|
||||
lxmf_preferred_propagation_node_destination_hash: destination_hash,
|
||||
});
|
||||
await this.requestPathForNode(destination_hash);
|
||||
},
|
||||
async stopUsingPropagationNode() {
|
||||
await this.updateConfig({
|
||||
@@ -541,12 +709,14 @@ export default {
|
||||
async useLocalPropagationNode() {
|
||||
if (!this.localPropagationNode) return;
|
||||
await this.usePropagationNode(this.localPropagationNode.destination_hash);
|
||||
await this.requestPathForNode(this.localPropagationNode.destination_hash);
|
||||
},
|
||||
async restartLocalPropagationNode() {
|
||||
try {
|
||||
await window.api.post("/api/v1/lxmf/propagation-node/restart");
|
||||
ToastUtils.success("Local propagation node restarted");
|
||||
await Promise.all([this.getConfig(), this.loadPropagationNodes()]);
|
||||
await this.refreshPriorityNodePaths();
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
}
|
||||
@@ -556,15 +726,132 @@ export default {
|
||||
await window.api.post("/api/v1/lxmf/propagation-node/stop");
|
||||
ToastUtils.success("Local propagation node stopped");
|
||||
await Promise.all([this.getConfig(), this.loadPropagationNodes()]);
|
||||
await this.refreshPriorityNodePaths();
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
}
|
||||
},
|
||||
async startLocalPropagationNode() {
|
||||
try {
|
||||
const didUpdate = await this.updateConfig({ lxmf_local_propagation_node_enabled: true });
|
||||
if (!didUpdate) {
|
||||
return;
|
||||
}
|
||||
ToastUtils.success("Local propagation node started");
|
||||
await Promise.all([this.getConfig(), this.loadPropagationNodes()]);
|
||||
await this.refreshPriorityNodePaths();
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
}
|
||||
},
|
||||
async announceNow(showSuccessToast = true) {
|
||||
try {
|
||||
await window.api.get("/api/v1/announce");
|
||||
if (showSuccessToast) {
|
||||
ToastUtils.success("Announce triggered");
|
||||
}
|
||||
await this.loadPropagationNodes();
|
||||
await this.refreshPriorityNodePaths();
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
}
|
||||
},
|
||||
async saveLocalNodeDisplayName() {
|
||||
const nextName = (this.localNodeDisplayNameDraft || "").trim() || "Anonymous Peer";
|
||||
try {
|
||||
const didUpdate = await this.updateConfig({ display_name: nextName });
|
||||
if (!didUpdate) {
|
||||
return;
|
||||
}
|
||||
this.localNodeDisplayNameDraft = nextName;
|
||||
await this.announceNow(false);
|
||||
ToastUtils.success("Name saved and announced");
|
||||
await this.loadPropagationNodes();
|
||||
await this.refreshPriorityNodePaths();
|
||||
} catch {
|
||||
ToastUtils.error(this.$t("common.save_failed"));
|
||||
}
|
||||
},
|
||||
async resetLocalNodeDisplayName() {
|
||||
this.localNodeDisplayNameDraft = "Anonymous Peer";
|
||||
await this.saveLocalNodeDisplayName();
|
||||
},
|
||||
syncManagerInputsFromConfig() {
|
||||
const displayName = (this.config.display_name || "").trim();
|
||||
this.localNodeDisplayNameDraft = displayName || "Anonymous Peer";
|
||||
this.deliveryLimitInputMb = this.bytesToMb(this.config.lxmf_delivery_transfer_limit_in_bytes);
|
||||
this.propagationLimitInputMb = this.bytesToMb(this.config.lxmf_propagation_transfer_limit_in_bytes);
|
||||
this.propagationSyncLimitInputMb = this.bytesToMb(this.config.lxmf_propagation_sync_limit_in_bytes);
|
||||
},
|
||||
bytesToMb(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0.001, Math.round((n / 1000000) * 1000) / 1000);
|
||||
},
|
||||
mbToBytes(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
return 1000;
|
||||
}
|
||||
return Math.max(1000, Math.round(n * 1000000));
|
||||
},
|
||||
async refreshPriorityNodePaths() {
|
||||
const hashes = new Set();
|
||||
const localHash = this.config.lxmf_local_propagation_node_address_hash;
|
||||
if (localHash) {
|
||||
hashes.add(localHash);
|
||||
}
|
||||
const preferredHash = this.config.lxmf_preferred_propagation_node_destination_hash;
|
||||
if (preferredHash) {
|
||||
hashes.add(preferredHash);
|
||||
}
|
||||
for (const hash of hashes) {
|
||||
await this.requestPathForNode(hash);
|
||||
}
|
||||
},
|
||||
async requestPathForNode(destinationHash) {
|
||||
const hash = (destinationHash || "").trim();
|
||||
if (!hash) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await window.api.get(`/api/v1/destination/${hash}/path`, {
|
||||
params: { request: "1", timeout: 4 },
|
||||
});
|
||||
this.nodePathsByHash = {
|
||||
...this.nodePathsByHash,
|
||||
[hash]: response.data.path || null,
|
||||
};
|
||||
} catch {
|
||||
this.nodePathsByHash = {
|
||||
...this.nodePathsByHash,
|
||||
[hash]: null,
|
||||
};
|
||||
}
|
||||
},
|
||||
nodePathFor(destinationHash) {
|
||||
const hash = (destinationHash || "").trim();
|
||||
if (!hash) {
|
||||
return null;
|
||||
}
|
||||
return this.nodePathsByHash[hash] || null;
|
||||
},
|
||||
formatPathLabel(path) {
|
||||
if (!path) {
|
||||
return "No path";
|
||||
}
|
||||
const hops = Number(path.hops);
|
||||
const hopsText = Number.isFinite(hops) ? `${hops} ${hops === 1 ? "hop" : "hops"}` : "Unknown hops";
|
||||
const iface = path.next_hop_interface || "unknown interface";
|
||||
return `${hopsText} via ${iface}`;
|
||||
},
|
||||
onDeliveryTransferLimitChange() {
|
||||
if (this.saveTimeouts.deliveryLimit) clearTimeout(this.saveTimeouts.deliveryLimit);
|
||||
this.saveTimeouts.deliveryLimit = setTimeout(async () => {
|
||||
await this.updateConfig({
|
||||
lxmf_delivery_transfer_limit_in_bytes: this.config.lxmf_delivery_transfer_limit_in_bytes,
|
||||
lxmf_delivery_transfer_limit_in_bytes: this.mbToBytes(this.deliveryLimitInputMb),
|
||||
});
|
||||
}, 450);
|
||||
},
|
||||
@@ -572,7 +859,7 @@ export default {
|
||||
if (this.saveTimeouts.propagationLimit) clearTimeout(this.saveTimeouts.propagationLimit);
|
||||
this.saveTimeouts.propagationLimit = setTimeout(async () => {
|
||||
await this.updateConfig({
|
||||
lxmf_propagation_transfer_limit_in_bytes: this.config.lxmf_propagation_transfer_limit_in_bytes,
|
||||
lxmf_propagation_transfer_limit_in_bytes: this.mbToBytes(this.propagationLimitInputMb),
|
||||
});
|
||||
}, 450);
|
||||
},
|
||||
@@ -580,7 +867,22 @@ export default {
|
||||
if (this.saveTimeouts.propagationSyncLimit) clearTimeout(this.saveTimeouts.propagationSyncLimit);
|
||||
this.saveTimeouts.propagationSyncLimit = setTimeout(async () => {
|
||||
await this.updateConfig({
|
||||
lxmf_propagation_sync_limit_in_bytes: this.config.lxmf_propagation_sync_limit_in_bytes,
|
||||
lxmf_propagation_sync_limit_in_bytes: this.mbToBytes(this.propagationSyncLimitInputMb),
|
||||
});
|
||||
}, 450);
|
||||
},
|
||||
onPropagationStampCostChange() {
|
||||
if (this.saveTimeouts.propagationStampCost) clearTimeout(this.saveTimeouts.propagationStampCost);
|
||||
this.saveTimeouts.propagationStampCost = setTimeout(async () => {
|
||||
let cost = Number(this.config.lxmf_propagation_node_stamp_cost);
|
||||
if (!Number.isFinite(cost) || cost < 13) {
|
||||
cost = 13;
|
||||
} else if (cost > 254) {
|
||||
cost = 254;
|
||||
}
|
||||
this.config.lxmf_propagation_node_stamp_cost = cost;
|
||||
await this.updateConfig({
|
||||
lxmf_propagation_node_stamp_cost: cost,
|
||||
});
|
||||
}, 450);
|
||||
},
|
||||
@@ -598,6 +900,25 @@ export default {
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d`;
|
||||
},
|
||||
formatByteSize(bytes) {
|
||||
const value = Number(bytes);
|
||||
if (!Number.isFinite(value) || value < 0) return "0 B";
|
||||
if (value < 1000) return `${Math.round(value)} B`;
|
||||
if (value < 1000 * 1000) return `${(value / 1000).toFixed(1)} KB`;
|
||||
if (value < 1000 * 1000 * 1000) return `${(value / (1000 * 1000)).toFixed(2)} MB`;
|
||||
return `${(value / (1000 * 1000 * 1000)).toFixed(2)} GB`;
|
||||
},
|
||||
formatStorageUsage(stats) {
|
||||
if (!stats || typeof stats !== "object") {
|
||||
return "0 B";
|
||||
}
|
||||
const used = this.formatByteSize(stats.messagestore_bytes);
|
||||
const limitValue = Number(stats.messagestore_limit_bytes);
|
||||
if (!Number.isFinite(limitValue) || limitValue <= 0) {
|
||||
return used;
|
||||
}
|
||||
return `${used} / ${this.formatByteSize(limitValue)}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1995,39 +1995,51 @@
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Delivery transfer limit (bytes)
|
||||
Delivery transfer limit (MB)
|
||||
</div>
|
||||
<input
|
||||
v-model.number="config.lxmf_delivery_transfer_limit_in_bytes"
|
||||
v-model.number="lxmfDeliveryTransferLimitInputMb"
|
||||
type="number"
|
||||
min="1000"
|
||||
min="0.001"
|
||||
step="0.01"
|
||||
class="input-field"
|
||||
@input="onLxmfDeliveryTransferLimitChange"
|
||||
/>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ formatByteSize(config.lxmf_delivery_transfer_limit_in_bytes) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Propagation transfer limit (bytes)
|
||||
Propagation transfer limit (MB)
|
||||
</div>
|
||||
<input
|
||||
v-model.number="config.lxmf_propagation_transfer_limit_in_bytes"
|
||||
v-model.number="lxmfPropagationTransferLimitInputMb"
|
||||
type="number"
|
||||
min="1000"
|
||||
min="0.001"
|
||||
step="0.01"
|
||||
class="input-field"
|
||||
@input="onLxmfPropagationTransferLimitChange"
|
||||
/>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ formatByteSize(config.lxmf_propagation_transfer_limit_in_bytes) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
Propagation sync limit (bytes)
|
||||
Propagation sync limit (MB)
|
||||
</div>
|
||||
<input
|
||||
v-model.number="config.lxmf_propagation_sync_limit_in_bytes"
|
||||
v-model.number="lxmfPropagationSyncLimitInputMb"
|
||||
type="number"
|
||||
min="1000"
|
||||
min="0.001"
|
||||
step="0.01"
|
||||
class="input-field"
|
||||
@input="onLxmfPropagationSyncLimitChange"
|
||||
/>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{{ formatByteSize(config.lxmf_propagation_sync_limit_in_bytes) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="config.lxmf_local_propagation_node_enabled" class="space-y-2">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
@@ -2236,6 +2248,9 @@ export default {
|
||||
nomad_default_page_path: "/page/index.mu",
|
||||
},
|
||||
saveTimeouts: {},
|
||||
lxmfDeliveryTransferLimitInputMb: 10,
|
||||
lxmfPropagationTransferLimitInputMb: 0.256,
|
||||
lxmfPropagationSyncLimitInputMb: 10.24,
|
||||
lastRememberedInboundStampCost: 8,
|
||||
shortcuts: [],
|
||||
reloadingRns: false,
|
||||
@@ -2544,6 +2559,7 @@ export default {
|
||||
if (json.config) {
|
||||
this.config = { ...this.config, ...json.config };
|
||||
this.sanitizeColorConfigFields();
|
||||
this.syncLxmfTransferLimitInputs();
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -2575,6 +2591,7 @@ export default {
|
||||
if (merged) {
|
||||
this.config = merged;
|
||||
normalizeConfigColors(this.config);
|
||||
this.syncLxmfTransferLimitInputs();
|
||||
const inbound = Number(this.config.lxmf_inbound_stamp_cost);
|
||||
if (inbound > 0) {
|
||||
this.lastRememberedInboundStampCost = Math.min(254, inbound);
|
||||
@@ -2613,6 +2630,7 @@ export default {
|
||||
const newConfig = await patchServerConfig(config, window.api);
|
||||
this.config = newConfig;
|
||||
normalizeConfigColors(this.config);
|
||||
this.syncLxmfTransferLimitInputs();
|
||||
if (label) {
|
||||
ToastUtils.success(this.$t("app.setting_auto_saved", { label: this.$t(`app.${label}`) }));
|
||||
}
|
||||
@@ -2621,6 +2639,35 @@ export default {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
syncLxmfTransferLimitInputs() {
|
||||
this.lxmfDeliveryTransferLimitInputMb = this.bytesToMb(this.config.lxmf_delivery_transfer_limit_in_bytes);
|
||||
this.lxmfPropagationTransferLimitInputMb = this.bytesToMb(
|
||||
this.config.lxmf_propagation_transfer_limit_in_bytes
|
||||
);
|
||||
this.lxmfPropagationSyncLimitInputMb = this.bytesToMb(this.config.lxmf_propagation_sync_limit_in_bytes);
|
||||
},
|
||||
bytesToMb(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0.001, Math.round((n / 1000000) * 1000) / 1000);
|
||||
},
|
||||
mbToBytes(value) {
|
||||
const n = Number(value);
|
||||
if (!Number.isFinite(n) || n <= 0) {
|
||||
return 1000;
|
||||
}
|
||||
return Math.max(1000, Math.round(n * 1000000));
|
||||
},
|
||||
formatByteSize(bytes) {
|
||||
const value = Number(bytes);
|
||||
if (!Number.isFinite(value) || value < 0) return "0 B";
|
||||
if (value < 1000) return `${Math.round(value)} B`;
|
||||
if (value < 1000 * 1000) return `${(value / 1000).toFixed(1)} KB`;
|
||||
if (value < 1000 * 1000 * 1000) return `${(value / (1000 * 1000)).toFixed(2)} MB`;
|
||||
return `${(value / (1000 * 1000 * 1000)).toFixed(2)} GB`;
|
||||
},
|
||||
sanitizeColorConfigFields() {
|
||||
if (!this.config) return;
|
||||
normalizeConfigColors(this.config);
|
||||
@@ -2870,7 +2917,7 @@ export default {
|
||||
}
|
||||
this.saveTimeouts.delivery_transfer_limit = setTimeout(async () => {
|
||||
await this.updateConfig({
|
||||
lxmf_delivery_transfer_limit_in_bytes: this.config.lxmf_delivery_transfer_limit_in_bytes,
|
||||
lxmf_delivery_transfer_limit_in_bytes: this.mbToBytes(this.lxmfDeliveryTransferLimitInputMb),
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
@@ -2880,7 +2927,7 @@ export default {
|
||||
}
|
||||
this.saveTimeouts.propagation_transfer_limit = setTimeout(async () => {
|
||||
await this.updateConfig({
|
||||
lxmf_propagation_transfer_limit_in_bytes: this.config.lxmf_propagation_transfer_limit_in_bytes,
|
||||
lxmf_propagation_transfer_limit_in_bytes: this.mbToBytes(this.lxmfPropagationTransferLimitInputMb),
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
@@ -2890,7 +2937,7 @@ export default {
|
||||
}
|
||||
this.saveTimeouts.propagation_sync_limit = setTimeout(async () => {
|
||||
await this.updateConfig({
|
||||
lxmf_propagation_sync_limit_in_bytes: this.config.lxmf_propagation_sync_limit_in_bytes,
|
||||
lxmf_propagation_sync_limit_in_bytes: this.mbToBytes(this.lxmfPropagationSyncLimitInputMb),
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
@@ -193,6 +193,14 @@ export default {
|
||||
titleKey: "tools.bots.title",
|
||||
descriptionKey: "tools.bots.description",
|
||||
},
|
||||
{
|
||||
name: "propagation-nodes",
|
||||
route: { name: "propagation-nodes" },
|
||||
icon: "mailbox",
|
||||
iconBg: "tool-card__icon bg-cyan-50 text-cyan-500 dark:bg-cyan-900/30 dark:text-cyan-200",
|
||||
title: "Propagation Nodes",
|
||||
description: "Manage preferred and local propagation nodes with live stats and path checks.",
|
||||
},
|
||||
{
|
||||
name: "forwarder",
|
||||
route: { name: "forwarder" },
|
||||
|
||||
@@ -25,7 +25,21 @@
|
||||
"export_identity": "Identität exportieren",
|
||||
"bot_deleted": "Bot erfolgreich gelöscht",
|
||||
"failed_to_delete": "Bot konnte nicht gelöscht werden",
|
||||
"more_bots_coming": "Weitere Bots folgen in Kürze!"
|
||||
"more_bots_coming": "Weitere Bots folgen in Kürze!",
|
||||
"chat_with_bot": "Chat",
|
||||
"lxmf_address": "LXMF-Adresse",
|
||||
"last_announce": "Letzter Announce",
|
||||
"never_announced": "Auf diesem Knoten noch nicht gesehen",
|
||||
"address_pending": "Erst nach dem ersten Start verfügbar",
|
||||
"status_running": "Läuft",
|
||||
"status_stopped": "Gestoppt",
|
||||
"force_announce": "Jetzt ankündigen",
|
||||
"edit_name": "Namen bearbeiten",
|
||||
"bot_renamed": "Name gespeichert",
|
||||
"rename_failed": "Name konnte nicht gespeichert werden",
|
||||
"name_required": "Namen eingeben",
|
||||
"announce_triggered": "Ankündigung angefordert",
|
||||
"announce_failed": "Ankündigung konnte nicht angefordert werden"
|
||||
},
|
||||
"app": {
|
||||
"name": "Reticulum MeshChatX",
|
||||
|
||||
@@ -25,7 +25,21 @@
|
||||
"export_identity": "Export Identity",
|
||||
"bot_deleted": "Bot deleted successfully",
|
||||
"failed_to_delete": "Failed to delete bot",
|
||||
"more_bots_coming": "More bots coming soon!"
|
||||
"more_bots_coming": "More bots coming soon!",
|
||||
"chat_with_bot": "Chat",
|
||||
"lxmf_address": "LXMF address",
|
||||
"last_announce": "Last announce",
|
||||
"never_announced": "Not seen on this node yet",
|
||||
"address_pending": "Not available until the bot has started once",
|
||||
"status_running": "Running",
|
||||
"status_stopped": "Stopped",
|
||||
"force_announce": "Announce now",
|
||||
"edit_name": "Edit name",
|
||||
"bot_renamed": "Bot name saved",
|
||||
"rename_failed": "Could not save name",
|
||||
"name_required": "Enter a name",
|
||||
"announce_triggered": "Announce requested",
|
||||
"announce_failed": "Could not request announce"
|
||||
},
|
||||
"app": {
|
||||
"name": "Reticulum MeshChatX",
|
||||
|
||||
@@ -25,7 +25,21 @@
|
||||
"export_identity": "Esporta Identità",
|
||||
"bot_deleted": "Bot eliminato con successo",
|
||||
"failed_to_delete": "Impossibile eliminare il bot",
|
||||
"more_bots_coming": "Altri bot in arrivo!"
|
||||
"more_bots_coming": "Altri bot in arrivo!",
|
||||
"chat_with_bot": "Chat",
|
||||
"lxmf_address": "Indirizzo LXMF",
|
||||
"last_announce": "Ultimo announce",
|
||||
"never_announced": "Non ancora visto su questo nodo",
|
||||
"address_pending": "Disponibile dopo il primo avvio del bot",
|
||||
"status_running": "In esecuzione",
|
||||
"status_stopped": "Fermato",
|
||||
"force_announce": "Annuncia ora",
|
||||
"edit_name": "Modifica nome",
|
||||
"bot_renamed": "Nome salvato",
|
||||
"rename_failed": "Impossibile salvare il nome",
|
||||
"name_required": "Inserisci un nome",
|
||||
"announce_triggered": "Annuncio richiesto",
|
||||
"announce_failed": "Impossibile richiedere l'annuncio"
|
||||
},
|
||||
"app": {
|
||||
"name": "Reticulum MeshChatX",
|
||||
|
||||
@@ -25,7 +25,21 @@
|
||||
"export_identity": "Экспорт личности",
|
||||
"bot_deleted": "Бот успешно удален",
|
||||
"failed_to_delete": "Не удалось удалить бота",
|
||||
"more_bots_coming": "Скоро появятся новые боты!"
|
||||
"more_bots_coming": "Скоро появятся новые боты!",
|
||||
"chat_with_bot": "Чат",
|
||||
"lxmf_address": "Адрес LXMF",
|
||||
"last_announce": "Последний announce",
|
||||
"never_announced": "На этом узле ещё не виден",
|
||||
"address_pending": "Появится после первого запуска бота",
|
||||
"status_running": "Запущен",
|
||||
"status_stopped": "Остановлен",
|
||||
"force_announce": "Объявить сейчас",
|
||||
"edit_name": "Изменить имя",
|
||||
"bot_renamed": "Имя бота сохранено",
|
||||
"rename_failed": "Не удалось сохранить имя",
|
||||
"name_required": "Введите имя",
|
||||
"announce_triggered": "Запрос announce отправлен",
|
||||
"announce_failed": "Не удалось запросить announce"
|
||||
},
|
||||
"app": {
|
||||
"name": "Reticulum MeshChatX",
|
||||
|
||||
@@ -64,6 +64,10 @@
|
||||
"method": "DELETE",
|
||||
"path": "/api/v1/blocked-destinations/{destination_hash}"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/bots/announce"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/bots/delete"
|
||||
@@ -84,6 +88,10 @@
|
||||
"method": "GET",
|
||||
"path": "/api/v1/bots/status"
|
||||
},
|
||||
{
|
||||
"method": "PATCH",
|
||||
"path": "/api/v1/bots/update"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/api/v1/bots/stop"
|
||||
|
||||
@@ -22,6 +22,9 @@ const syncingStates = [
|
||||
|
||||
function makeSyncContext(axiosMock, tOverrides = {}) {
|
||||
return {
|
||||
config: {
|
||||
lxmf_preferred_propagation_node_destination_hash: "deadbeef",
|
||||
},
|
||||
propagationNodeStatus: null,
|
||||
_propagationSyncPollTimer: null,
|
||||
propagationSyncLiveToastMessage: App.methods.propagationSyncLiveToastMessage,
|
||||
@@ -77,6 +80,7 @@ function makeSyncContext(axiosMock, tOverrides = {}) {
|
||||
describe("App propagation sync", () => {
|
||||
const axiosMock = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -91,6 +95,7 @@ describe("App propagation sync", () => {
|
||||
});
|
||||
|
||||
it("shows detailed success toast with stored, confirmations and hidden counts", async () => {
|
||||
axiosMock.post.mockResolvedValue({ data: { message: "ok" } });
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/sync") {
|
||||
return Promise.resolve({ data: { message: "Sync is starting" } });
|
||||
@@ -122,10 +127,12 @@ describe("App propagation sync", () => {
|
||||
expect(ToastUtils.success).toHaveBeenCalledWith(
|
||||
"Sync complete. 8 messages received. (3 stored, 2 confirmations, 3 hidden)"
|
||||
);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/destination/deadbeef/request-path");
|
||||
expect(ToastUtils.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("polls status while syncing and updates live loading toast", async () => {
|
||||
axiosMock.post.mockResolvedValue({ data: { message: "ok" } });
|
||||
let statusCalls = 0;
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/sync") {
|
||||
@@ -178,6 +185,7 @@ describe("App propagation sync", () => {
|
||||
});
|
||||
|
||||
it("uses translated status in error toast when sync ends in a failure state", async () => {
|
||||
axiosMock.post.mockResolvedValue({ data: { message: "ok" } });
|
||||
axiosMock.get.mockImplementation((url) => {
|
||||
if (url === "/api/v1/lxmf/propagation-node/sync") {
|
||||
return Promise.resolve({ data: { message: "Sync is starting" } });
|
||||
|
||||
@@ -11,7 +11,9 @@ vi.mock("../../meshchatx/src/frontend/js/ToastUtils", () => ({
|
||||
|
||||
describe("PropagationNodesPage", () => {
|
||||
const axiosMock = {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -39,20 +41,42 @@ describe("PropagationNodesPage", () => {
|
||||
const ctx = {
|
||||
localPropagationNode: { destination_hash: "local-node" },
|
||||
usePropagationNode: vi.fn(),
|
||||
requestPathForNode: vi.fn(),
|
||||
};
|
||||
|
||||
await PropagationNodesPage.methods.useLocalPropagationNode.call(ctx);
|
||||
expect(ctx.usePropagationNode).toHaveBeenCalledWith("local-node");
|
||||
expect(ctx.requestPathForNode).toHaveBeenCalledWith("local-node");
|
||||
});
|
||||
|
||||
it("prefers runtime local node state for running indicator", () => {
|
||||
const runningByStats = PropagationNodesPage.computed.localNodeIsRunning.call({
|
||||
localPropagationNode: {
|
||||
is_propagation_enabled: true,
|
||||
local_node_stats: { is_running: false },
|
||||
},
|
||||
});
|
||||
expect(runningByStats).toBe(false);
|
||||
});
|
||||
|
||||
it("formats storage usage with limit when available", () => {
|
||||
const ctx = {
|
||||
formatByteSize: PropagationNodesPage.methods.formatByteSize,
|
||||
};
|
||||
const text = PropagationNodesPage.methods.formatStorageUsage.call(ctx, {
|
||||
messagestore_bytes: 76500,
|
||||
messagestore_limit_bytes: 10240000,
|
||||
});
|
||||
expect(text).toBe("76.5 KB / 10.24 MB");
|
||||
});
|
||||
|
||||
it("debounces propagation transfer limit save", async () => {
|
||||
const ctx = {
|
||||
config: {
|
||||
lxmf_propagation_transfer_limit_in_bytes: 123456,
|
||||
},
|
||||
propagationLimitInputMb: 1.234,
|
||||
saveTimeouts: {
|
||||
propagationLimit: null,
|
||||
},
|
||||
mbToBytes: PropagationNodesPage.methods.mbToBytes,
|
||||
updateConfig: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
@@ -61,7 +85,24 @@ describe("PropagationNodesPage", () => {
|
||||
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(ctx.updateConfig).toHaveBeenCalledWith({
|
||||
lxmf_propagation_transfer_limit_in_bytes: 123456,
|
||||
lxmf_propagation_transfer_limit_in_bytes: 1234000,
|
||||
});
|
||||
});
|
||||
|
||||
it("debounces propagation stamp cost save with bounds", async () => {
|
||||
const ctx = {
|
||||
config: {
|
||||
lxmf_propagation_node_stamp_cost: 3,
|
||||
},
|
||||
saveTimeouts: {
|
||||
propagationStampCost: null,
|
||||
},
|
||||
updateConfig: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
await PropagationNodesPage.methods.onPropagationStampCostChange.call(ctx);
|
||||
await vi.advanceTimersByTimeAsync(500);
|
||||
expect(ctx.updateConfig).toHaveBeenCalledWith({
|
||||
lxmf_propagation_node_stamp_cost: 13,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,6 +111,7 @@ describe("PropagationNodesPage", () => {
|
||||
const ctx = {
|
||||
getConfig: vi.fn().mockResolvedValue(undefined),
|
||||
loadPropagationNodes: vi.fn().mockResolvedValue(undefined),
|
||||
refreshPriorityNodePaths: vi.fn().mockResolvedValue(undefined),
|
||||
$t: (k) => k,
|
||||
};
|
||||
|
||||
@@ -80,4 +122,93 @@ describe("PropagationNodesPage", () => {
|
||||
expect(axiosMock.post).toHaveBeenCalledWith("/api/v1/lxmf/propagation-node/restart");
|
||||
expect(ToastUtils.success).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("triggers announce via icon action", async () => {
|
||||
axiosMock.get.mockResolvedValue({ data: {} });
|
||||
const ctx = {
|
||||
loadPropagationNodes: vi.fn().mockResolvedValue(undefined),
|
||||
refreshPriorityNodePaths: vi.fn().mockResolvedValue(undefined),
|
||||
$t: (k) => k,
|
||||
};
|
||||
await PropagationNodesPage.methods.announceNow.call(ctx);
|
||||
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/announce");
|
||||
expect(ToastUtils.success).toHaveBeenCalledWith("Announce triggered");
|
||||
});
|
||||
|
||||
it("resets local node display name to Anonymous Peer", async () => {
|
||||
const ctx = {
|
||||
localNodeDisplayNameDraft: "Custom Name",
|
||||
saveLocalNodeDisplayName: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
await PropagationNodesPage.methods.resetLocalNodeDisplayName.call(ctx);
|
||||
expect(ctx.localNodeDisplayNameDraft).toBe("Anonymous Peer");
|
||||
expect(ctx.saveLocalNodeDisplayName).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses collapsed manager on small screens", () => {
|
||||
const originalMatchMedia = window.matchMedia;
|
||||
window.matchMedia = vi.fn().mockReturnValue({ matches: true });
|
||||
const ctx = {
|
||||
isLocalManagerCollapsed: false,
|
||||
getConfig: vi.fn(),
|
||||
loadPropagationNodes: vi.fn(),
|
||||
refreshPriorityNodePaths: vi.fn(),
|
||||
};
|
||||
PropagationNodesPage.mounted.call(ctx);
|
||||
expect(ctx.isLocalManagerCollapsed).toBe(true);
|
||||
window.matchMedia = originalMatchMedia;
|
||||
});
|
||||
|
||||
it("saves local display name and announces immediately", async () => {
|
||||
axiosMock.patch.mockResolvedValue({
|
||||
data: {
|
||||
config: {
|
||||
display_name: "Friendly Node",
|
||||
lxmf_delivery_transfer_limit_in_bytes: 10000000,
|
||||
lxmf_propagation_transfer_limit_in_bytes: 256000,
|
||||
lxmf_propagation_sync_limit_in_bytes: 10240000,
|
||||
},
|
||||
},
|
||||
});
|
||||
axiosMock.get.mockResolvedValue({ data: {} });
|
||||
|
||||
const ctx = {
|
||||
localNodeDisplayNameDraft: " Friendly Node ",
|
||||
config: {
|
||||
lxmf_delivery_transfer_limit_in_bytes: 10000000,
|
||||
lxmf_propagation_transfer_limit_in_bytes: 256000,
|
||||
lxmf_propagation_sync_limit_in_bytes: 10240000,
|
||||
},
|
||||
syncManagerInputsFromConfig: vi.fn(),
|
||||
loadPropagationNodes: vi.fn().mockResolvedValue(undefined),
|
||||
refreshPriorityNodePaths: vi.fn().mockResolvedValue(undefined),
|
||||
announceNow: PropagationNodesPage.methods.announceNow,
|
||||
updateConfig: PropagationNodesPage.methods.updateConfig,
|
||||
$t: (k) => k,
|
||||
};
|
||||
|
||||
await PropagationNodesPage.methods.saveLocalNodeDisplayName.call(ctx);
|
||||
|
||||
expect(axiosMock.patch).toHaveBeenCalledWith("/api/v1/config", {
|
||||
display_name: "Friendly Node",
|
||||
});
|
||||
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/announce");
|
||||
expect(ToastUtils.success).toHaveBeenCalledWith("Name saved and announced");
|
||||
});
|
||||
|
||||
it("fetches path for a destination hash", async () => {
|
||||
axiosMock.get.mockResolvedValueOnce({
|
||||
data: {
|
||||
path: { hops: 2, next_hop_interface: "TCP Client" },
|
||||
},
|
||||
});
|
||||
const ctx = {
|
||||
nodePathsByHash: {},
|
||||
};
|
||||
await PropagationNodesPage.methods.requestPathForNode.call(ctx, "abcd");
|
||||
expect(axiosMock.get).toHaveBeenCalledWith("/api/v1/destination/abcd/path", {
|
||||
params: { request: "1", timeout: 4 },
|
||||
});
|
||||
expect(ctx.nodePathsByHash.abcd).toEqual({ hops: 2, next_hop_interface: "TCP Client" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -258,21 +258,21 @@ describe("SettingsPage — config persistence (PATCH and related)", () => {
|
||||
|
||||
it("LXMF transfer/sync limits PATCH after debounce", async () => {
|
||||
const w = await mountSettingsPage(api);
|
||||
w.vm.config.lxmf_delivery_transfer_limit_in_bytes = 9_000_000;
|
||||
w.vm.lxmfDeliveryTransferLimitInputMb = 9;
|
||||
await w.vm.onLxmfDeliveryTransferLimitChange();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(api.patch).toHaveBeenCalledWith("/api/v1/config", {
|
||||
lxmf_delivery_transfer_limit_in_bytes: 9_000_000,
|
||||
});
|
||||
|
||||
w.vm.config.lxmf_propagation_transfer_limit_in_bytes = 300_000;
|
||||
w.vm.lxmfPropagationTransferLimitInputMb = 0.3;
|
||||
await w.vm.onLxmfPropagationTransferLimitChange();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(api.patch).toHaveBeenCalledWith("/api/v1/config", {
|
||||
lxmf_propagation_transfer_limit_in_bytes: 300_000,
|
||||
});
|
||||
|
||||
w.vm.config.lxmf_propagation_sync_limit_in_bytes = 9_000_000;
|
||||
w.vm.lxmfPropagationSyncLimitInputMb = 9;
|
||||
await w.vm.onLxmfPropagationSyncLimitChange();
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
expect(api.patch).toHaveBeenCalledWith("/api/v1/config", {
|
||||
|
||||
@@ -15,6 +15,7 @@ describe("ToolsPage.vue", () => {
|
||||
{ path: "/rnpath-trace", name: "rnpath-trace", component: { template: "div" } },
|
||||
{ path: "/translator", name: "translator", component: { template: "div" } },
|
||||
{ path: "/bots", name: "bots", component: { template: "div" } },
|
||||
{ path: "/propagation-nodes", name: "propagation-nodes", component: { template: "div" } },
|
||||
{ path: "/forwarder", name: "forwarder", component: { template: "div" } },
|
||||
{ path: "/documentation", name: "documentation", component: { template: "div" } },
|
||||
{ path: "/micron-editor", name: "micron-editor", component: { template: "div" } },
|
||||
@@ -51,7 +52,7 @@ describe("ToolsPage.vue", () => {
|
||||
it("renders all tool rows", () => {
|
||||
const wrapper = mountToolsPage();
|
||||
const toolRows = wrapper.findAll(".tool-row");
|
||||
expect(toolRows.length).toBe(17);
|
||||
expect(toolRows.length).toBe(18);
|
||||
});
|
||||
|
||||
it("filters tools based on search query", async () => {
|
||||
@@ -76,6 +77,6 @@ describe("ToolsPage.vue", () => {
|
||||
await clearButton.trigger("click");
|
||||
|
||||
expect(wrapper.vm.searchQuery).toBe("");
|
||||
expect(wrapper.vm.filteredTools.length).toBe(17);
|
||||
expect(wrapper.vm.filteredTools.length).toBe(18);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user