backup commit

This commit is contained in:
Dorian
2026-03-17 00:03:08 +00:00
parent f23be63bba
commit 253c305cc8
43 changed files with 9514 additions and 308 deletions

View File

@@ -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
View 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,
}
})

View 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,
}
})

View File

@@ -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

File diff suppressed because it is too large Load Diff