mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-04-03 08:43:20 +00:00
wip
This commit is contained in:
parent
f8aed8e199
commit
082c824a69
@ -26,6 +26,7 @@ import Dashboard from './pages/Dashboard';
|
|||||||
import Index from './pages/Index';
|
import Index from './pages/Index';
|
||||||
import ProfilePage from './pages/ProfilePage';
|
import ProfilePage from './pages/ProfilePage';
|
||||||
import BookmarksPage from './pages/BookmarksPage';
|
import BookmarksPage from './pages/BookmarksPage';
|
||||||
|
import DebugPage from './pages/DebugPage';
|
||||||
import { appkitConfig } from './lib/wallet/config';
|
import { appkitConfig } from './lib/wallet/config';
|
||||||
import { WagmiProvider } from 'wagmi';
|
import { WagmiProvider } from 'wagmi';
|
||||||
import { config } from './lib/wallet/config';
|
import { config } from './lib/wallet/config';
|
||||||
@ -52,6 +53,7 @@ const App = () => (
|
|||||||
<Route path="/post/:postId" element={<PostPage />} />
|
<Route path="/post/:postId" element={<PostPage />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
<Route path="/profile" element={<ProfilePage />} />
|
||||||
<Route path="/bookmarks" element={<BookmarksPage />} />
|
<Route path="/bookmarks" element={<BookmarksPage />} />
|
||||||
|
<Route path="/debug" element={<DebugPage />} />
|
||||||
<Route path="*" element={<NotFound />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
|||||||
@ -259,6 +259,16 @@ const Header = () => {
|
|||||||
<span>Setup Wizard</span>
|
<span>Setup Wizard</span>
|
||||||
</DropdownMenuItem>
|
</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" />
|
<DropdownMenuSeparator className="bg-cyber-muted/30" />
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
|
|||||||
73
src/components/ui/missing-message-indicator.tsx
Normal file
73
src/components/ui/missing-message-indicator.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -47,6 +47,11 @@ interface ForumContextType {
|
|||||||
// Network status
|
// Network status
|
||||||
isNetworkConnected: boolean;
|
isNetworkConnected: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
// Missing message tracking
|
||||||
|
missingMessageCount: number;
|
||||||
|
recoveredMessageCount: number;
|
||||||
|
totalMissingMessages: number;
|
||||||
|
totalRecoveredMessages: number;
|
||||||
getCellById: (id: string) => Cell | undefined;
|
getCellById: (id: string) => Cell | undefined;
|
||||||
getPostsByCell: (cellId: string) => Post[];
|
getPostsByCell: (cellId: string) => Post[];
|
||||||
getCommentsByPost: (postId: string) => Comment[];
|
getCommentsByPost: (postId: string) => Comment[];
|
||||||
@ -104,6 +109,10 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [isNetworkConnected, setIsNetworkConnected] = useState(false);
|
const [isNetworkConnected, setIsNetworkConnected] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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] =
|
const [userVerificationStatus, setUserVerificationStatus] =
|
||||||
useState<UserVerificationStatus>({});
|
useState<UserVerificationStatus>({});
|
||||||
|
|
||||||
@ -328,6 +337,85 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [updateStateFromCache]);
|
}, [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
|
// Simple reactive updates: check for new data periodically when connected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isNetworkConnected) return;
|
if (!isNetworkConnected) return;
|
||||||
@ -642,6 +730,10 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
|
|||||||
isRefreshing,
|
isRefreshing,
|
||||||
isNetworkConnected,
|
isNetworkConnected,
|
||||||
error,
|
error,
|
||||||
|
missingMessageCount,
|
||||||
|
recoveredMessageCount,
|
||||||
|
totalMissingMessages,
|
||||||
|
totalRecoveredMessages,
|
||||||
getCellById,
|
getCellById,
|
||||||
getPostsByCell,
|
getPostsByCell,
|
||||||
getCommentsByPost,
|
getCommentsByPost,
|
||||||
|
|||||||
@ -19,6 +19,13 @@ import { DelegationInfo } from '@/lib/delegation/types';
|
|||||||
import { openLocalDB, STORE, StoreName } from '@/lib/database/schema';
|
import { openLocalDB, STORE, StoreName } from '@/lib/database/schema';
|
||||||
import { Bookmark, BookmarkCache } from '@/types/forum';
|
import { Bookmark, BookmarkCache } from '@/types/forum';
|
||||||
|
|
||||||
|
export interface MissingMessageStats {
|
||||||
|
totalMissing: number;
|
||||||
|
totalRecovered: number;
|
||||||
|
lastDetected: number | null;
|
||||||
|
lastRecovered: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LocalDatabaseCache {
|
export interface LocalDatabaseCache {
|
||||||
cells: CellCache;
|
cells: CellCache;
|
||||||
posts: PostCache;
|
posts: PostCache;
|
||||||
@ -27,6 +34,7 @@ export interface LocalDatabaseCache {
|
|||||||
moderations: { [targetId: string]: ModerateMessage };
|
moderations: { [targetId: string]: ModerateMessage };
|
||||||
userIdentities: UserIdentityCache;
|
userIdentities: UserIdentityCache;
|
||||||
bookmarks: BookmarkCache;
|
bookmarks: BookmarkCache;
|
||||||
|
missingMessageStats: MissingMessageStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,6 +58,12 @@ export class LocalDatabase {
|
|||||||
moderations: {},
|
moderations: {},
|
||||||
userIdentities: {},
|
userIdentities: {},
|
||||||
bookmarks: {},
|
bookmarks: {},
|
||||||
|
missingMessageStats: {
|
||||||
|
totalMissing: 0,
|
||||||
|
totalRecovered: 0,
|
||||||
|
lastDetected: null,
|
||||||
|
lastRecovered: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -255,15 +269,21 @@ export class LocalDatabase {
|
|||||||
const meta = await this.getAllFromStore<{ key: string; value: unknown }>(
|
const meta = await this.getAllFromStore<{ key: string; value: unknown }>(
|
||||||
STORE.META
|
STORE.META
|
||||||
);
|
);
|
||||||
meta
|
|
||||||
.filter(
|
meta.forEach(entry => {
|
||||||
entry =>
|
if (typeof entry.key === 'string') {
|
||||||
typeof entry.key === 'string' && entry.key.startsWith('pending:')
|
if (entry.key.startsWith('pending:')) {
|
||||||
)
|
const id = entry.key.substring('pending:'.length);
|
||||||
.forEach(entry => {
|
this.pendingIds.add(id);
|
||||||
const id = (entry.key as string).substring('pending:'.length);
|
} else if (entry.key === 'missingMessageStats') {
|
||||||
this.pendingIds.add(id);
|
// 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[]> {
|
private getAllFromStore<T>(storeName: StoreName): Promise<T[]> {
|
||||||
@ -584,6 +604,59 @@ export class LocalDatabase {
|
|||||||
public getAllBookmarks(): Bookmark[] {
|
public getAllBookmarks(): Bookmark[] {
|
||||||
return Object.values(this.cache.bookmarks);
|
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();
|
export const localDatabase = new LocalDatabase();
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
import { CodecManager } from '../CodecManager';
|
import { CodecManager } from '../CodecManager';
|
||||||
import { generateStringId } from '@/lib/utils';
|
import { generateStringId } from '@/lib/utils';
|
||||||
import { OpchanMessage } from '@/types/forum';
|
import { OpchanMessage } from '@/types/forum';
|
||||||
|
import { HistoryEntry, MessageChannelEvent } from '@waku/sds';
|
||||||
|
|
||||||
export interface MessageStatusCallback {
|
export interface MessageStatusCallback {
|
||||||
onSent?: (messageId: string) => void;
|
onSent?: (messageId: string) => void;
|
||||||
@ -14,13 +15,27 @@ export interface MessageStatusCallback {
|
|||||||
onError?: (messageId: string, error: string) => void;
|
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 IncomingMessageCallback = (message: OpchanMessage) => void;
|
||||||
|
export type MissingMessageCallback = (event: MissingMessageEvent) => void;
|
||||||
|
|
||||||
export class ReliableMessaging {
|
export class ReliableMessaging {
|
||||||
private channel: ReliableChannel<IDecodedMessage> | null = null;
|
private channel: ReliableChannel<IDecodedMessage> | null = null;
|
||||||
private messageCallbacks: Map<string, MessageStatusCallback> = new Map();
|
private messageCallbacks: Map<string, MessageStatusCallback> = new Map();
|
||||||
private incomingMessageCallbacks: Set<IncomingMessageCallback> = new Set();
|
private incomingMessageCallbacks: Set<IncomingMessageCallback> = new Set();
|
||||||
|
private missingMessageCallbacks: Set<MissingMessageCallback> = new Set();
|
||||||
private codecManager: CodecManager;
|
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) {
|
constructor(node: LightNode) {
|
||||||
this.codecManager = new CodecManager(node);
|
this.codecManager = new CodecManager(node);
|
||||||
@ -53,7 +68,25 @@ export class ReliableMessaging {
|
|||||||
channel.addEventListener(ReliableChannelEvent.InMessageReceived, event => {
|
channel.addEventListener(ReliableChannelEvent.InMessageReceived, event => {
|
||||||
try {
|
try {
|
||||||
const wakuMessage = event.detail;
|
const wakuMessage = event.detail;
|
||||||
|
console.log('Received incoming message:', wakuMessage);
|
||||||
if (wakuMessage.payload) {
|
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(
|
const opchanMessage = this.codecManager.decodeMessage(
|
||||||
wakuMessage.payload
|
wakuMessage.payload
|
||||||
);
|
);
|
||||||
@ -93,6 +126,78 @@ export class ReliableMessaging {
|
|||||||
this.messageCallbacks.delete(messageId);
|
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(
|
public async sendMessage(
|
||||||
@ -123,9 +228,34 @@ export class ReliableMessaging {
|
|||||||
return () => this.incomingMessageCallbacks.delete(callback);
|
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 {
|
public cleanup(): void {
|
||||||
this.messageCallbacks.clear();
|
this.messageCallbacks.clear();
|
||||||
this.incomingMessageCallbacks.clear();
|
this.incomingMessageCallbacks.clear();
|
||||||
|
this.missingMessageCallbacks.clear();
|
||||||
|
this.seenMessages.clear();
|
||||||
|
this.missingMessages.clear();
|
||||||
|
this.recoveredMessages.clear();
|
||||||
this.channel = null;
|
this.channel = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,10 +4,13 @@ import { WakuNodeManager, HealthChangeCallback } from './core/WakuNodeManager';
|
|||||||
import {
|
import {
|
||||||
MessageService,
|
MessageService,
|
||||||
MessageStatusCallback,
|
MessageStatusCallback,
|
||||||
|
MissingMessageCallback,
|
||||||
|
MissingMessageEvent,
|
||||||
|
MissingMessageInfo,
|
||||||
} from './services/MessageService';
|
} from './services/MessageService';
|
||||||
import { ReliableMessaging } from './core/ReliableMessaging';
|
import { ReliableMessaging } from './core/ReliableMessaging';
|
||||||
|
|
||||||
export type { HealthChangeCallback, MessageStatusCallback };
|
export type { HealthChangeCallback, MessageStatusCallback, MissingMessageCallback, MissingMessageEvent, MissingMessageInfo };
|
||||||
|
|
||||||
class MessageManager {
|
class MessageManager {
|
||||||
private nodeManager: WakuNodeManager | null = null;
|
private nodeManager: WakuNodeManager | null = null;
|
||||||
@ -114,6 +117,41 @@ class MessageManager {
|
|||||||
return this.messageService.onMessageReceived(callback);
|
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() {
|
public get messageCache() {
|
||||||
if (!this.messageService) {
|
if (!this.messageService) {
|
||||||
throw new Error('MessageManager not fully initialized');
|
throw new Error('MessageManager not fully initialized');
|
||||||
|
|||||||
@ -2,15 +2,19 @@ import { OpchanMessage } from '@/types/forum';
|
|||||||
import {
|
import {
|
||||||
ReliableMessaging,
|
ReliableMessaging,
|
||||||
MessageStatusCallback,
|
MessageStatusCallback,
|
||||||
|
MissingMessageEvent,
|
||||||
|
MissingMessageInfo,
|
||||||
} from '../core/ReliableMessaging';
|
} from '../core/ReliableMessaging';
|
||||||
import { WakuNodeManager } from '../core/WakuNodeManager';
|
import { WakuNodeManager } from '../core/WakuNodeManager';
|
||||||
import { localDatabase } from '@/lib/database/LocalDatabase';
|
import { localDatabase } from '@/lib/database/LocalDatabase';
|
||||||
|
|
||||||
export type MessageReceivedCallback = (message: OpchanMessage) => void;
|
export type MessageReceivedCallback = (message: OpchanMessage) => void;
|
||||||
export type { MessageStatusCallback };
|
export type MissingMessageCallback = (event: MissingMessageEvent) => void;
|
||||||
|
export type { MessageStatusCallback, MissingMessageEvent, MissingMessageInfo };
|
||||||
|
|
||||||
export class MessageService {
|
export class MessageService {
|
||||||
private messageReceivedCallbacks: Set<MessageReceivedCallback> = new Set();
|
private messageReceivedCallbacks: Set<MessageReceivedCallback> = new Set();
|
||||||
|
private missingMessageCallbacks: Set<MissingMessageCallback> = new Set();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private reliableMessaging: ReliableMessaging | null,
|
private reliableMessaging: ReliableMessaging | null,
|
||||||
@ -29,6 +33,12 @@ export class MessageService {
|
|||||||
localDatabase.setSyncing(false);
|
localDatabase.setSyncing(false);
|
||||||
if (isNew) this.messageReceivedCallbacks.forEach(cb => cb(message));
|
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);
|
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(
|
public updateReliableMessaging(
|
||||||
reliableMessaging: ReliableMessaging | null
|
reliableMessaging: ReliableMessaging | null
|
||||||
): void {
|
): void {
|
||||||
@ -99,6 +130,7 @@ export class MessageService {
|
|||||||
|
|
||||||
public cleanup(): void {
|
public cleanup(): void {
|
||||||
this.messageReceivedCallbacks.clear();
|
this.messageReceivedCallbacks.clear();
|
||||||
|
this.missingMessageCallbacks.clear();
|
||||||
this.reliableMessaging?.cleanup();
|
this.reliableMessaging?.cleanup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
385
src/pages/DebugPage.tsx
Normal file
385
src/pages/DebugPage.tsx
Normal 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;
|
||||||
Loading…
x
Reference in New Issue
Block a user