This commit is contained in:
Danish Arora 2025-09-09 12:31:26 +05:30
parent f8aed8e199
commit 082c824a69
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
9 changed files with 846 additions and 11 deletions

View File

@ -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 = () => (
<Route path="/post/:postId" element={<PostPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/bookmarks" element={<BookmarksPage />} />
<Route path="/debug" element={<DebugPage />} />
<Route path="*" element={<NotFound />} />
</Routes>
</TooltipProvider>

View File

@ -259,6 +259,16 @@ const Header = () => {
<span>Setup Wizard</span>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link
to="/debug"
className="flex items-center space-x-2"
>
<Terminal className="w-4 h-4" />
<span>Debug Console</span>
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator className="bg-cyber-muted/30" />
<DropdownMenuItem

View File

@ -0,0 +1,73 @@
import { Badge } from '@/components/ui/badge';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { AlertCircle, CheckCircle, Clock } from 'lucide-react';
interface MissingMessageIndicatorProps {
missingCount: number;
recoveredCount: number;
totalMissing: number;
totalRecovered: number;
className?: string;
}
export function MissingMessageIndicator({
missingCount,
recoveredCount,
totalMissing,
totalRecovered,
className = '',
}: MissingMessageIndicatorProps) {
// Don't show if no missing messages detected
if (totalMissing === 0) {
return null;
}
const recoveryRate = totalMissing > 0 ? (totalRecovered / totalMissing) * 100 : 0;
const hasActiveMissing = missingCount > 0;
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className={`flex items-center gap-2 ${className}`}>
{hasActiveMissing ? (
<Badge variant="destructive" className="flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
{missingCount} Missing
</Badge>
) : totalMissing > 0 ? (
<Badge variant="secondary" className="flex items-center gap-1">
<Clock className="h-3 w-3" />
Synced
</Badge>
) : null}
{totalRecovered > 0 && (
<Badge variant="default" className="flex items-center gap-1 bg-green-600 hover:bg-green-700">
<CheckCircle className="h-3 w-3" />
{recoveredCount > 0 ? `+${recoveredCount}` : totalRecovered} Recovered
</Badge>
)}
</div>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<div className="space-y-1 text-sm">
<div className="font-medium">Message Synchronization Status</div>
<div className="space-y-1 text-xs">
<div> Currently Missing: {missingCount}</div>
<div> Recently Recovered: {recoveredCount}</div>
<div> Total Missing (Session): {totalMissing}</div>
<div> Total Recovered (Session): {totalRecovered}</div>
<div> Recovery Rate: {recoveryRate.toFixed(1)}%</div>
</div>
{hasActiveMissing && (
<div className="text-xs text-muted-foreground mt-2">
The system is automatically attempting to recover missing messages.
</div>
)}
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

View File

@ -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<string | null>(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<UserVerificationStatus>({});
@ -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,

View File

@ -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<T>(storeName: StoreName): Promise<T[]> {
@ -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<void> {
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<void> {
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<void> {
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();

View File

@ -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<IDecodedMessage> | null = null;
private messageCallbacks: Map<string, MessageStatusCallback> = new Map();
private incomingMessageCallbacks: Set<IncomingMessageCallback> = new Set();
private missingMessageCallbacks: Set<MissingMessageCallback> = new Set();
private codecManager: CodecManager;
private seenMessages: Set<string> = new Set();
private missingMessages: Map<string, MissingMessageInfo> = new Map();
private recoveredMessages: Set<string> = 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<IDecodedMessage>
): 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;
}
}

View File

@ -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');

View File

@ -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<MessageReceivedCallback> = new Set();
private missingMessageCallbacks: Set<MissingMessageCallback> = 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();
}
}

385
src/pages/DebugPage.tsx Normal file
View File

@ -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<MessageStats>({
totalMessages: 0,
cellMessages: 0,
postMessages: 0,
commentMessages: 0,
voteMessages: 0,
moderationMessages: 0,
userProfileMessages: 0,
});
const [dbStats, setDbStats] = useState<any>(null);
const [realTimeStats, setRealTimeStats] = useState<any>(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 (
<div className="container mx-auto px-4 py-8 max-w-6xl">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold text-cyber-primary">Debug Console</h1>
<p className="text-cyber-neutral mt-2">
Real-time message synchronization and network diagnostics
</p>
</div>
<div className="flex gap-2">
<Button onClick={handleExportDebugInfo} variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export Debug Info
</Button>
<Button onClick={handleResetStats} variant="outline" size="sm">
<Trash2 className="h-4 w-4 mr-2" />
Reset Stats
</Button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
{/* Network Status */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<Activity className="h-5 w-5" />
Network Status
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Connection</span>
<Badge variant={isNetworkConnected ? "default" : "destructive"}>
{isNetworkConnected ? "Connected" : "Disconnected"}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Syncing</span>
<Badge variant={isSyncing ? "secondary" : "outline"}>
{isSyncing ? "Syncing" : "Idle"}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Last Sync</span>
<span className="text-xs text-cyber-neutral">
{lastSync ? new Date(lastSync).toLocaleTimeString() : "Never"}
</span>
</div>
</CardContent>
</Card>
{/* Missing Messages Overview */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
Missing Messages
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Currently Missing</span>
<Badge variant={missingMessageCount > 0 ? "destructive" : "outline"}>
{missingMessageCount}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Recently Recovered</span>
<Badge variant="default" className="bg-green-600">
{recoveredMessageCount}
</Badge>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Total Missing</span>
<span className="text-sm font-mono">{totalMissingMessages}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Total Recovered</span>
<span className="text-sm font-mono">{totalRecoveredMessages}</span>
</div>
</CardContent>
</Card>
{/* Recovery Rate */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2">
<CheckCircle className="h-5 w-5" />
Recovery Performance
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm text-cyber-neutral">Recovery Rate</span>
<span className="text-lg font-bold text-cyber-primary">
{recoveryRate.toFixed(1)}%
</span>
</div>
<div className="w-full bg-cyber-muted/20 rounded-full h-2">
<div
className="bg-green-500 h-2 rounded-full transition-all duration-300"
style={{ width: `${Math.min(recoveryRate, 100)}%` }}
/>
</div>
<div className="text-xs text-cyber-neutral">
{totalMissingMessages === 0
? "No missing messages detected"
: `${totalRecoveredMessages} of ${totalMissingMessages} recovered`}
</div>
</CardContent>
</Card>
</div>
{/* Message Statistics */}
<Card className="mb-8">
<CardHeader>
<CardTitle>Message Statistics</CardTitle>
<CardDescription>
Breakdown of cached messages by type
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-cyber-primary">{messageStats.totalMessages}</div>
<div className="text-sm text-cyber-neutral">Total Messages</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-cyber-primary">{messageStats.cellMessages}</div>
<div className="text-sm text-cyber-neutral">Cells</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-cyber-primary">{messageStats.postMessages}</div>
<div className="text-sm text-cyber-neutral">Posts</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-cyber-primary">{messageStats.commentMessages}</div>
<div className="text-sm text-cyber-neutral">Comments</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-cyber-primary">{messageStats.voteMessages}</div>
<div className="text-sm text-cyber-neutral">Votes</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-cyber-primary">{messageStats.moderationMessages}</div>
<div className="text-sm text-cyber-neutral">Moderations</div>
</div>
</div>
</CardContent>
</Card>
{/* Real-time Missing Message Details */}
{realTimeStats && realTimeStats.allMissingMessages && realTimeStats.allMissingMessages.length > 0 && (
<Card className="mb-8">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Clock className="h-5 w-5" />
Active Missing Messages
</CardTitle>
<CardDescription>
Messages currently being tracked for recovery
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2 max-h-60 overflow-y-auto">
{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 (
<div key={idx} className="flex items-center justify-between p-2 bg-cyber-muted/10 rounded border">
<div className="flex flex-col">
<span className="text-sm font-mono">
ID: {messageIdHex.substring(0, 16)}...
</span>
<span className="text-xs text-cyber-neutral">
Hint: {hintHex.substring(0, 24)}...
</span>
</div>
<Badge variant="destructive" size="sm">
Missing
</Badge>
</div>
);
})}
{realTimeStats.allMissingMessages.length > 20 && (
<div className="text-center text-sm text-cyber-neutral py-2">
... and {realTimeStats.allMissingMessages.length - 20} more
</div>
)}
</div>
</CardContent>
</Card>
)}
{/* Database Statistics */}
{dbStats && (
<Card>
<CardHeader>
<CardTitle>Persistent Statistics</CardTitle>
<CardDescription>
Statistics stored in local database
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-cyber-neutral">Last Detected</div>
<div className="text-sm font-mono">
{dbStats.lastDetected
? new Date(dbStats.lastDetected).toLocaleString()
: "Never"}
</div>
</div>
<div>
<div className="text-sm text-cyber-neutral">Last Recovered</div>
<div className="text-sm font-mono">
{dbStats.lastRecovered
? new Date(dbStats.lastRecovered).toLocaleString()
: "Never"}
</div>
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
};
export default DebugPage;