mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +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 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>
|
||||
|
||||
@ -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
|
||||
|
||||
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
|
||||
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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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
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