// 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(null) const peers = ref([]) const messages = ref([]) const loading = ref(false) const error = ref(null) const sending = ref(false) // Track unread message counts per peer (contact_id -> count) const unreadCounts = ref>({}) // Currently viewing chat for this contact_id (clears unread) const viewingChatId = ref(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({ 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) { 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, } })