+
-
- Dependency Chain
+
+ Technical Issues Detected
-
-
-
-
+
+ •
+ {{ issue }}
+
+
+
+
+
+ {{ $t("about.no_integrity_violations") }}
+
+
+
+
+
+
+
+ Environment Information
+
+
+
+
Reticulum Config
+
+ {{ appInfo.reticulum_config_path }}
+
+
+ Reveal File
+
+
+
+
Database Path
+
+ {{ appInfo.database_path }}
+
+
+ Reveal DB
+
+
+
+
+
+
Identity Hash
-
-
-
-
- MeshChatX
-
-
- v{{ appInfo.version }}
-
-
+
{{
+ config.identity_hash
+ }}
-
-
-
+ LXMF Address
- LXMF
-
-
-
- Lightweight Extensible Message Format
-
-
- v{{ appInfo.lxmf_version }}
-
-
-
-
-
-
- RNS
-
-
-
- Reticulum Network Stack
-
-
- v{{ appInfo.rns_version }}
-
-
+
{{
+ config.lxmf_address_hash
+ }}
-
-
-
+
Python
-
- Core Runtime
-
-
-
- LXST Engine
- v{{ appInfo.lxst_version }}
-
-
- Electron
- v{{ electronVersion }}
-
-
- Chrome
- v{{ chromeVersion }}
-
-
- Node.js
- v{{ nodeVersion }}
-
-
-
-
-
-
- Backend Stack
-
-
-
- {{ name.replace("_", " ") }}
- v{{ version }}
-
-
-
+
v{{ appInfo.python_version }}
+
+
+ LXMF
+ v{{ appInfo.lxmf_version }}
+
+
+ RNS
+ v{{ appInfo.rns_version }}
+
-
-
-
-
-
- Database Health & Maintenance
-
-
-
- Refresh
-
-
- Vacuum
-
-
- Recovery
-
-
-
-
-
-
+
+
+
+
+ Dependency Chain
+
+
+
+
- Integrity
+
-
- {{ databaseHealth.quick_check }}
+
+
MeshChatX
+
+ v{{ appInfo.version }}
+
+
- Journal
+ LXMF
-
- {{ databaseHealth.journal_mode }}
+
+
+ Lightweight Extensible Message Format
+
+
+ v{{ appInfo.lxmf_version }}
+
- Page Count
-
-
- {{ databaseHealth.page_count }}
-
-
-
+ class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-purple-500 to-indigo-500"
+ >
- Free Space
+ RNS
-
- {{ formatBytes(databaseHealth.estimated_free_bytes) }}
+
+
+ Reticulum Network Stack
+
+
+ v{{ appInfo.rns_version }}
+
-
-
-
-
-
-
- Database Backups
-
-
- Full snapshots of your communications database.
-
-
-
+
+
-
- Download Backup
-
-
-
-
-
-
-
-
+
+
+ LXST Engine
+ v{{ appInfo.lxst_version }}
-
- Local Snapshots
-
-
- Create point-in-time restore points on disk.
-
-
-
-
+ Electron
+ v{{ electronVersion }}
+
+
+ Chrome
+ v{{ chromeVersion }}
+
+
+ Node.js
+ v{{ nodeVersion }}
- Create
-
+
-
+
+
+ Backend Stack
+
+
-
- {{ snapshot.name }}
- {{ formatBytes(snapshot.size) }} • {{ snapshot.created_at }}
-
-
{{ name.replace("_", " ") }}
+ v{{ version }}
- Restore
-
-
-
-
-
-
-
-
Identity Key Control
-
- Critical Security Warning
-
-
-
-
-
-
-
- Export Key File
-
-
-
- Copy Base32 Key
-
-
-
-
-
- Restore Identity
-
-
-
-
- Upload Key File
-
-
-
- — or —
-
-
-
- Paste Base32
-
-
-
-
-
-
-
- Confirm Key Restore
-
-
-
-
-
-
+
+
+
+
+
+
+ Database Health & Maintenance
+
+
+
+
+ Loading...
+ Refresh
+
+
+ Vacuum
+
+
+ Recovery
+
+
+
+
+
+
+
+ Integrity
+
+
+ {{ databaseHealth.quick_check }}
+
+
+
+
+ Journal
+
+
+ {{ databaseHealth.journal_mode }}
+
+
+
+
+ Page Count
+
+
+ {{ databaseHealth.page_count }}
+
+
+
+
+ Free Space
+
+
+ {{ formatBytes(databaseHealth.estimated_free_bytes) }}
+
+
+
+
+
+
+
+
+
+
+ Database Backups
+
+
+ Full snapshots of your communications database.
+
+
+
+
+ Downloading...
+ Download Backup
+
+
+
+
+
+
+
+
+
+ Local Snapshots
+
+
+ Create point-in-time restore points on disk.
+
+
+
+
+
+ Creating...
+ Create
+
+
+
+
+
+
+
+ {{ snapshot.name }}
+ {{ formatBytes(snapshot.size) }} • {{ snapshot.created_at }}
+
+
+ Restore
+
+
+
+
+
+
+
+
+
+
+
Identity Key Control
+
+ Critical Security Warning
+
+
+
+
+
+
+
+ Export Key File
+
+
+
+ Copy Base32 Key
+
+
+
+
+
+ Restore Identity
+
+
+
+
+ Upload Key File
+
+
+
+ — or —
+
+
+
+ Paste Base32
+
+
+
+
+
+
+
+ Restoring...
+ Confirm Key Restore
+
+
+
+
+
+
+
+
@@ -715,6 +656,7 @@ import Utils from "../../js/Utils";
import ElectronUtils from "../../js/ElectronUtils";
import DialogUtils from "../../js/DialogUtils";
import ToastUtils from "../../js/ToastUtils";
+import GlobalEmitter from "../../js/GlobalEmitter";
export default {
name: "AboutPage",
components: {},
@@ -758,7 +700,6 @@ export default {
electronVersion: null,
chromeVersion: null,
nodeVersion: null,
- showAdvanced: false,
showIdentityPaste: false,
};
},
@@ -1030,10 +971,10 @@ export default {
}
},
showChangelog() {
- this.$router.push({ name: "changelog" });
+ GlobalEmitter.emit("show-changelog");
},
showTutorial() {
- this.$router.push({ name: "tutorial" });
+ GlobalEmitter.emit("show-tutorial");
},
showReticulumConfigFile() {
const reticulumConfigPath = this.appInfo.reticulum_config_path;
@@ -1178,3 +1119,10 @@ export default {
},
};
+
+
diff --git a/meshchatx/src/frontend/components/blocked/BlockedPage.vue b/meshchatx/src/frontend/components/blocked/BlockedPage.vue
index 80603ad..57a3b6d 100644
--- a/meshchatx/src/frontend/components/blocked/BlockedPage.vue
+++ b/meshchatx/src/frontend/components/blocked/BlockedPage.vue
@@ -8,8 +8,8 @@
-
Blocked
-
Manage blocked users and nodes
+
Banished
+
Manage Banished users and nodes
@@ -39,7 +39,7 @@
-
Loading blocked items...
+
Loading banished items...
-
No blocked items
+
No banished items
{{
searchQuery
- ? "No blocked items match your search."
- : "You haven't blocked any users or nodes yet."
+ ? "No banished items match your search."
+ : "You haven't banished any users or nodes yet."
}}
@@ -95,6 +95,13 @@
>
User
+
+ RNS Blackhole
+
-
- Blocked {{ formatTimeAgo(item.created_at) }}
+
+ Banished {{ formatTimeAgo(item.created_at) }}
+
+
+ Source: {{ item.rns_source }}
+
+
+ "{{ item.rns_reason }}"
@@ -114,7 +133,7 @@
@click="onUnblock(item)"
>
-
Unblock
+
Lift Banishment
@@ -137,17 +156,31 @@ export default {
data() {
return {
blockedItems: [],
+ reticulumBlackholedItems: [],
isLoading: false,
searchQuery: "",
};
},
computed: {
+ allBlockedItems() {
+ // Combine local blocked items and reticulum blackholed items
+ // Prioritize local items if they overlap
+ const localHashes = new Set(this.blockedItems.map((i) => i.destination_hash));
+ const combined = [...this.blockedItems];
+
+ for (const item of this.reticulumBlackholedItems) {
+ if (!localHashes.has(item.destination_hash)) {
+ combined.push(item);
+ }
+ }
+ return combined;
+ },
filteredBlockedItems() {
if (!this.searchQuery.trim()) {
- return this.blockedItems;
+ return this.allBlockedItems;
}
const query = this.searchQuery.toLowerCase();
- return this.blockedItems.filter((item) => {
+ return this.allBlockedItems.filter((item) => {
const matchesHash = item.destination_hash.toLowerCase().includes(query);
const matchesDisplayName = (item.display_name || "").toLowerCase().includes(query);
return matchesHash || matchesDisplayName;
@@ -161,74 +194,70 @@ export default {
async loadBlockedDestinations() {
this.isLoading = true;
try {
+ // Load local blocked destinations
const response = await window.axios.get("/api/v1/blocked-destinations");
const blockedHashes = response.data.blocked_destinations || [];
- const items = await Promise.all(
- blockedHashes.map(async (blocked) => {
- let displayName = "Unknown";
- let isNode = false;
+ // Load Reticulum blackholed identities
+ let reticulumBlackholed = {};
+ try {
+ const rnsResponse = await window.axios.get("/api/v1/reticulum/blackhole");
+ reticulumBlackholed = rnsResponse.data.blackholed_identities || {};
+ } catch (e) {
+ console.error("Failed to load Reticulum blackhole", e);
+ }
- try {
- const nodeAnnounceResponse = await window.axios.get("/api/v1/announces", {
- params: {
- aspect: "nomadnetwork.node",
- identity_hash: blocked.destination_hash,
- include_blocked: true,
- limit: 1,
- },
- });
+ const processItem = async (hash, data = {}) => {
+ let displayName = "Unknown";
+ let isNode = false;
- if (nodeAnnounceResponse.data.announces && nodeAnnounceResponse.data.announces.length > 0) {
- const announce = nodeAnnounceResponse.data.announces[0];
- displayName = announce.display_name || "Unknown";
- isNode = true;
- } else {
- const announceResponse = await window.axios.get("/api/v1/announces", {
- params: {
- identity_hash: blocked.destination_hash,
- include_blocked: true,
- limit: 1,
- },
- });
+ try {
+ const announceResponse = await window.axios.get("/api/v1/announces", {
+ params: {
+ identity_hash: hash,
+ include_blocked: true,
+ limit: 1,
+ },
+ });
- if (announceResponse.data.announces && announceResponse.data.announces.length > 0) {
- const announce = announceResponse.data.announces[0];
- displayName = announce.display_name || "Unknown";
- isNode = announce.aspect === "nomadnetwork.node";
- } else {
- const lxmfResponse = await window.axios.get("/api/v1/announces", {
- params: {
- destination_hash: blocked.destination_hash,
- include_blocked: true,
- limit: 1,
- },
- });
-
- if (lxmfResponse.data.announces && lxmfResponse.data.announces.length > 0) {
- const announce = lxmfResponse.data.announces[0];
- displayName = announce.display_name || "Unknown";
- isNode = announce.aspect === "nomadnetwork.node";
- }
- }
- }
- } catch (e) {
- console.log(e);
+ if (announceResponse.data.announces && announceResponse.data.announces.length > 0) {
+ const announce = announceResponse.data.announces[0];
+ displayName = announce.display_name || "Unknown";
+ isNode = announce.aspect === "nomadnetwork.node";
}
+ } catch {
+ // ignore error
+ }
- return {
- destination_hash: blocked.destination_hash,
- display_name: displayName,
- created_at: blocked.created_at,
- is_node: isNode,
- };
- })
+ return {
+ destination_hash: hash,
+ display_name: displayName,
+ created_at: data.created_at || null,
+ is_node: isNode,
+ is_rns_blackholed: !!data.is_rns,
+ rns_source: data.source || null,
+ rns_reason: data.reason || null,
+ rns_until: data.until || null,
+ };
+ };
+
+ const items = await Promise.all(
+ blockedHashes.map((blocked) =>
+ processItem(blocked.destination_hash, { created_at: blocked.created_at })
+ )
+ );
+
+ const rnsItems = await Promise.all(
+ Object.entries(reticulumBlackholed).map(([hash, info]) =>
+ processItem(hash, { ...info, is_rns: true })
+ )
);
this.blockedItems = items;
+ this.reticulumBlackholedItems = rnsItems;
} catch (e) {
console.log(e);
- ToastUtils.error("Failed to load blocked destinations");
+ ToastUtils.error("Failed to load banished destinations");
} finally {
this.isLoading = false;
}
@@ -236,7 +265,7 @@ export default {
async onUnblock(item) {
if (
!(await DialogUtils.confirm(
- `Are you sure you want to unblock ${item.display_name || item.destination_hash}?`
+ `Are you sure you want to lift the banishment for ${item.display_name || item.destination_hash}?`
))
) {
return;
@@ -245,10 +274,10 @@ export default {
try {
await window.axios.delete(`/api/v1/blocked-destinations/${item.destination_hash}`);
await this.loadBlockedDestinations();
- ToastUtils.success("Unblocked successfully");
+ ToastUtils.success("Banishment lifted successfully");
} catch (e) {
console.log(e);
- ToastUtils.error("Failed to unblock");
+ ToastUtils.error("Failed to lift banishment");
}
},
onSearchInput() {},
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 78585b2..eb10c63 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -2380,15 +2380,15 @@ export default {
}
},
async blockIdentity(hash) {
- if (!confirm(`Are you sure you want to block this identity?`)) return;
+ if (!confirm(`Are you sure you want to banish this identity?`)) return;
try {
await window.axios.post("/api/v1/blocked-destinations", {
destination_hash: hash,
});
- ToastUtils.success("Identity blocked");
+ ToastUtils.success("Identity banished");
this.getHistory();
} catch {
- ToastUtils.error("Failed to block identity");
+ ToastUtils.error("Failed to banish identity");
}
},
async getVoicemailStatus() {
diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
index 8a75642..46b5358 100644
--- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
+++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
@@ -1588,12 +1588,6 @@ export default {
.glass-label {
@apply mb-1 text-sm font-semibold text-gray-800 dark:text-gray-200;
}
-.primary-chip {
- @apply inline-flex items-center gap-x-2 rounded-full bg-blue-600/90 px-3 py-1.5 text-xs font-semibold text-white shadow hover:bg-blue-500 transition;
-}
-.secondary-chip {
- @apply inline-flex items-center gap-x-2 rounded-full border border-gray-300 dark:border-zinc-700 px-3 py-1.5 text-xs font-semibold text-gray-700 dark:text-gray-100 bg-white/80 dark:bg-zinc-900/70 hover:border-blue-400;
-}
.glass-field {
@apply space-y-1;
}
diff --git a/meshchatx/src/frontend/components/map/MapPage.vue b/meshchatx/src/frontend/components/map/MapPage.vue
index 8da7ef5..ae39c2f 100644
--- a/meshchatx/src/frontend/components/map/MapPage.vue
+++ b/meshchatx/src/frontend/components/map/MapPage.vue
@@ -78,12 +78,12 @@
@@ -147,9 +147,7 @@
class="absolute top-2 left-4 right-4 sm:left-auto sm:right-4 sm:w-80 z-30"
>
-
+
@@ -250,15 +248,15 @@
>
Delete
Save
@@ -922,8 +920,8 @@
Edit Note
@@ -938,15 +936,15 @@
Delete
Save Note
diff --git a/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue b/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue
index a062e35..d6cd0e4 100644
--- a/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue
+++ b/meshchatx/src/frontend/components/messages/AudioWaveformPlayer.vue
@@ -1,6 +1,6 @@
- Block User
+ Banish User
- Unblock User
+ Lift Banishment
@@ -83,7 +83,7 @@ export default {
async onBlockDestination() {
if (
!(await DialogUtils.confirm(
- "Are you sure you want to block this user? They will not be able to send you messages or establish links."
+ "Are you sure you want to banish this user? They will not be able to send you messages or establish links."
))
) {
return;
@@ -94,10 +94,10 @@ export default {
destination_hash: this.peer.destination_hash,
});
GlobalEmitter.emit("block-status-changed");
- DialogUtils.alert("User blocked successfully");
+ DialogUtils.alert("User banished successfully");
this.$emit("block-status-changed");
} catch (e) {
- DialogUtils.alert("Failed to block user");
+ DialogUtils.alert("Failed to banish user");
console.log(e);
}
},
@@ -105,10 +105,10 @@ export default {
try {
await window.axios.delete(`/api/v1/blocked-destinations/${this.peer.destination_hash}`);
GlobalEmitter.emit("block-status-changed");
- DialogUtils.alert("User unblocked successfully");
+ DialogUtils.alert("Banishment lifted successfully");
this.$emit("block-status-changed");
} catch (e) {
- DialogUtils.alert("Failed to unblock user");
+ DialogUtils.alert("Failed to lift banishment");
console.log(e);
}
},
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index d6b94d3..b72ed04 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -50,7 +50,7 @@
{{ selectedPeer.custom_display_name ?? selectedPeer.display_name }}
@@ -193,16 +193,16 @@
class="h-full overflow-y-scroll bg-gray-50/30 dark:bg-zinc-950/50"
@scroll="onMessagesScroll"
>
-
+
-
+
-
+
Paper Message detected
-
+
This message contains a signed LXMF URI that can be ingested into your
conversations.
@@ -321,7 +321,7 @@
{{ line }}
@@ -674,7 +675,7 @@
class="w-full border-t border-gray-200/60 dark:border-zinc-800/60 bg-white/80 dark:bg-zinc-900/50 backdrop-blur-sm px-3 sm:px-4 py-2.5"
>
-
+
You have blocked this user. They cannot send you messages or establish links. You have banished this user. They cannot send you messages or establish links.
@@ -914,13 +915,27 @@
class="group cursor-pointer p-4 bg-white dark:bg-zinc-900/50 border border-gray-100 dark:border-zinc-800 rounded-2xl hover:border-blue-500/50 hover:shadow-xl hover:shadow-blue-500/5 transition-all duration-300 flex items-center gap-4"
@click="$emit('update:selectedPeer', chat)"
>
-
+
+
+
{{ chat.custom_display_name ?? chat.display_name }}
@@ -984,12 +999,14 @@
diff --git a/meshchatx/src/frontend/components/messages/PaperMessageModal.vue b/meshchatx/src/frontend/components/messages/PaperMessageModal.vue
index 73b278d..b140c44 100644
--- a/meshchatx/src/frontend/components/messages/PaperMessageModal.vue
+++ b/meshchatx/src/frontend/components/messages/PaperMessageModal.vue
@@ -32,8 +32,8 @@
-
-
+
+
-
+
+
+
+
+
+
+ Print
+
+
+
+
+ Sending...
+
+
+
+ Send
+
+
+
+
+
@@ -134,12 +188,18 @@ export default {
required: false,
default: null,
},
+ recipientHash: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
emits: ["close"],
data() {
return {
uri: this.initialUri,
isLoading: !this.initialUri,
+ isSending: false,
};
},
async mounted() {
@@ -173,7 +233,7 @@ export default {
try {
await QRCode.toCanvas(this.$refs.qrcode, this.uri, {
- width: 320,
+ width: 256,
margin: 2,
color: {
dark: "#000000",
@@ -211,11 +271,56 @@ export default {
const dataUrl = canvas.toDataURL("image/png");
if (dataUrl) {
const link = document.createElement("a");
- link.download = `lxmf-paper-message-${this.messageHash.substring(0, 8)}.png`;
+ link.download = `lxmf-paper-message-${this.messageHash ? this.messageHash.substring(0, 8) : Date.now()}.png`;
link.href = dataUrl;
link.click();
}
},
+ async sendPaperMessage() {
+ const canvas = this.$refs.qrcode;
+ if (!canvas || !this.recipientHash || !this.uri) return;
+
+ try {
+ this.isSending = true;
+
+ // get data url from canvas (format: data:image/png;base64,iVBORw0KG...)
+ const dataUrl = canvas.toDataURL("image/png");
+
+ // extract base64 data by removing the prefix
+ const imageBytes = dataUrl.split(",")[1];
+
+ // build lxmf message
+ const lxmf_message = {
+ destination_hash: this.recipientHash,
+ content: this.uri,
+ fields: {
+ image: {
+ image_type: "png",
+ image_bytes: imageBytes,
+ name: "qrcode.png",
+ },
+ },
+ };
+
+ // send message
+ const response = await window.axios.post(`/api/v1/lxmf-messages/send`, {
+ delivery_method: "opportunistic",
+ lxmf_message: lxmf_message,
+ });
+
+ if (response.data.status === "success") {
+ ToastUtils.success("Paper message sent successfully");
+ this.close();
+ } else {
+ ToastUtils.error(response.data.message || "Failed to send paper message");
+ }
+ } catch (err) {
+ console.error("Failed to send paper message:", err);
+ ToastUtils.error("Failed to send paper message");
+ } finally {
+ this.isSending = false;
+ }
+ },
printQRCode() {
const canvas = this.$refs.qrcode;
if (!canvas) return;
diff --git a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue
index cbe5b36..e67efea 100644
--- a/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue
+++ b/meshchatx/src/frontend/components/nomadnetwork/NomadNetworkSidebar.vue
@@ -92,7 +92,7 @@
- Unblock Node
+ Lift Banishment
@@ -169,7 +169,7 @@
- Unblock Node
+ Lift Banishment
@@ -321,9 +321,9 @@ export default {
try {
await window.axios.delete(`/api/v1/blocked-destinations/${identityHash}`);
GlobalEmitter.emit("block-status-changed");
- DialogUtils.alert("Node unblocked successfully");
+ DialogUtils.alert("Banishment lifted successfully");
} catch (e) {
- DialogUtils.alert("Failed to unblock node");
+ DialogUtils.alert("Failed to lift banishment");
console.log(e);
}
},
diff --git a/meshchatx/src/frontend/components/ping/PingPage.vue b/meshchatx/src/frontend/components/ping/PingPage.vue
index e7069a9..7f1cca3 100644
--- a/meshchatx/src/frontend/components/ping/PingPage.vue
+++ b/meshchatx/src/frontend/components/ping/PingPage.vue
@@ -40,28 +40,24 @@
-
+
{{ $t("ping.start_ping") }}
{{ $t("ping.stop") }}
-
+
{{ $t("ping.clear_results") }}
-
+
{{ $t("ping.drop_path") }}
diff --git a/meshchatx/src/frontend/components/rncp/RNCPPage.vue b/meshchatx/src/frontend/components/rncp/RNCPPage.vue
index 81a7f0b..7ba3af8 100644
--- a/meshchatx/src/frontend/components/rncp/RNCPPage.vue
+++ b/meshchatx/src/frontend/components/rncp/RNCPPage.vue
@@ -15,6 +15,33 @@
{{ $t("rncp.description") }}
+
+
+
+ {{ $t("rncp.usage_steps") }}
+
+
+
@@ -493,6 +520,10 @@ export default {
this.listenDestinationHash = null;
this.listenResult = null;
},
+ renderMarkdown(text) {
+ if (!text) return "";
+ return text.replace(/\*\*(.*?)\*\*/g, "$1 ");
+ },
},
};
diff --git a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
index 92a6b38..8ff7d99 100644
--- a/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
+++ b/meshchatx/src/frontend/components/rnstatus/RNStatusPage.vue
@@ -48,11 +48,36 @@
-
-
Active Links: {{ linkCount }}
+
+
+
Active Links: {{ linkCount }}
+
+
+
+
+ Blackhole: {{ blackholeEnabled ? "Publishing" : "Active" }}
+ {{ blackholeCount }} Identities
+
+
+
+
+
+
@@ -189,6 +214,9 @@ export default {
linkCount: null,
includeLinkStats: false,
sorting: "",
+ blackholeEnabled: null,
+ blackholeSources: [],
+ blackholeCount: 0,
};
},
watch: {
@@ -215,6 +243,9 @@ export default {
const response = await window.axios.get("/api/v1/rnstatus", { params });
this.interfaces = response.data.interfaces || [];
this.linkCount = response.data.link_count;
+ this.blackholeEnabled = response.data.blackhole_enabled;
+ this.blackholeSources = response.data.blackhole_sources || [];
+ this.blackholeCount = response.data.blackhole_count || 0;
} catch (e) {
console.error(e);
} finally {
diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index 0ee6f4a..61c3340 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -436,6 +436,40 @@
+
+
+
+
+
+
+
+ {{ $t("app.blackhole_integration_enabled") }}
+
+
+ {{ $t("app.blackhole_integration_description") }}
+
+
+
+
+
+
+
-
-
+
@@ -181,11 +181,20 @@
-
- Save
+
+
+ Sending...
+
+
+
+ Send
+
@@ -227,6 +236,7 @@ export default {
isGenerating: false,
generatedUri: null,
ingestUri: "",
+ isSending: false,
};
},
computed: {
@@ -284,7 +294,7 @@ export default {
try {
await QRCode.toCanvas(this.$refs.qrcode, this.generatedUri, {
- width: 320,
+ width: 256,
margin: 2,
color: {
dark: "#000000",
@@ -341,6 +351,54 @@ export default {
link.click();
}
},
+ async sendPaperMessage() {
+ const canvas = this.$refs.qrcode;
+ if (!canvas || !this.destinationHash || !this.generatedUri) return;
+
+ try {
+ this.isSending = true;
+
+ // get data url from canvas (format: data:image/png;base64,iVBORw0KG...)
+ const dataUrl = canvas.toDataURL("image/png");
+
+ // extract base64 data by removing the prefix
+ const imageBytes = dataUrl.split(",")[1];
+
+ // build lxmf message
+ const lxmf_message = {
+ destination_hash: this.destinationHash,
+ content: this.generatedUri,
+ fields: {
+ image: {
+ image_type: "png",
+ image_bytes: imageBytes,
+ name: "qrcode.png",
+ },
+ },
+ };
+
+ // send message
+ const response = await window.axios.post(`/api/v1/lxmf-messages/send`, {
+ delivery_method: "opportunistic",
+ lxmf_message: lxmf_message,
+ });
+
+ if (response.data.status === "success") {
+ ToastUtils.success("Paper message sent successfully");
+ this.generatedUri = null;
+ this.destinationHash = "";
+ this.content = "";
+ this.title = "";
+ } else {
+ ToastUtils.error(response.data.message || "Failed to send paper message");
+ }
+ } catch (err) {
+ console.error("Failed to send paper message:", err);
+ ToastUtils.error("Failed to send paper message");
+ } finally {
+ this.isSending = false;
+ }
+ },
printQRCode() {
const canvas = this.$refs.qrcode;
if (!canvas) return;
diff --git a/meshchatx/src/frontend/components/tools/RNPathPage.vue b/meshchatx/src/frontend/components/tools/RNPathPage.vue
new file mode 100644
index 0000000..588a699
--- /dev/null
+++ b/meshchatx/src/frontend/components/tools/RNPathPage.vue
@@ -0,0 +1,311 @@
+
+
+
+
+
+
+
+
+
+
RNPath
+
Reticulum Path Management Utility
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ t.charAt(0).toUpperCase() + t.slice(1) }}
+
+
+
+
+
+
+ No paths currently known.
+
+
+
+
+
+
+ {{ path.hash }}
+
+
+ {{ path.hops }} {{ path.hops === 1 ? "hop" : "hops" }}
+
+
+
+ via {{ path.via }} on {{ path.interface }}
+
+
Expires: {{ formatDate(path.expires) }}
+
+
+ Drop Path
+
+
+
+
+
+
+
+
+ No announce rate data available.
+
+
+
+
+
+ {{ rate.hash }}
+
+
+ RATE LIMITED
+
+
+
+
+
Last Heard
+
{{ formatTimeAgo(rate.last) }}
+
+
+
Announces
+
{{ rate.timestamps.length }}
+
+
+
Violations
+
+ {{ rate.rate_violations }}
+
+
+
+
Rate
+
{{ calculateRate(rate) }} / hr
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Drop Announce Queues
+
+ Clear all outbound announce packets currently queued on all interfaces.
+
+
+ Purge All Queues
+
+
+
+
+
+
+
+
+
+
diff --git a/meshchatx/src/frontend/components/tools/ToolsPage.vue b/meshchatx/src/frontend/components/tools/ToolsPage.vue
index bb8a85d..389ceae 100644
--- a/meshchatx/src/frontend/components/tools/ToolsPage.vue
+++ b/meshchatx/src/frontend/components/tools/ToolsPage.vue
@@ -70,6 +70,21 @@
+
+
+
+
+
+
{{ $t("tools.rnpath.title") }}
+
+ {{ $t("tools.rnpath.description") }}
+
+
+
+
+