backup commit
This commit is contained in:
@@ -82,7 +82,7 @@ define(['./workbox-21a80088'], (function (workbox) { 'use strict';
|
||||
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||
}, {
|
||||
"url": "index.html",
|
||||
"revision": "0.tmc04bnmkho"
|
||||
"revision": "0.9f8m1arrh28"
|
||||
}], {});
|
||||
workbox.cleanupOutdatedCaches();
|
||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||
|
||||
@@ -1275,6 +1275,236 @@ app.post('/rpc/v1', (req, res) => {
|
||||
return res.json({ result: [] })
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Mesh Networking (LoRa radio via Meshcore)
|
||||
// =====================================================================
|
||||
case 'mesh.status': {
|
||||
return res.json({
|
||||
result: {
|
||||
enabled: true,
|
||||
device_type: 'Meshcore',
|
||||
device_path: '/dev/ttyUSB0',
|
||||
device_connected: true,
|
||||
firmware_version: '2.3.1',
|
||||
self_node_id: 42,
|
||||
self_advert_name: 'archy-228',
|
||||
peer_count: 4,
|
||||
channel_name: 'archipelago',
|
||||
messages_sent: 23,
|
||||
messages_received: 47,
|
||||
detected_devices: ['/dev/ttyUSB0'],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'mesh.peers': {
|
||||
return res.json({
|
||||
result: {
|
||||
peers: [
|
||||
{
|
||||
contact_id: 1,
|
||||
advert_name: 'archy-198',
|
||||
did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9',
|
||||
pubkey_hex: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
|
||||
rssi: -67,
|
||||
snr: 9.5,
|
||||
last_heard: new Date(Date.now() - 30000).toISOString(),
|
||||
hops: 0,
|
||||
},
|
||||
{
|
||||
contact_id: 2,
|
||||
advert_name: 'satoshi-relay',
|
||||
did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH',
|
||||
pubkey_hex: 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5',
|
||||
rssi: -82,
|
||||
snr: 4.2,
|
||||
last_heard: new Date(Date.now() - 120000).toISOString(),
|
||||
hops: 1,
|
||||
},
|
||||
{
|
||||
contact_id: 3,
|
||||
advert_name: 'mountain-node',
|
||||
did: null,
|
||||
pubkey_hex: null,
|
||||
rssi: -95,
|
||||
snr: 1.8,
|
||||
last_heard: new Date(Date.now() - 600000).toISOString(),
|
||||
hops: 2,
|
||||
},
|
||||
{
|
||||
contact_id: 4,
|
||||
advert_name: 'bunker-alpha',
|
||||
did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb',
|
||||
pubkey_hex: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
|
||||
rssi: -74,
|
||||
snr: 7.1,
|
||||
last_heard: new Date(Date.now() - 45000).toISOString(),
|
||||
hops: 0,
|
||||
},
|
||||
],
|
||||
count: 4,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'mesh.messages': {
|
||||
const limit = params?.limit || 100
|
||||
const now = Date.now()
|
||||
const allMessages = [
|
||||
{ id: 1, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Node online. Bitcoin Knots synced to tip.', timestamp: new Date(now - 3600000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 2, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Good. Electrs index at 98%. Channel capacity 2.5M sats.', timestamp: new Date(now - 3540000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 3, direction: 'received', peer_contact_id: 2, peer_name: 'satoshi-relay', plaintext: 'ARCHY:2:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2:d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5', timestamp: new Date(now - 3000000).toISOString(), delivered: true, encrypted: false },
|
||||
{ id: 4, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Federation state sync complete. 3 containers matched.', timestamp: new Date(now - 1800000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 5, direction: 'sent', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Running mesh-only mode. No internet for 48h. All good.', timestamp: new Date(now - 900000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 6, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Copy. Block height 890,412 via compact headers. 6 confirmations on last tx.', timestamp: new Date(now - 840000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 7, direction: 'received', peer_contact_id: 2, peer_name: 'satoshi-relay', plaintext: 'New block relayed: 890,413. Fees averaging 12 sat/vB.', timestamp: new Date(now - 600000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 8, direction: 'sent', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Opening 1M sat channel to your node. Approve?', timestamp: new Date(now - 300000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 9, direction: 'received', peer_contact_id: 1, peer_name: 'archy-198', plaintext: 'Approved. Waiting for funding tx confirmation.', timestamp: new Date(now - 240000).toISOString(), delivered: true, encrypted: true },
|
||||
{ id: 10, direction: 'received', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'Anyone copy? Solar panel restored, back online.', timestamp: new Date(now - 120000).toISOString(), delivered: true, encrypted: false },
|
||||
{ id: 11, direction: 'sent', peer_contact_id: 3, peer_name: 'mountain-node', plaintext: 'Copy mountain-node. Welcome back. Relaying your backlog.', timestamp: new Date(now - 60000).toISOString(), delivered: true, encrypted: false },
|
||||
{ id: 12, direction: 'received', peer_contact_id: 4, peer_name: 'bunker-alpha', plaintext: 'Dead man switch check-in. All systems nominal. Battery 78%.', timestamp: new Date(now - 30000).toISOString(), delivered: true, encrypted: true },
|
||||
]
|
||||
return res.json({
|
||||
result: {
|
||||
messages: allMessages.slice(0, limit),
|
||||
count: allMessages.length,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'mesh.send': {
|
||||
const contactId = params?.contact_id
|
||||
const message = params?.message || ''
|
||||
const peer = [
|
||||
{ id: 1, name: 'archy-198', encrypted: true },
|
||||
{ id: 2, name: 'satoshi-relay', encrypted: true },
|
||||
{ id: 3, name: 'mountain-node', encrypted: false },
|
||||
{ id: 4, name: 'bunker-alpha', encrypted: true },
|
||||
].find(p => p.id === contactId)
|
||||
console.log(`[Mesh] Send to ${peer?.name || contactId}: ${message}`)
|
||||
return res.json({
|
||||
result: {
|
||||
sent: true,
|
||||
message_id: Math.floor(Math.random() * 10000) + 100,
|
||||
encrypted: peer?.encrypted ?? false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'mesh.broadcast': {
|
||||
console.log('[Mesh] Broadcasting identity over LoRa')
|
||||
return res.json({ result: { broadcast: true } })
|
||||
}
|
||||
|
||||
case 'mesh.configure': {
|
||||
console.log(`[Mesh] Configure:`, params)
|
||||
return res.json({ result: { configured: true } })
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Transport Layer (unified routing: mesh > lan > tor)
|
||||
// =====================================================================
|
||||
case 'transport.status': {
|
||||
return res.json({
|
||||
result: {
|
||||
transports: [
|
||||
{ kind: 'mesh', available: true },
|
||||
{ kind: 'lan', available: true },
|
||||
{ kind: 'tor', available: true },
|
||||
],
|
||||
mesh_only: false,
|
||||
peer_count: 5,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'transport.peers': {
|
||||
return res.json({
|
||||
result: {
|
||||
peers: [
|
||||
{
|
||||
did: 'did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2ReMkBe4bR6XBIDNq9',
|
||||
pubkey_hex: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
|
||||
name: 'archy-198',
|
||||
trust_level: 'trusted',
|
||||
mesh_contact_id: 1,
|
||||
lan_address: '192.168.1.198:5678',
|
||||
onion_address: 'peer1abc2def3ghi4jkl5mno6pqr7stu8vwx9yz.onion',
|
||||
preferred_transport: 'lan',
|
||||
available_transports: ['mesh', 'lan', 'tor'],
|
||||
last_seen: new Date(Date.now() - 30000).toISOString(),
|
||||
},
|
||||
{
|
||||
did: 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH',
|
||||
pubkey_hex: 'f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5',
|
||||
name: 'satoshi-relay',
|
||||
trust_level: 'trusted',
|
||||
mesh_contact_id: 2,
|
||||
lan_address: null,
|
||||
onion_address: 'peer2xyz9wvu8tsr7qpo6nml5kji4hgf3edc2ba.onion',
|
||||
preferred_transport: 'mesh',
|
||||
available_transports: ['mesh', 'tor'],
|
||||
last_seen: new Date(Date.now() - 120000).toISOString(),
|
||||
},
|
||||
{
|
||||
did: 'did:key:z6MkrHKPxJP6tvCvXMaJKZd3rRA2Y44tyftVhR8FDCMKGFjb',
|
||||
pubkey_hex: 'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
|
||||
name: 'bunker-alpha',
|
||||
trust_level: 'observer',
|
||||
mesh_contact_id: 4,
|
||||
lan_address: null,
|
||||
onion_address: null,
|
||||
preferred_transport: 'mesh',
|
||||
available_transports: ['mesh'],
|
||||
last_seen: new Date(Date.now() - 45000).toISOString(),
|
||||
},
|
||||
{
|
||||
did: 'did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG',
|
||||
pubkey_hex: 'd4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5',
|
||||
name: 'office-node',
|
||||
trust_level: 'trusted',
|
||||
mesh_contact_id: null,
|
||||
lan_address: '192.168.1.42:5678',
|
||||
onion_address: 'peer4mno6pqr7stu8vwx9yzabc2def3ghi4jkl5.onion',
|
||||
preferred_transport: 'lan',
|
||||
available_transports: ['lan', 'tor'],
|
||||
last_seen: new Date(Date.now() - 60000).toISOString(),
|
||||
},
|
||||
{
|
||||
did: 'did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp7NQD5EjEREWh',
|
||||
pubkey_hex: 'e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6',
|
||||
name: 'remote-cabin',
|
||||
trust_level: 'trusted',
|
||||
mesh_contact_id: null,
|
||||
lan_address: null,
|
||||
onion_address: 'peer5xyz9abc2def3ghi4jkl5mno6pqr7stu8vw.onion',
|
||||
preferred_transport: 'tor',
|
||||
available_transports: ['tor'],
|
||||
last_seen: new Date(Date.now() - 300000).toISOString(),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'transport.send': {
|
||||
const targetDid = params?.did
|
||||
console.log(`[Transport] Send to ${targetDid} via best transport`)
|
||||
return res.json({
|
||||
result: {
|
||||
sent: true,
|
||||
transport_used: 'mesh',
|
||||
did: targetDid,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
case 'transport.set-mode': {
|
||||
const meshOnly = params?.mesh_only ?? false
|
||||
console.log(`[Transport] Set mesh_only mode: ${meshOnly}`)
|
||||
return res.json({ result: { mesh_only: meshOnly, configured: true } })
|
||||
}
|
||||
|
||||
default: {
|
||||
console.log(`[RPC] Unknown method: ${method}`)
|
||||
return res.json({
|
||||
|
||||
BIN
neode-ui/public/assets/img/bg-mesh.jpg
Normal file
BIN
neode-ui/public/assets/img/bg-mesh.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 MiB |
@@ -139,6 +139,11 @@ const router = createRouter({
|
||||
name: 'federation',
|
||||
component: () => import('../views/Federation.vue'),
|
||||
},
|
||||
{
|
||||
path: 'mesh',
|
||||
name: 'mesh',
|
||||
component: () => import('../views/Mesh.vue'),
|
||||
},
|
||||
{
|
||||
path: 'web5',
|
||||
name: 'web5',
|
||||
|
||||
189
neode-ui/src/stores/mesh.ts
Normal file
189
neode-ui/src/stores/mesh.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// Pinia store for mesh networking state (Meshcore LoRa)
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
export interface MeshStatus {
|
||||
enabled: boolean
|
||||
device_type: string
|
||||
device_path: string | null
|
||||
device_connected: boolean
|
||||
firmware_version: string | null
|
||||
self_node_id: number | null
|
||||
self_advert_name: string | null
|
||||
peer_count: number
|
||||
channel_name: string
|
||||
messages_sent: number
|
||||
messages_received: number
|
||||
detected_devices?: string[]
|
||||
}
|
||||
|
||||
export interface MeshPeer {
|
||||
contact_id: number
|
||||
advert_name: string
|
||||
did: string | null
|
||||
pubkey_hex: string | null
|
||||
rssi: number | null
|
||||
snr: number | null
|
||||
last_heard: string
|
||||
hops: number
|
||||
}
|
||||
|
||||
export interface MeshChannel {
|
||||
index: number
|
||||
name: string
|
||||
has_secret: boolean
|
||||
}
|
||||
|
||||
export interface MeshMessage {
|
||||
id: number
|
||||
direction: 'sent' | 'received'
|
||||
peer_contact_id: number
|
||||
peer_name: string | null
|
||||
plaintext: string
|
||||
timestamp: string
|
||||
delivered: boolean
|
||||
encrypted: boolean
|
||||
}
|
||||
|
||||
export const useMeshStore = defineStore('mesh', () => {
|
||||
const status = ref<MeshStatus | null>(null)
|
||||
const peers = ref<MeshPeer[]>([])
|
||||
const messages = ref<MeshMessage[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const sending = ref(false)
|
||||
|
||||
// Track unread message counts per peer (contact_id -> count)
|
||||
const unreadCounts = ref<Record<number, number>>({})
|
||||
// Currently viewing chat for this contact_id (clears unread)
|
||||
const viewingChatId = ref<number | null>(null)
|
||||
// Total unread count for nav badge
|
||||
const totalUnread = computed(() =>
|
||||
Object.values(unreadCounts.value).reduce((a, b) => a + b, 0)
|
||||
)
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const res = await rpcClient.call<MeshStatus>({ method: 'mesh.status' })
|
||||
status.value = res
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh status'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPeers() {
|
||||
try {
|
||||
const res = await rpcClient.call<{ peers: MeshPeer[]; count: number }>({
|
||||
method: 'mesh.peers',
|
||||
})
|
||||
peers.value = res.peers
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh peers'
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchMessages(limit?: number) {
|
||||
try {
|
||||
const res = await rpcClient.call<{ messages: MeshMessage[]; count: number }>({
|
||||
method: 'mesh.messages',
|
||||
params: limit ? { limit } : {},
|
||||
})
|
||||
// Detect new incoming messages and increment unread counts
|
||||
const newMsgs = res.messages.filter(
|
||||
m => m.direction === 'received' && !messages.value.some(existing => existing.id === m.id)
|
||||
)
|
||||
for (const msg of newMsgs) {
|
||||
// Don't count as unread if we're currently viewing that chat
|
||||
if (msg.peer_contact_id !== viewingChatId.value) {
|
||||
unreadCounts.value[msg.peer_contact_id] = (unreadCounts.value[msg.peer_contact_id] || 0) + 1
|
||||
}
|
||||
}
|
||||
messages.value = res.messages
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh messages'
|
||||
}
|
||||
}
|
||||
|
||||
function markChatRead(contactId: number) {
|
||||
viewingChatId.value = contactId
|
||||
delete unreadCounts.value[contactId]
|
||||
}
|
||||
|
||||
function clearViewingChat() {
|
||||
viewingChatId.value = null
|
||||
}
|
||||
|
||||
async function sendMessage(contactId: number, message: string) {
|
||||
try {
|
||||
sending.value = true
|
||||
error.value = null
|
||||
const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({
|
||||
method: 'mesh.send',
|
||||
params: { contact_id: contactId, message: message.trim() },
|
||||
})
|
||||
// Refresh messages after sending
|
||||
if (res.sent) {
|
||||
await fetchMessages()
|
||||
}
|
||||
return res
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to send mesh message'
|
||||
throw err
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function broadcastIdentity() {
|
||||
try {
|
||||
error.value = null
|
||||
await rpcClient.call<{ broadcast: boolean }>({ method: 'mesh.broadcast' })
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to broadcast identity'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function configure(config: Partial<MeshStatus>) {
|
||||
try {
|
||||
error.value = null
|
||||
await rpcClient.call<{ configured: boolean }>({
|
||||
method: 'mesh.configure',
|
||||
params: config,
|
||||
})
|
||||
await fetchStatus()
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to configure mesh'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([fetchStatus(), fetchPeers(), fetchMessages()])
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
peers,
|
||||
messages,
|
||||
loading,
|
||||
error,
|
||||
sending,
|
||||
unreadCounts,
|
||||
totalUnread,
|
||||
fetchStatus,
|
||||
fetchPeers,
|
||||
fetchMessages,
|
||||
sendMessage,
|
||||
broadcastIdentity,
|
||||
configure,
|
||||
refreshAll,
|
||||
markChatRead,
|
||||
clearViewingChat,
|
||||
}
|
||||
})
|
||||
113
neode-ui/src/stores/transport.ts
Normal file
113
neode-ui/src/stores/transport.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
// Pinia store for transport layer state (unified routing: mesh > lan > tor)
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
|
||||
export type TransportKind = 'mesh' | 'lan' | 'tor'
|
||||
|
||||
export interface TransportInfo {
|
||||
kind: TransportKind
|
||||
available: boolean
|
||||
}
|
||||
|
||||
export interface TransportStatus {
|
||||
transports: TransportInfo[]
|
||||
mesh_only: boolean
|
||||
peer_count: number
|
||||
}
|
||||
|
||||
export interface TransportPeer {
|
||||
did: string
|
||||
pubkey_hex: string
|
||||
name: string | null
|
||||
trust_level: string | null
|
||||
mesh_contact_id: number | null
|
||||
lan_address: string | null
|
||||
onion_address: string | null
|
||||
preferred_transport: TransportKind
|
||||
available_transports: TransportKind[]
|
||||
last_seen: string | null
|
||||
}
|
||||
|
||||
export const useTransportStore = defineStore('transport', () => {
|
||||
const status = ref<TransportStatus | null>(null)
|
||||
const peers = ref<TransportPeer[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const meshOnly = computed(() => status.value?.mesh_only ?? false)
|
||||
|
||||
const availableTransports = computed(() =>
|
||||
(status.value?.transports ?? []).filter((t) => t.available).map((t) => t.kind)
|
||||
)
|
||||
|
||||
async function fetchStatus() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
const res = await rpcClient.call<TransportStatus>({ method: 'transport.status' })
|
||||
status.value = res
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch transport status'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPeers() {
|
||||
try {
|
||||
const res = await rpcClient.call<{ peers: TransportPeer[] }>({
|
||||
method: 'transport.peers',
|
||||
})
|
||||
peers.value = res.peers
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to fetch transport peers'
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage(did: string, payload: string) {
|
||||
try {
|
||||
error.value = null
|
||||
const res = await rpcClient.call<{ sent: boolean; transport_used: TransportKind }>({
|
||||
method: 'transport.send',
|
||||
params: { did, payload },
|
||||
})
|
||||
return res
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to send via transport'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function setMeshOnly(enabled: boolean) {
|
||||
try {
|
||||
error.value = null
|
||||
await rpcClient.call<{ mesh_only: boolean; configured: boolean }>({
|
||||
method: 'transport.set-mode',
|
||||
params: { mesh_only: enabled },
|
||||
})
|
||||
await fetchStatus()
|
||||
} catch (err: unknown) {
|
||||
error.value = err instanceof Error ? err.message : 'Failed to set transport mode'
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshAll() {
|
||||
await Promise.all([fetchStatus(), fetchPeers()])
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
peers,
|
||||
loading,
|
||||
error,
|
||||
meshOnly,
|
||||
availableTransports,
|
||||
fetchStatus,
|
||||
fetchPeers,
|
||||
sendMessage,
|
||||
setMeshOnly,
|
||||
refreshAll,
|
||||
}
|
||||
})
|
||||
@@ -101,6 +101,10 @@
|
||||
v-if="item.path === '/dashboard/web5' && web5Badge.pendingRequestCount > 0"
|
||||
class="ml-auto w-5 h-5 flex items-center justify-center rounded-full bg-orange-500 text-white text-[10px] font-bold"
|
||||
>{{ web5Badge.pendingRequestCount }}</span>
|
||||
<span
|
||||
v-if="item.path === '/dashboard/mesh' && meshStore.totalUnread > 0"
|
||||
class="ml-auto w-5 h-5 flex items-center justify-center rounded-full bg-orange-500 text-white text-[10px] font-bold"
|
||||
>{{ meshStore.totalUnread }}</span>
|
||||
</RouterLink>
|
||||
|
||||
<!-- Chat launcher button -->
|
||||
@@ -406,6 +410,7 @@ import ModeSwitcher from '@/components/ModeSwitcher.vue'
|
||||
import { useUIModeStore } from '@/stores/uiMode'
|
||||
import { playDashboardLoadOomph } from '@/composables/useLoginSounds'
|
||||
import { useWeb5BadgeStore } from '@/stores/web5Badge'
|
||||
import { useMeshStore } from '@/stores/mesh'
|
||||
|
||||
const uiMode = useUIModeStore()
|
||||
|
||||
@@ -419,6 +424,7 @@ const store = useAppStore()
|
||||
const appLauncher = useAppLauncherStore()
|
||||
const loginTransition = useLoginTransitionStore()
|
||||
const web5Badge = useWeb5BadgeStore()
|
||||
const meshStore = useMeshStore()
|
||||
|
||||
const showZoomIn = ref(false)
|
||||
const pendingTimers: ReturnType<typeof setTimeout>[] = []
|
||||
@@ -444,6 +450,7 @@ const ROUTE_BACKGROUNDS: Record<string, string> = {
|
||||
'/dashboard/apps': 'bg-myapps.jpg',
|
||||
'/dashboard/marketplace': 'bg-appstore.jpg',
|
||||
'/dashboard/cloud': 'bg-cloud.jpg',
|
||||
'/dashboard/mesh': 'bg-mesh.jpg',
|
||||
'/dashboard/server': 'bg-network.jpg',
|
||||
'/dashboard/web5': 'bg-web5.jpg',
|
||||
'/dashboard/settings': 'bg-settings.jpg',
|
||||
@@ -665,6 +672,7 @@ const gamerDesktopNav: NavItem[] = [
|
||||
{ path: '/dashboard', label: 'Home', icon: 'home' },
|
||||
{ path: '/dashboard/apps', label: 'Apps', icon: 'apps', isCombined: true },
|
||||
{ path: '/dashboard/cloud', label: 'Cloud', icon: 'cloud' },
|
||||
{ path: '/dashboard/mesh', label: 'Mesh', icon: 'mesh' },
|
||||
{ path: '/dashboard/server', label: 'Network', icon: 'server' },
|
||||
{ path: '/dashboard/web5', label: 'Web5', icon: 'web5' },
|
||||
{ path: '/dashboard/settings', label: 'Settings', icon: 'settings' },
|
||||
@@ -723,6 +731,7 @@ function getIconPath(iconName: string): string[] {
|
||||
cloud: ['M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z'],
|
||||
server: ['M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01'],
|
||||
web5: ['M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9'],
|
||||
mesh: ['M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01M5.636 13.636a9 9 0 0112.728 0M1.5 10.5a14 14 0 0121 0'],
|
||||
chat: ['M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z'],
|
||||
settings: [
|
||||
'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z',
|
||||
@@ -752,6 +761,7 @@ const tabOrder = [
|
||||
'/dashboard/apps',
|
||||
'/dashboard/marketplace',
|
||||
'/dashboard/cloud',
|
||||
'/dashboard/mesh',
|
||||
'/dashboard/server',
|
||||
'/dashboard/web5',
|
||||
'/dashboard/chat',
|
||||
|
||||
1016
neode-ui/src/views/Mesh.vue
Normal file
1016
neode-ui/src/views/Mesh.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user