From 082c824a69fd934092afc8ded63ec452fd89e3e7 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Tue, 9 Sep 2025 12:31:26 +0530 Subject: [PATCH] wip --- src/App.tsx | 2 + src/components/Header.tsx | 10 + .../ui/missing-message-indicator.tsx | 73 ++++ src/contexts/ForumContext.tsx | 92 +++++ src/lib/database/LocalDatabase.ts | 91 ++++- src/lib/waku/core/ReliableMessaging.ts | 130 ++++++ src/lib/waku/index.ts | 40 +- src/lib/waku/services/MessageService.ts | 34 +- src/pages/DebugPage.tsx | 385 ++++++++++++++++++ 9 files changed, 846 insertions(+), 11 deletions(-) create mode 100644 src/components/ui/missing-message-indicator.tsx create mode 100644 src/pages/DebugPage.tsx diff --git a/src/App.tsx b/src/App.tsx index a51f247..6fd1d28 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import Dashboard from './pages/Dashboard'; import Index from './pages/Index'; import ProfilePage from './pages/ProfilePage'; import BookmarksPage from './pages/BookmarksPage'; +import DebugPage from './pages/DebugPage'; import { appkitConfig } from './lib/wallet/config'; import { WagmiProvider } from 'wagmi'; import { config } from './lib/wallet/config'; @@ -52,6 +53,7 @@ const App = () => ( } /> } /> } /> + } /> } /> diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 1218f6e..a360893 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -259,6 +259,16 @@ const Header = () => { Setup Wizard + + + + Debug Console + + + 0 ? (totalRecovered / totalMissing) * 100 : 0; + const hasActiveMissing = missingCount > 0; + + return ( + + + +
+ {hasActiveMissing ? ( + + + {missingCount} Missing + + ) : totalMissing > 0 ? ( + + + Synced + + ) : null} + + {totalRecovered > 0 && ( + + + {recoveredCount > 0 ? `+${recoveredCount}` : totalRecovered} Recovered + + )} +
+
+ +
+
Message Synchronization Status
+
+
• Currently Missing: {missingCount}
+
• Recently Recovered: {recoveredCount}
+
• Total Missing (Session): {totalMissing}
+
• Total Recovered (Session): {totalRecovered}
+
• Recovery Rate: {recoveryRate.toFixed(1)}%
+
+ {hasActiveMissing && ( +
+ The system is automatically attempting to recover missing messages. +
+ )} +
+
+
+
+ ); +} diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index 9095d61..148b6cb 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -47,6 +47,11 @@ interface ForumContextType { // Network status isNetworkConnected: boolean; error: string | null; + // Missing message tracking + missingMessageCount: number; + recoveredMessageCount: number; + totalMissingMessages: number; + totalRecoveredMessages: number; getCellById: (id: string) => Cell | undefined; getPostsByCell: (cellId: string) => Post[]; getCommentsByPost: (postId: string) => Comment[]; @@ -104,6 +109,10 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { const [isRefreshing, setIsRefreshing] = useState(false); const [isNetworkConnected, setIsNetworkConnected] = useState(false); const [error, setError] = useState(null); + const [missingMessageCount, setMissingMessageCount] = useState(0); + const [recoveredMessageCount, setRecoveredMessageCount] = useState(0); + const [totalMissingMessages, setTotalMissingMessages] = useState(0); + const [totalRecoveredMessages, setTotalRecoveredMessages] = useState(0); const [userVerificationStatus, setUserVerificationStatus] = useState({}); @@ -328,6 +337,85 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { return unsubscribe; }, [updateStateFromCache]); + // Subscribe to missing message events for tracking + useEffect(() => { + // Defensive check: only setup if the method exists + if (!messageManager.onMissingMessage || typeof messageManager.onMissingMessage !== 'function') { + console.warn('Missing message tracking not available - messageManager.onMissingMessage not found'); + return; + } + + const unsubscribe = messageManager.onMissingMessage(async (event) => { + const newMissingCount = event.missingMessages.length; + console.log(`Missing messages detected: ${newMissingCount}`); + + // Update local state + setMissingMessageCount(prev => prev + newMissingCount); + + // Update database stats + await localDatabase.recordMissingMessages(newMissingCount); + + // Update total from database + const stats = localDatabase.getMissingMessageStats(); + setTotalMissingMessages(stats.totalMissing); + setTotalRecoveredMessages(stats.totalRecovered); + + // Show toast notification for significant missing message counts + if (newMissingCount > 5) { + toast({ + title: 'Messages Missing', + description: `${newMissingCount} messages detected as missing. The system will attempt to recover them automatically.`, + variant: 'default', + }); + } + }); + + return unsubscribe; + }, [toast]); + + // Initialize missing message stats from database + useEffect(() => { + const loadStats = async () => { + const stats = localDatabase.getMissingMessageStats(); + setTotalMissingMessages(stats.totalMissing); + setTotalRecoveredMessages(stats.totalRecovered); + + // Get current real-time counts from messageManager + if (messageManager.isReady && messageManager.getMissingMessageCount) { + setMissingMessageCount(messageManager.getMissingMessageCount()); + setRecoveredMessageCount(messageManager.getRecoveredMessageCount()); + } + }; + + loadStats(); + }, []); + + // Track message recovery in real-time + useEffect(() => { + if (!isNetworkConnected || !messageManager.isReady || !messageManager.getRecoveredMessageCount) return; + + const interval = setInterval(() => { + const currentRecovered = messageManager.getRecoveredMessageCount(); + const previousRecovered = recoveredMessageCount; + + if (currentRecovered > previousRecovered) { + const newRecoveredCount = currentRecovered - previousRecovered; + setRecoveredMessageCount(currentRecovered); + + // Update database with new recovered messages + localDatabase.recordRecoveredMessages(newRecoveredCount); + + // Update totals + const stats = localDatabase.getMissingMessageStats(); + setTotalRecoveredMessages(stats.totalRecovered); + + console.log(`Messages recovered: ${newRecoveredCount}`); + } + }, 2000); // Check every 2 seconds + + return () => clearInterval(interval); + }, [isNetworkConnected, recoveredMessageCount]); + // Simple reactive updates: check for new data periodically when connected useEffect(() => { if (!isNetworkConnected) return; @@ -642,6 +730,10 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { isRefreshing, isNetworkConnected, error, + missingMessageCount, + recoveredMessageCount, + totalMissingMessages, + totalRecoveredMessages, getCellById, getPostsByCell, getCommentsByPost, diff --git a/src/lib/database/LocalDatabase.ts b/src/lib/database/LocalDatabase.ts index d98288d..567b5c6 100644 --- a/src/lib/database/LocalDatabase.ts +++ b/src/lib/database/LocalDatabase.ts @@ -19,6 +19,13 @@ import { DelegationInfo } from '@/lib/delegation/types'; import { openLocalDB, STORE, StoreName } from '@/lib/database/schema'; import { Bookmark, BookmarkCache } from '@/types/forum'; +export interface MissingMessageStats { + totalMissing: number; + totalRecovered: number; + lastDetected: number | null; + lastRecovered: number | null; +} + export interface LocalDatabaseCache { cells: CellCache; posts: PostCache; @@ -27,6 +34,7 @@ export interface LocalDatabaseCache { moderations: { [targetId: string]: ModerateMessage }; userIdentities: UserIdentityCache; bookmarks: BookmarkCache; + missingMessageStats: MissingMessageStats; } /** @@ -50,6 +58,12 @@ export class LocalDatabase { moderations: {}, userIdentities: {}, bookmarks: {}, + missingMessageStats: { + totalMissing: 0, + totalRecovered: 0, + lastDetected: null, + lastRecovered: null, + }, }; constructor() { @@ -255,15 +269,21 @@ export class LocalDatabase { const meta = await this.getAllFromStore<{ key: string; value: unknown }>( STORE.META ); - meta - .filter( - entry => - typeof entry.key === 'string' && entry.key.startsWith('pending:') - ) - .forEach(entry => { - const id = (entry.key as string).substring('pending:'.length); - this.pendingIds.add(id); - }); + + meta.forEach(entry => { + if (typeof entry.key === 'string') { + if (entry.key.startsWith('pending:')) { + const id = entry.key.substring('pending:'.length); + this.pendingIds.add(id); + } else if (entry.key === 'missingMessageStats') { + // Load missing message stats from persistence + const stats = entry.value as MissingMessageStats; + if (stats) { + this.cache.missingMessageStats = { ...stats }; + } + } + } + }); } private getAllFromStore(storeName: StoreName): Promise { @@ -584,6 +604,59 @@ export class LocalDatabase { public getAllBookmarks(): Bookmark[] { return Object.values(this.cache.bookmarks); } + + /** + * Track missing messages detected + */ + public async recordMissingMessages(count: number): Promise { + this.cache.missingMessageStats.totalMissing += count; + this.cache.missingMessageStats.lastDetected = Date.now(); + + // Persist to IndexedDB + await this.put(STORE.META, { + key: 'missingMessageStats', + value: this.cache.missingMessageStats, + }); + } + + /** + * Track messages recovered + */ + public async recordRecoveredMessages(count: number): Promise { + this.cache.missingMessageStats.totalRecovered += count; + this.cache.missingMessageStats.lastRecovered = Date.now(); + + // Persist to IndexedDB + await this.put(STORE.META, { + key: 'missingMessageStats', + value: this.cache.missingMessageStats, + }); + } + + /** + * Get missing message statistics + */ + public getMissingMessageStats(): MissingMessageStats { + return { ...this.cache.missingMessageStats }; + } + + /** + * Reset missing message statistics + */ + public async resetMissingMessageStats(): Promise { + this.cache.missingMessageStats = { + totalMissing: 0, + totalRecovered: 0, + lastDetected: null, + lastRecovered: null, + }; + + // Persist to IndexedDB + await this.put(STORE.META, { + key: 'missingMessageStats', + value: this.cache.missingMessageStats, + }); + } } export const localDatabase = new LocalDatabase(); diff --git a/src/lib/waku/core/ReliableMessaging.ts b/src/lib/waku/core/ReliableMessaging.ts index 1f6dc96..400abd3 100644 --- a/src/lib/waku/core/ReliableMessaging.ts +++ b/src/lib/waku/core/ReliableMessaging.ts @@ -7,6 +7,7 @@ import { import { CodecManager } from '../CodecManager'; import { generateStringId } from '@/lib/utils'; import { OpchanMessage } from '@/types/forum'; +import { HistoryEntry, MessageChannelEvent } from '@waku/sds'; export interface MessageStatusCallback { onSent?: (messageId: string) => void; @@ -14,13 +15,27 @@ export interface MessageStatusCallback { onError?: (messageId: string, error: string) => void; } +export interface MissingMessageInfo { + messageId: Uint8Array; + retrievalHint?: Uint8Array; +} + +export interface MissingMessageEvent { + missingMessages: MissingMessageInfo[]; +} + export type IncomingMessageCallback = (message: OpchanMessage) => void; +export type MissingMessageCallback = (event: MissingMessageEvent) => void; export class ReliableMessaging { private channel: ReliableChannel | null = null; private messageCallbacks: Map = new Map(); private incomingMessageCallbacks: Set = new Set(); + private missingMessageCallbacks: Set = new Set(); private codecManager: CodecManager; + private seenMessages: Set = new Set(); + private missingMessages: Map = new Map(); + private recoveredMessages: Set = new Set(); constructor(node: LightNode) { this.codecManager = new CodecManager(node); @@ -53,7 +68,25 @@ export class ReliableMessaging { channel.addEventListener(ReliableChannelEvent.InMessageReceived, event => { try { const wakuMessage = event.detail; + console.log('Received incoming message:', wakuMessage); if (wakuMessage.payload) { + // Check if this message fills a gap in missing messages + const messageId = this.getMessageIdFromPayload(wakuMessage.payload); + if (messageId && this.missingMessages.has(messageId)) { + this.recoveredMessages.add(messageId); + this.missingMessages.delete(messageId); + console.log('Missing message recovered:', messageId.substring(0, 12) + '...'); + } + + // Mark message as seen to avoid processing duplicates + if (messageId) { + if (this.seenMessages.has(messageId)) { + console.log('Duplicate message ignored:', messageId.substring(0, 12) + '...'); + return; + } + this.seenMessages.add(messageId); + } + const opchanMessage = this.codecManager.decodeMessage( wakuMessage.payload ); @@ -93,6 +126,78 @@ export class ReliableMessaging { this.messageCallbacks.delete(messageId); } ); + + // Setup missing message detection + this.setupMissingMessageDetection(channel); + } + + private setupMissingMessageDetection( + channel: ReliableChannel + ): void { + try { + + const messageChannel = (channel).messageChannel; + + messageChannel.addEventListener(MessageChannelEvent.InMessageMissing, (event) => { + this.handleMissingMessages(event.detail); + }); + console.log('Missing message detection enabled'); + + } catch (error) { + console.warn('Failed to setup missing message detection:', error); + } + } + + + private handleMissingMessages(missingMessageData: HistoryEntry[]): void { + try { + const missingMessages: MissingMessageInfo[] = []; + + if (Array.isArray(missingMessageData)) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + missingMessageData.forEach((item: any) => { + if (item.messageId) { + const messageId = this.bytesToHex(item.messageId); + const missingInfo: MissingMessageInfo = { + messageId: item.messageId, + retrievalHint: item.retrievalHint, + }; + + // Store missing message info for tracking + if (!this.missingMessages.has(messageId) && !this.recoveredMessages.has(messageId)) { + this.missingMessages.set(messageId, missingInfo); + missingMessages.push(missingInfo); + } + } + }); + } + + if (missingMessages.length > 0) { + console.log(`Detected ${missingMessages.length} missing messages`); + const event: MissingMessageEvent = { missingMessages }; + this.missingMessageCallbacks.forEach(callback => callback(event)); + } + } catch (error) { + console.error('Failed to handle missing messages:', error); + } + } + + private getMessageIdFromPayload(payload: Uint8Array): string | null { + try { + const messageId = (ReliableChannel).getMessageId?.(payload); + if (messageId) { + return this.bytesToHex(messageId); + } + return null; + } catch (error) { + console.warn('Failed to get message ID from payload:', error); + return null; + } + } + + private bytesToHex(bytes: Uint8Array): string { + if (!bytes || bytes.length === 0) return ''; + return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join(''); } public async sendMessage( @@ -123,9 +228,34 @@ export class ReliableMessaging { return () => this.incomingMessageCallbacks.delete(callback); } + public onMissingMessage(callback: MissingMessageCallback): () => void { + this.missingMessageCallbacks.add(callback); + return () => this.missingMessageCallbacks.delete(callback); + } + + public getMissingMessages(): MissingMessageInfo[] { + return Array.from(this.missingMessages.values()); + } + + public getRecoveredMessages(): string[] { + return Array.from(this.recoveredMessages); + } + + public getMissingMessageCount(): number { + return this.missingMessages.size; + } + + public getRecoveredMessageCount(): number { + return this.recoveredMessages.size; + } + public cleanup(): void { this.messageCallbacks.clear(); this.incomingMessageCallbacks.clear(); + this.missingMessageCallbacks.clear(); + this.seenMessages.clear(); + this.missingMessages.clear(); + this.recoveredMessages.clear(); this.channel = null; } } diff --git a/src/lib/waku/index.ts b/src/lib/waku/index.ts index 0d50633..bbb55d2 100644 --- a/src/lib/waku/index.ts +++ b/src/lib/waku/index.ts @@ -4,10 +4,13 @@ import { WakuNodeManager, HealthChangeCallback } from './core/WakuNodeManager'; import { MessageService, MessageStatusCallback, + MissingMessageCallback, + MissingMessageEvent, + MissingMessageInfo, } from './services/MessageService'; import { ReliableMessaging } from './core/ReliableMessaging'; -export type { HealthChangeCallback, MessageStatusCallback }; +export type { HealthChangeCallback, MessageStatusCallback, MissingMessageCallback, MissingMessageEvent, MissingMessageInfo }; class MessageManager { private nodeManager: WakuNodeManager | null = null; @@ -114,6 +117,41 @@ class MessageManager { return this.messageService.onMessageReceived(callback); } + public onMissingMessage(callback: MissingMessageCallback): () => void { + if (!this.messageService) { + throw new Error('MessageManager not fully initialized'); + } + return this.messageService.onMissingMessage(callback); + } + + public getMissingMessages(): MissingMessageInfo[] { + if (!this.messageService) { + return []; + } + return this.messageService.getMissingMessages(); + } + + public getRecoveredMessages(): string[] { + if (!this.messageService) { + return []; + } + return this.messageService.getRecoveredMessages(); + } + + public getMissingMessageCount(): number { + if (!this.messageService) { + return 0; + } + return this.messageService.getMissingMessageCount(); + } + + public getRecoveredMessageCount(): number { + if (!this.messageService) { + return 0; + } + return this.messageService.getRecoveredMessageCount(); + } + public get messageCache() { if (!this.messageService) { throw new Error('MessageManager not fully initialized'); diff --git a/src/lib/waku/services/MessageService.ts b/src/lib/waku/services/MessageService.ts index 50cb61d..954bd00 100644 --- a/src/lib/waku/services/MessageService.ts +++ b/src/lib/waku/services/MessageService.ts @@ -2,15 +2,19 @@ import { OpchanMessage } from '@/types/forum'; import { ReliableMessaging, MessageStatusCallback, + MissingMessageEvent, + MissingMessageInfo, } from '../core/ReliableMessaging'; import { WakuNodeManager } from '../core/WakuNodeManager'; import { localDatabase } from '@/lib/database/LocalDatabase'; export type MessageReceivedCallback = (message: OpchanMessage) => void; -export type { MessageStatusCallback }; +export type MissingMessageCallback = (event: MissingMessageEvent) => void; +export type { MessageStatusCallback, MissingMessageEvent, MissingMessageInfo }; export class MessageService { private messageReceivedCallbacks: Set = new Set(); + private missingMessageCallbacks: Set = new Set(); constructor( private reliableMessaging: ReliableMessaging | null, @@ -29,6 +33,12 @@ export class MessageService { localDatabase.setSyncing(false); if (isNew) this.messageReceivedCallbacks.forEach(cb => cb(message)); }); + + // Setup missing message handling + this.reliableMessaging.onMissingMessage(event => { + console.log(`Missing messages detected: ${event.missingMessages.length}`); + this.missingMessageCallbacks.forEach(cb => cb(event)); + }); } } @@ -86,6 +96,27 @@ export class MessageService { return () => this.messageReceivedCallbacks.delete(callback); } + public onMissingMessage(callback: MissingMessageCallback): () => void { + this.missingMessageCallbacks.add(callback); + return () => this.missingMessageCallbacks.delete(callback); + } + + public getMissingMessages(): MissingMessageInfo[] { + return this.reliableMessaging?.getMissingMessages() || []; + } + + public getRecoveredMessages(): string[] { + return this.reliableMessaging?.getRecoveredMessages() || []; + } + + public getMissingMessageCount(): number { + return this.reliableMessaging?.getMissingMessageCount() || 0; + } + + public getRecoveredMessageCount(): number { + return this.reliableMessaging?.getRecoveredMessageCount() || 0; + } + public updateReliableMessaging( reliableMessaging: ReliableMessaging | null ): void { @@ -99,6 +130,7 @@ export class MessageService { public cleanup(): void { this.messageReceivedCallbacks.clear(); + this.missingMessageCallbacks.clear(); this.reliableMessaging?.cleanup(); } } diff --git a/src/pages/DebugPage.tsx b/src/pages/DebugPage.tsx new file mode 100644 index 0000000..e067cd3 --- /dev/null +++ b/src/pages/DebugPage.tsx @@ -0,0 +1,385 @@ +import React, { useState, useEffect } from 'react'; +import { useForum } from '@/contexts/useForum'; +import { localDatabase } from '@/lib/database/LocalDatabase'; +import messageManager from '@/lib/waku'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { RefreshCw, Trash2, Download, AlertCircle, CheckCircle, Clock, Activity } from 'lucide-react'; +import { useToast } from '@/components/ui/use-toast'; + +interface MessageStats { + totalMessages: number; + cellMessages: number; + postMessages: number; + commentMessages: number; + voteMessages: number; + moderationMessages: number; + userProfileMessages: number; +} + +const DebugPage = () => { + const { + missingMessageCount, + recoveredMessageCount, + totalMissingMessages, + totalRecoveredMessages, + lastSync, + isSyncing, + isNetworkConnected, + } = useForum(); + + const [messageStats, setMessageStats] = useState({ + totalMessages: 0, + cellMessages: 0, + postMessages: 0, + commentMessages: 0, + voteMessages: 0, + moderationMessages: 0, + userProfileMessages: 0, + }); + + const [dbStats, setDbStats] = useState(null); + const [realTimeStats, setRealTimeStats] = useState(null); + const { toast } = useToast(); + + // Update stats periodically + useEffect(() => { + const updateStats = () => { + const cache = localDatabase.cache; + const messageCache = messageManager.messageCache; + + setMessageStats({ + totalMessages: Object.keys(cache.cells).length + + Object.keys(cache.posts).length + + Object.keys(cache.comments).length + + Object.keys(cache.votes).length + + Object.keys(cache.moderations).length + + Object.keys(cache.userIdentities).length, + cellMessages: Object.keys(cache.cells).length, + postMessages: Object.keys(cache.posts).length, + commentMessages: Object.keys(cache.comments).length, + voteMessages: Object.keys(cache.votes).length, + moderationMessages: Object.keys(cache.moderations).length, + userProfileMessages: Object.keys(cache.userIdentities).length, + }); + + setDbStats(localDatabase.getMissingMessageStats()); + + if (messageManager.isReady && messageManager.getMissingMessageCount) { + setRealTimeStats({ + missingMessages: messageManager.getMissingMessageCount(), + recoveredMessages: messageManager.getRecoveredMessageCount(), + allMissingMessages: messageManager.getMissingMessages(), + allRecoveredMessages: messageManager.getRecoveredMessages(), + }); + } + }; + + updateStats(); + const interval = setInterval(updateStats, 2000); + return () => clearInterval(interval); + }, []); + + const handleResetStats = async () => { + try { + await localDatabase.resetMissingMessageStats(); + toast({ + title: 'Stats Reset', + description: 'Missing message statistics have been reset.', + }); + } catch (error) { + toast({ + title: 'Reset Failed', + description: 'Failed to reset statistics.', + variant: 'destructive', + }); + } + }; + + const handleExportDebugInfo = () => { + const debugInfo = { + timestamp: new Date().toISOString(), + network: { + isConnected: isNetworkConnected, + lastSync: lastSync, + isSyncing: isSyncing, + }, + missingMessages: { + current: { + missing: missingMessageCount, + recovered: recoveredMessageCount, + }, + total: { + missing: totalMissingMessages, + recovered: totalRecoveredMessages, + }, + database: dbStats, + realTime: realTimeStats, + }, + messageStats, + cache: { + cellsCount: Object.keys(localDatabase.cache.cells).length, + postsCount: Object.keys(localDatabase.cache.posts).length, + commentsCount: Object.keys(localDatabase.cache.comments).length, + votesCount: Object.keys(localDatabase.cache.votes).length, + moderationsCount: Object.keys(localDatabase.cache.moderations).length, + userIdentitiesCount: Object.keys(localDatabase.cache.userIdentities).length, + }, + }; + + const blob = new Blob([JSON.stringify(debugInfo, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `opchan-debug-${Date.now()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + toast({ + title: 'Debug Info Exported', + description: 'Debug information has been downloaded as JSON.', + }); + }; + + const recoveryRate = totalMissingMessages > 0 ? (totalRecoveredMessages / totalMissingMessages) * 100 : 0; + + return ( +
+
+
+

Debug Console

+

+ Real-time message synchronization and network diagnostics +

+
+
+ + +
+
+ +
+ {/* Network Status */} + + + + + Network Status + + + +
+ Connection + + {isNetworkConnected ? "Connected" : "Disconnected"} + +
+
+ Syncing + + {isSyncing ? "Syncing" : "Idle"} + +
+
+ Last Sync + + {lastSync ? new Date(lastSync).toLocaleTimeString() : "Never"} + +
+
+
+ + {/* Missing Messages Overview */} + + + + + Missing Messages + + + +
+ Currently Missing + 0 ? "destructive" : "outline"}> + {missingMessageCount} + +
+
+ Recently Recovered + + {recoveredMessageCount} + +
+
+ Total Missing + {totalMissingMessages} +
+
+ Total Recovered + {totalRecoveredMessages} +
+
+
+ + {/* Recovery Rate */} + + + + + Recovery Performance + + + +
+ Recovery Rate + + {recoveryRate.toFixed(1)}% + +
+
+
+
+
+ {totalMissingMessages === 0 + ? "No missing messages detected" + : `${totalRecoveredMessages} of ${totalMissingMessages} recovered`} +
+ + +
+ + {/* Message Statistics */} + + + Message Statistics + + Breakdown of cached messages by type + + + +
+
+
{messageStats.totalMessages}
+
Total Messages
+
+
+
{messageStats.cellMessages}
+
Cells
+
+
+
{messageStats.postMessages}
+
Posts
+
+
+
{messageStats.commentMessages}
+
Comments
+
+
+
{messageStats.voteMessages}
+
Votes
+
+
+
{messageStats.moderationMessages}
+
Moderations
+
+
+
+
+ + {/* Real-time Missing Message Details */} + {realTimeStats && realTimeStats.allMissingMessages && realTimeStats.allMissingMessages.length > 0 && ( + + + + + Active Missing Messages + + + Messages currently being tracked for recovery + + + +
+ {realTimeStats.allMissingMessages.slice(0, 20).map((msg: any, idx: number) => { + const messageIdHex = Array.from(msg.messageId, (byte: number) => + byte.toString(16).padStart(2, '0') + ).join(''); + const hintHex = msg.retrievalHint ? Array.from(msg.retrievalHint, (byte: number) => + byte.toString(16).padStart(2, '0') + ).join('') : 'N/A'; + + return ( +
+
+ + ID: {messageIdHex.substring(0, 16)}... + + + Hint: {hintHex.substring(0, 24)}... + +
+ + Missing + +
+ ); + })} + {realTimeStats.allMissingMessages.length > 20 && ( +
+ ... and {realTimeStats.allMissingMessages.length - 20} more +
+ )} +
+
+
+ )} + + {/* Database Statistics */} + {dbStats && ( + + + Persistent Statistics + + Statistics stored in local database + + + +
+
+
Last Detected
+
+ {dbStats.lastDetected + ? new Date(dbStats.lastDetected).toLocaleString() + : "Never"} +
+
+
+
Last Recovered
+
+ {dbStats.lastRecovered + ? new Date(dbStats.lastRecovered).toLocaleString() + : "Never"} +
+
+
+
+
+ )} +
+ ); +}; + +export default DebugPage;