From bcf5a842200ef599290f4a48a2563b1cd4ab199a Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 12 Mar 2026 18:50:18 -0400 Subject: [PATCH 01/15] feat: add internal debug logging system - Add comprehensive debug logger utility with circular buffer (1000 entries max) - Add debug log state management with Jotai atoms - Create DebugLogViewer UI component with real-time updates and filtering - Integrate debug logger into Developer Tools settings - Add logging for sync state changes (initMatrix, slidingSync) - Add logging for authentication (login, logout, session management) - Add logging for push notifications (permissions, subscriptions) - Add logging for app lifecycle (visibility changes, backgrounding) - Add logging for network connectivity (online/offline) - Add logging for notifications (filtering, routing, mute decisions) - Add logging for messages (send success/failure, uploads, scheduled) - Add logging for calls (widget lifecycle, join/hangup, events) - Support filtering by category and level - Support exporting logs as JSON (all or filtered) - Support copying logs to clipboard --- src/app/features/room/RoomInput.tsx | 35 +- .../developer-tools/DebugLogViewer.tsx | 566 ++++++++++++++++++ .../settings/developer-tools/DevelopTools.tsx | 6 + .../notifications/PushNotifications.tsx | 17 +- src/app/hooks/useAppVisibility.ts | 4 + src/app/pages/auth/login/loginUtil.ts | 9 + .../pages/client/BackgroundNotifications.tsx | 40 ++ src/app/plugins/call/CallEmbed.ts | 9 + src/app/plugins/call/CallWidgetDriver.ts | 17 +- src/app/state/debugLogger.ts | 50 ++ src/app/utils/debugLogger.ts | 169 ++++++ src/client/initMatrix.ts | 56 +- src/client/slidingSync.ts | 17 + 13 files changed, 968 insertions(+), 27 deletions(-) create mode 100644 src/app/features/settings/developer-tools/DebugLogViewer.tsx create mode 100644 src/app/state/debugLogger.ts create mode 100644 src/app/utils/debugLogger.ts diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 6cbf52de1..8627bb8e4 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -128,6 +128,7 @@ import { useImagePackRooms } from '$hooks/useImagePackRooms'; import { useComposingCheck } from '$hooks/useComposingCheck'; import { useSableCosmetics } from '$hooks/useSableCosmetics'; import { createLogger } from '$utils/debug'; +import { createDebugLogger } from '$utils/debugLogger'; import FocusTrap from 'focus-trap-react'; import { useQueryClient } from '@tanstack/react-query'; import { @@ -176,6 +177,7 @@ const getReplyContent = (replyDraft: IReplyDraft | undefined): IEventRelation => }; const log = createLogger('RoomInput'); +const debugLog = createDebugLogger('RoomInput'); interface ReplyEventContent { 'm.relates_to'?: IEventRelation; } @@ -422,10 +424,16 @@ export const RoomInput = forwardRef( await Promise.all( contents.map((content) => - mx.sendMessage(roomId, content as any).catch((error: unknown) => { - log.error('failed to send uploaded message', { roomId }, error); - throw error; - }) + mx.sendMessage(roomId, content as any) + .then((res) => { + debugLog.info('message', 'Uploaded file message sent', { roomId, eventId: res.event_id, msgtype: content.msgtype }); + return res; + }) + .catch((error: unknown) => { + debugLog.error('message', 'Failed to send uploaded file message', { roomId, error: error instanceof Error ? error.message : String(error) }); + log.error('failed to send uploaded message', { roomId }, error); + throw error; + }) ) ); }; @@ -569,18 +577,27 @@ export const RoomInput = forwardRef( } else if (editingScheduledDelayId) { try { await cancelDelayedEvent(mx, editingScheduledDelayId); - mx.sendMessage(roomId, content as any); + debugLog.info('message', 'Sending message after cancelling scheduled event', { roomId, scheduledDelayId: editingScheduledDelayId }); + const res = await mx.sendMessage(roomId, content as any); + debugLog.info('message', 'Message sent successfully', { roomId, eventId: res.event_id }); invalidate(); setEditingScheduledDelayId(null); resetInput(); - } catch { + } catch (error) { + debugLog.error('message', 'Failed to send message after cancelling scheduled event', { roomId, error: error instanceof Error ? error.message : String(error) }); // Cancel failed — leave state intact for retry } } else { resetInput(); - mx.sendMessage(roomId, content as any).catch((error: unknown) => { - log.error('failed to send message', { roomId }, error); - }); + debugLog.info('message', 'Sending message', { roomId, msgtype: (content as any).msgtype }); + mx.sendMessage(roomId, content as any) + .then((res) => { + debugLog.info('message', 'Message sent successfully', { roomId, eventId: res.event_id }); + }) + .catch((error: unknown) => { + debugLog.error('message', 'Failed to send message', { roomId, error: error instanceof Error ? error.message : String(error) }); + log.error('failed to send message', { roomId }, error); + }); } }, [ editor, diff --git a/src/app/features/settings/developer-tools/DebugLogViewer.tsx b/src/app/features/settings/developer-tools/DebugLogViewer.tsx new file mode 100644 index 000000000..1c9ae8365 --- /dev/null +++ b/src/app/features/settings/developer-tools/DebugLogViewer.tsx @@ -0,0 +1,566 @@ +import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useAtom, useAtomValue, useSetAtom } from 'jotai'; +import { Box, Text, Button, color, config, Badge, Menu, MenuItem, PopOut } from 'folds'; +import { SequenceCard } from '$components/sequence-card'; + +import { + debugLoggerEnabledAtom, + debugLogsAtom, + clearDebugLogsAtom, +} from '$state/debugLogger'; +import { LogEntry, getDebugLogger, LogLevel, LogCategory } from '$utils/debugLogger'; +import { SequenceCardStyle } from '$features/settings/styles.css'; + +const formatTimestamp = (timestamp: number): string => { + const date = new Date(timestamp); + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3, + }); +}; + +const getLevelColor = (level: string): string => { + switch (level) { + case 'error': + return color.Critical.Main; + case 'warn': + return color.Warning.Main; + case 'info': + return color.Success.Main; + default: + return color.Secondary.Main; + } +}; + +const getCategoryBadgeVariant = ( + category: string +): 'Primary' | 'Secondary' | 'Success' | 'Warning' | 'Critical' => { + switch (category) { + case 'error': + return 'Critical'; + case 'sync': + return 'Primary'; + case 'notification': + return 'Success'; + case 'message': + return 'Secondary'; + case 'call': + return 'Warning'; + default: + return 'Secondary'; + } +}; + +function LogEntryItem({ entry }: { entry: LogEntry }) { + const [expanded, setExpanded] = useState(false); + + return ( + + + + + {entry.level.toUpperCase()} + + + {entry.category} + + + {formatTimestamp(entry.timestamp)} + + + [{entry.namespace}] + + + {entry.data && ( + + )} + + + {entry.message} + + {expanded && entry.data && ( + + + {JSON.stringify(entry.data, null, 2)} + + + )} + + ); +} + +export function DebugLogViewer() { + const [enabled, setEnabled] = useAtom(debugLoggerEnabledAtom); + const logs = useAtomValue(debugLogsAtom); + const clearLogs = useSetAtom(clearDebugLogsAtom); + const [autoScroll, setAutoScroll] = useState(true); + const scrollRef = useState(null)[0]; + const [filterLevel, setFilterLevel] = useState('all'); + const [filterCategory, setFilterCategory] = useState('all'); + + // Filter logs based on current filters + const filteredLogs = useMemo(() => { + if (filterLevel === 'all' && filterCategory === 'all') { + return logs; + } + + const debugLogger = getDebugLogger(); + return debugLogger.getFilteredLogs({ + level: filterLevel !== 'all' ? filterLevel : undefined, + category: filterCategory !== 'all' ? filterCategory : undefined, + }); + }, [logs, filterLevel, filterCategory]); + + // Auto-refresh logs when new entries arrive + useEffect(() => { + if (!enabled) return undefined; + + const debugLogger = getDebugLogger(); + const unsubscribe = debugLogger.addListener(() => { + // Trigger re-render by refreshing the atom + // This will be handled by the debugLogsAtom's refresh mechanism + }); + + return unsubscribe; + }, [enabled]); + + // Auto-scroll to bottom when new logs arrive + useEffect(() => { + if (autoScroll && scrollRef) { + scrollRef.scrollTop = scrollRef.scrollHeight; + } + }, [filteredLogs, autoScroll, scrollRef]); + + const handleExportLogs = useCallback( + (filtered: boolean) => { + const debugLogger = getDebugLogger(); + let jsonData: string; + + if (filtered && (filterLevel !== 'all' || filterCategory !== 'all')) { + // Export filtered logs + const logsToExport = debugLogger.getFilteredLogs({ + level: filterLevel !== 'all' ? filterLevel : undefined, + category: filterCategory !== 'all' ? filterCategory : undefined, + }); + jsonData = JSON.stringify( + { + exportedAt: new Date().toISOString(), + filters: { + level: filterLevel !== 'all' ? filterLevel : 'none', + category: filterCategory !== 'all' ? filterCategory : 'none', + }, + logsCount: logsToExport.length, + logs: logsToExport.map((log) => ({ + ...log, + timestamp: new Date(log.timestamp).toISOString(), + })), + }, + null, + 2 + ); + } else { + // Export all logs + jsonData = debugLogger.exportLogs(); + } + + const blob = new Blob([jsonData], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + const filterSuffix = filtered && (filterLevel !== 'all' || filterCategory !== 'all') + ? `-${filterCategory !== 'all' ? filterCategory : 'all'}-${filterLevel !== 'all' ? filterLevel : 'all'}` + : ''; + a.download = `sable-debug-logs${filterSuffix}-${new Date().toISOString()}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, + [filterLevel, filterCategory] + ); + + const handleCopyToClipboard = useCallback( + (filtered: boolean) => { + const debugLogger = getDebugLogger(); + let jsonData: string; + + if (filtered && (filterLevel !== 'all' || filterCategory !== 'all')) { + const logsToExport = debugLogger.getFilteredLogs({ + level: filterLevel !== 'all' ? filterLevel : undefined, + category: filterCategory !== 'all' ? filterCategory : undefined, + }); + jsonData = JSON.stringify( + { + exportedAt: new Date().toISOString(), + filters: { + level: filterLevel !== 'all' ? filterLevel : 'none', + category: filterCategory !== 'all' ? filterCategory : 'none', + }, + logsCount: logsToExport.length, + logs: logsToExport.map((log) => ({ + ...log, + timestamp: new Date(log.timestamp).toISOString(), + })), + }, + null, + 2 + ); + } else { + jsonData = debugLogger.exportLogs(); + } + + navigator.clipboard.writeText(jsonData); + }, + [filterLevel, filterCategory] + ); + + return ( + + + + Debug Log Status + + {enabled ? 'Active' : 'Inactive'} + + + + + Internal debug logging captures sync state, errors, notifications, messages, and call + events. Logs are stored in memory (max 1000 entries) and cleared when you close the app. + + + {/* Filter Controls */} + + + Filters: + + + setFilterCategory('all')} + disabled={filterCategory === 'all'} + > + All Categories + + setFilterCategory('sync')} + disabled={filterCategory === 'sync'} + > + Sync + + setFilterCategory('network')} + disabled={filterCategory === 'network'} + > + Network + + setFilterCategory('notification')} + disabled={filterCategory === 'notification'} + > + Notification + + setFilterCategory('message')} + disabled={filterCategory === 'message'} + > + Message + + setFilterCategory('call')} + disabled={filterCategory === 'call'} + > + Call + + setFilterCategory('general')} + disabled={filterCategory === 'general'} + > + General + + setFilterCategory('error')} + disabled={filterCategory === 'error'} + > + Error + + + } + > + {(targetRef) => ( + + )} + + + + setFilterLevel('all')} + disabled={filterLevel === 'all'} + > + All Levels + + setFilterLevel('debug')} + disabled={filterLevel === 'debug'} + > + Debug + + setFilterLevel('info')} + disabled={filterLevel === 'info'} + > + Info + + setFilterLevel('warn')} + disabled={filterLevel === 'warn'} + > + Warning + + setFilterLevel('error')} + disabled={filterLevel === 'error'} + > + Error + + + } + > + {(targetRef) => ( + + )} + + + {(filterLevel !== 'all' || filterCategory !== 'all') && ( + + )} + + + + + + + + + + + {enabled && ( + + + + Recent Logs ( + {filterLevel !== 'all' || filterCategory !== 'all' + ? `${filteredLogs.length}/${logs.length}` + : `${logs.length}/1000`} + ) + + + + + + {filteredLogs.length === 0 ? ( + + + {logs.length === 0 + ? 'No logs captured yet. Use the app to generate log entries.' + : 'No logs match the current filters.'} + + + ) : ( + filteredLogs.map((log) => ( + + )) + )} + + + )} + + + ); +} diff --git a/src/app/features/settings/developer-tools/DevelopTools.tsx b/src/app/features/settings/developer-tools/DevelopTools.tsx index d65c97db0..d230620ae 100644 --- a/src/app/features/settings/developer-tools/DevelopTools.tsx +++ b/src/app/features/settings/developer-tools/DevelopTools.tsx @@ -11,6 +11,7 @@ import { copyToClipboard } from '$utils/dom'; import { SequenceCardStyle } from '$features/settings/styles.css'; import { AccountData } from './AccountData'; import { SyncDiagnostics } from './SyncDiagnostics'; +import { DebugLogViewer } from './DebugLogViewer'; type DeveloperToolsProps = { requestClose: () => void; @@ -120,6 +121,11 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) { onSelect={setAccountDataType} /> )} + {developerTools && ( + + + + )} diff --git a/src/app/features/settings/notifications/PushNotifications.tsx b/src/app/features/settings/notifications/PushNotifications.tsx index 08310d28d..c01cba410 100644 --- a/src/app/features/settings/notifications/PushNotifications.tsx +++ b/src/app/features/settings/notifications/PushNotifications.tsx @@ -1,5 +1,8 @@ import { MatrixClient } from '$types/matrix-sdk'; import { ClientConfig } from '../../../hooks/useClientConfig'; +import { createDebugLogger } from '$utils/debugLogger'; + +const debugLog = createDebugLogger('PushNotifications'); type PushSubscriptionState = [ PushSubscriptionJSON | null, @@ -8,12 +11,16 @@ type PushSubscriptionState = [ export async function requestBrowserNotificationPermission(): Promise { if (!('Notification' in window)) { + debugLog.warn('notification', 'Notification API not available in this browser'); return 'denied'; } try { + debugLog.info('notification', 'Requesting browser notification permission'); const permission: NotificationPermission = await Notification.requestPermission(); + debugLog.info('notification', 'Notification permission result', { permission }); return permission; - } catch { + } catch (error) { + debugLog.error('notification', 'Failed to request notification permission', { error: error instanceof Error ? error.message : String(error) }); return 'denied'; } } @@ -24,8 +31,10 @@ export async function enablePushNotifications( pushSubscriptionAtom: PushSubscriptionState ): Promise { if (!('serviceWorker' in navigator) || !('PushManager' in window)) { + debugLog.error('notification', 'Push messaging not supported - missing serviceWorker or PushManager'); throw new Error('Push messaging is not supported in this browser.'); } + debugLog.info('notification', 'Enabling push notifications'); const [pushSubAtom, setPushSubscription] = pushSubscriptionAtom; const registration = await navigator.serviceWorker.ready; const currentBrowserSub = await registration.pushManager.getSubscription(); @@ -34,6 +43,7 @@ export async function enablePushNotifications( only when necessary. This prevents us from needing an external call to get back the web push info. */ if (currentBrowserSub && pushSubAtom && currentBrowserSub.endpoint === pushSubAtom.endpoint) { + debugLog.info('notification', 'Push subscription already exists and is valid - reusing', { endpoint: pushSubAtom.endpoint }); const { keys } = pushSubAtom; if (!keys?.p256dh || !keys.auth) return; const pusherData = { @@ -63,19 +73,23 @@ export async function enablePushNotifications( } if (currentBrowserSub) { + debugLog.info('notification', 'Unsubscribing old push subscription'); await currentBrowserSub.unsubscribe(); } + debugLog.info('notification', 'Creating new push subscription'); const newSubscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: clientConfig.pushNotificationDetails?.vapidPublicKey, }); + debugLog.info('notification', 'Push subscription created successfully', { endpoint: newSubscription.endpoint }); setPushSubscription(newSubscription); const subJson = newSubscription.toJSON(); const { keys } = subJson; if (!keys?.p256dh || !keys.auth) { + debugLog.error('notification', 'Push subscription missing required keys'); throw new Error('Push subscription keys missing.'); } const pusherData = { @@ -113,6 +127,7 @@ export async function disablePushNotifications( clientConfig: ClientConfig, pushSubscriptionAtom: PushSubscriptionState ): Promise { + debugLog.info('notification', 'Disabling push notifications'); const [pushSubAtom] = pushSubscriptionAtom; const pusherData = { diff --git a/src/app/hooks/useAppVisibility.ts b/src/app/hooks/useAppVisibility.ts index 0688f2637..2d60d2f03 100644 --- a/src/app/hooks/useAppVisibility.ts +++ b/src/app/hooks/useAppVisibility.ts @@ -8,6 +8,9 @@ import { useSetting } from '../state/hooks/settings'; import { settingsAtom } from '../state/settings'; import { pushSubscriptionAtom } from '../state/pushSubscription'; import { mobileOrTablet } from '../utils/user-agent'; +import { createDebugLogger } from '../utils/debugLogger'; + +const debugLog = createDebugLogger('AppVisibility'); export function useAppVisibility(mx: MatrixClient | undefined) { const clientConfig = useClientConfig(); @@ -18,6 +21,7 @@ export function useAppVisibility(mx: MatrixClient | undefined) { useEffect(() => { const handleVisibilityChange = () => { const isVisible = document.visibilityState === 'visible'; + debugLog.info('general', `App visibility changed: ${isVisible ? 'visible (foreground)' : 'hidden (background)'}`, { visibilityState: document.visibilityState }); appEvents.onVisibilityChange?.(isVisible); if (!isVisible) { appEvents.onVisibilityHidden?.(); diff --git a/src/app/pages/auth/login/loginUtil.ts b/src/app/pages/auth/login/loginUtil.ts index 1659e5474..f9f4ec465 100644 --- a/src/app/pages/auth/login/loginUtil.ts +++ b/src/app/pages/auth/login/loginUtil.ts @@ -11,10 +11,12 @@ import { import { getHomePath } from '$pages/pathUtils'; import { activeSessionIdAtom, sessionsAtom } from '$state/sessions'; import { createLogger } from '$utils/debug'; +import { createDebugLogger } from '$utils/debugLogger'; import { ErrorCode } from '../../../cs-errorcode'; import { autoDiscovery, specVersions } from '../../../cs-api'; const log = createLogger('loginUtil'); +const debugLog = createDebugLogger('loginUtil'); export enum GetBaseUrlError { NotAllow = 'NotAllow', @@ -75,35 +77,42 @@ export const login = async ( } const mx = createClient({ baseUrl: url }); + debugLog.info('general', 'Attempting login', { baseUrl: url, loginType: data.type }); const [err, res] = await to(mx.loginRequest(data)); if (err) { if (err.httpStatus === 400) { + debugLog.error('general', 'Login failed - invalid request', { httpStatus: 400 }); throw new MatrixError({ errcode: LoginError.InvalidRequest, }); } if (err.httpStatus === 429) { + debugLog.error('general', 'Login failed - rate limited', { httpStatus: 429 }); throw new MatrixError({ errcode: LoginError.RateLimited, }); } if (err.errcode === ErrorCode.M_USER_DEACTIVATED) { + debugLog.error('general', 'Login failed - user deactivated', { errcode: err.errcode }); throw new MatrixError({ errcode: LoginError.UserDeactivated, }); } if (err.httpStatus === 403) { + debugLog.error('general', 'Login failed - forbidden', { httpStatus: 403 }); throw new MatrixError({ errcode: LoginError.Forbidden, }); } + debugLog.error('general', 'Login failed - unknown error', { error: err.message, httpStatus: err.httpStatus }); throw new MatrixError({ errcode: LoginError.Unknown, }); } + debugLog.info('general', 'Login successful', { userId: res.user_id, deviceId: res.device_id }); return { baseUrl: url, response: res, diff --git a/src/app/pages/client/BackgroundNotifications.tsx b/src/app/pages/client/BackgroundNotifications.tsx index 9ca53b5cb..ffb7da763 100644 --- a/src/app/pages/client/BackgroundNotifications.tsx +++ b/src/app/pages/client/BackgroundNotifications.tsx @@ -32,6 +32,7 @@ import { } from '$utils/room'; import { NotificationType, StateEvent } from '$types/matrix/room'; import { createLogger } from '$utils/debug'; +import { createDebugLogger } from '$utils/debugLogger'; import LogoSVG from '$public/res/svg/cinny.svg'; import { nicknamesAtom } from '$state/nicknames'; import { @@ -43,6 +44,7 @@ import { useClientConfig } from '$hooks/useClientConfig'; import { mobileOrTablet } from '$utils/user-agent'; const log = createLogger('BackgroundNotifications'); +const debugLog = createDebugLogger('BackgroundNotifications'); const isClientReadyForNotifications = (state: SyncState | string | null): boolean => state === SyncState.Prepared || state === SyncState.Syncing || state === SyncState.Catchup; @@ -303,6 +305,10 @@ export function BackgroundNotifications() { const notificationType = getNotificationType(mx, room.roomId); if (notificationType === NotificationType.Mute) { + debugLog.debug('notification', 'Room is muted - skipping notification', { + roomId: room.roomId, + eventId, + }); return; } @@ -329,9 +335,24 @@ export function BackgroundNotifications() { const shouldNotify = pushActions?.notify || shouldForceDMNotification; if (!shouldNotify) { + debugLog.debug('notification', 'Event filtered - no push action match', { + eventId, + roomId: room.roomId, + eventType, + isDM, + }); return; } + debugLog.info('notification', 'Processing notification event', { + eventId, + roomId: room.roomId, + eventType, + isDM, + isHighlight, + loud: loudByRule, + }); + const senderName = getMemberDisplayName(room, sender, nicknamesRef.current) ?? getMxIdLocalPart(sender) ?? @@ -360,6 +381,10 @@ export function BackgroundNotifications() { // Silent-rule events: unread badge updated above; no OS notification or sound. if (!loudByRule && !isHighlight) { + debugLog.debug('notification', 'Silent notification - badge updated only', { + eventId, + roomId: room.roomId, + }); return; } @@ -410,6 +435,11 @@ export function BackgroundNotifications() { if (canShowInAppBanner) { // App is in the foreground on a different account — show the themed in-app banner. + debugLog.info('notification', 'Showing in-app banner', { + eventId, + roomId: room.roomId, + title: notificationPayload.title, + }); setInAppBannerRef.current({ id: dedupeId, title: notificationPayload.title, @@ -422,6 +452,12 @@ export function BackgroundNotifications() { } else if (loudByRule) { // App is backgrounded or in-app notifications disabled — fire an OS notification. // Only send for loud (sound-tweak) rules; highlight-only events are silently counted. + debugLog.info('notification', 'Sending OS notification', { + eventId, + roomId: room.roomId, + title: notificationPayload.title, + hasSound: !notificationPayload.options.silent, + }); sendNotification({ title: notificationPayload.title, icon: notificationPayload.options.icon, @@ -438,6 +474,10 @@ export function BackgroundNotifications() { }) .catch((err) => { log.error('failed to start background client for', session.userId, err); + debugLog.error('notification', 'Failed to start background client', { + userId: session.userId, + error: err, + }); }); }); diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts index c370bfac5..8aaf6acd1 100644 --- a/src/app/plugins/call/CallEmbed.ts +++ b/src/app/plugins/call/CallEmbed.ts @@ -25,6 +25,9 @@ import { } from './types'; import { CallControl } from './CallControl'; import { CallControlState } from './CallControlState'; +import { createDebugLogger } from '../../utils/debugLogger'; + +const debugLog = createDebugLogger('CallEmbed'); export class CallEmbed { private mx: MatrixClient; @@ -127,6 +130,8 @@ export class CallEmbed { container: HTMLElement, initialControlState?: CallControlState ) { + debugLog.info('call', 'Initializing call embed', { roomId: room.roomId }); + const iframe = CallEmbed.getIframe( widget.getCompleteUrl({ currentUserId: mx.getSafeUserId() }) ); @@ -174,6 +179,7 @@ export class CallEmbed { } public hangup() { + debugLog.info('call', 'Hanging up call', { roomId: this.roomId }); return this.call.transport.send(ElementWidgetActions.HangupCall, {}); } @@ -194,6 +200,7 @@ export class CallEmbed { } private start() { + debugLog.info('call', 'Starting call widget', { roomId: this.roomId }); // Room widgets get locked to the room they were added in this.call.setViewedRoomId(this.roomId); this.disposables.push( @@ -224,6 +231,7 @@ export class CallEmbed { * @param opts */ public dispose(): void { + debugLog.info('call', 'Disposing call widget', { roomId: this.roomId }); this.disposables.forEach((disposable) => { disposable(); }); @@ -242,6 +250,7 @@ export class CallEmbed { } private onCallJoined(): void { + debugLog.info('call', 'Call joined', { roomId: this.roomId }); this.joined = true; this.applyStyles(); this.control.startObserving(); diff --git a/src/app/plugins/call/CallWidgetDriver.ts b/src/app/plugins/call/CallWidgetDriver.ts index a2cbc8ddc..27a2c4e02 100644 --- a/src/app/plugins/call/CallWidgetDriver.ts +++ b/src/app/plugins/call/CallWidgetDriver.ts @@ -26,6 +26,9 @@ import { } from 'matrix-js-sdk'; import { getCallCapabilities } from './utils'; import { downloadMedia, mxcUrlToHttp } from '../../utils/matrix'; +import { createDebugLogger } from '../../utils/debugLogger'; + +const debugLog = createDebugLogger('CallWidgetDriver'); export class CallWidgetDriver extends WidgetDriver { private allowedCapabilities: Set; @@ -40,8 +43,12 @@ export class CallWidgetDriver extends WidgetDriver { this.mx = mx; const deviceId = mx.getDeviceId(); - if (!deviceId) throw new Error('Failed to initialize CallWidgetDriver! Device ID not found.'); + if (!deviceId) { + debugLog.error('call', 'Failed to initialize CallWidgetDriver - no device ID'); + throw new Error('Failed to initialize CallWidgetDriver! Device ID not found.'); + } + debugLog.info('call', 'Initializing CallWidgetDriver', { roomId: inRoomId, deviceId }); this.allowedCapabilities = getCallCapabilities(inRoomId, mx.getSafeUserId(), deviceId); } @@ -59,7 +66,12 @@ export class CallWidgetDriver extends WidgetDriver { const client = this.mx; const roomId = targetRoomId || this.inRoomId; - if (!client || !roomId) throw new Error('Not in a room or not attached to a client'); + if (!client || !roomId) { + debugLog.error('call', 'Cannot send event - no client or room', { eventType, roomId }); + throw new Error('Not in a room or not attached to a client'); + } + + debugLog.info('call', 'Sending call event', { eventType, roomId, hasStateKey: stateKey !== null }); let r: { event_id: string } | null; if (typeof stateKey === 'string') { @@ -80,6 +92,7 @@ export class CallWidgetDriver extends WidgetDriver { ); } + debugLog.info('call', 'Call event sent successfully', { eventId: r.event_id, eventType }); return { roomId, eventId: r.event_id }; } diff --git a/src/app/state/debugLogger.ts b/src/app/state/debugLogger.ts new file mode 100644 index 000000000..9c155fdd7 --- /dev/null +++ b/src/app/state/debugLogger.ts @@ -0,0 +1,50 @@ +/** + * Jotai atoms for debug logger state management + */ +import { atom } from 'jotai'; +import { atomWithRefresh } from 'jotai/utils'; +import { getDebugLogger, LogEntry } from '$utils/debugLogger'; + +const debugLogger = getDebugLogger(); + +/** + * Atom for enabling/disabling debug logging + */ +export const debugLoggerEnabledAtom = atom( + () => debugLogger.isEnabled(), + (_, set, enabled: boolean) => { + debugLogger.setEnabled(enabled); + set(debugLogsAtom); + } +); + +/** + * Atom for retrieving debug logs with refresh capability + */ +export const debugLogsAtom = atomWithRefresh(() => debugLogger.getLogs()); + +/** + * Atom for filtered logs + */ +export const filteredDebugLogsAtom = atom( + (get) => get(debugLogsAtom), + (get, set, filters?: { level?: string; category?: string; since?: number }) => { + const allLogs = get(debugLogsAtom); + return allLogs; // Can be extended with filtering logic + } +); + +/** + * Action to clear all debug logs + */ +export const clearDebugLogsAtom = atom(null, (_, set) => { + debugLogger.clear(); + set(debugLogsAtom); +}); + +/** + * Action to export debug logs + */ +export const exportDebugLogsAtom = atom(null, () => { + return debugLogger.exportLogs(); +}); diff --git a/src/app/utils/debugLogger.ts b/src/app/utils/debugLogger.ts new file mode 100644 index 000000000..bef5fd416 --- /dev/null +++ b/src/app/utils/debugLogger.ts @@ -0,0 +1,169 @@ +/** + * Enhanced debug logger for Sable with circular buffer storage and categorization. + * + * Enable via Developer Tools UI or with: + * localStorage.setItem('sable_internal_debug', '1'); location.reload(); + */ + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +export type LogCategory = + | 'sync' + | 'network' + | 'notification' + | 'message' + | 'call' + | 'error' + | 'general'; + +export interface LogEntry { + timestamp: number; + level: LogLevel; + category: LogCategory; + namespace: string; + message: string; + data?: unknown; +} + +type LogListener = (entry: LogEntry) => void; + +class DebugLoggerService { + private logs: LogEntry[] = []; + private maxLogs = 1000; // Circular buffer size + private enabled = false; + private listeners: Set = new Set(); + + constructor() { + // Check if debug logging is enabled from localStorage + this.enabled = localStorage.getItem('sable_internal_debug') === '1'; + } + + public isEnabled(): boolean { + return this.enabled; + } + + public setEnabled(enabled: boolean): void { + this.enabled = enabled; + if (enabled) { + localStorage.setItem('sable_internal_debug', '1'); + } else { + localStorage.removeItem('sable_internal_debug'); + } + } + + public addListener(listener: LogListener): () => void { + this.listeners.add(listener); + return () => this.listeners.delete(listener); + } + + private notifyListeners(entry: LogEntry): void { + this.listeners.forEach((listener) => { + try { + listener(entry); + } catch (error) { + // Silently catch listener errors to prevent debug logging from breaking the app + console.error('[DebugLogger] Listener error:', error); + } + }); + } + + public log( + level: LogLevel, + category: LogCategory, + namespace: string, + message: string, + data?: unknown + ): void { + if (!this.enabled && level !== 'error') return; + + const entry: LogEntry = { + timestamp: Date.now(), + level, + category, + namespace, + message, + data, + }; + + // Add to circular buffer + if (this.logs.length >= this.maxLogs) { + this.logs.shift(); // Remove oldest entry + } + this.logs.push(entry); + + // Notify listeners + this.notifyListeners(entry); + + // Also log to console for developer convenience + const prefix = `[sable:${category}:${namespace}]`; + const consoleLevel = level === 'debug' ? 'log' : level; + // eslint-disable-next-line no-console + console[consoleLevel](prefix, message, data !== undefined ? data : ''); + } + + public getLogs(): LogEntry[] { + return [...this.logs]; + } + + public getFilteredLogs( + filters?: { + level?: LogLevel; + category?: LogCategory; + since?: number; + } + ): LogEntry[] { + let filtered = [...this.logs]; + + if (filters?.level) { + filtered = filtered.filter((log) => log.level === filters.level); + } + + if (filters?.category) { + filtered = filtered.filter((log) => log.category === filters.category); + } + + if (filters?.since) { + filtered = filtered.filter((log) => log.timestamp >= filters.since); + } + + return filtered; + } + + public clear(): void { + this.logs = []; + } + + public exportLogs(): string { + return JSON.stringify( + { + exportedAt: new Date().toISOString(), + logsCount: this.logs.length, + logs: this.logs.map((log) => ({ + ...log, + timestamp: new Date(log.timestamp).toISOString(), + })), + }, + null, + 2 + ); + } +} + +// Singleton instance +const debugLoggerService = new DebugLoggerService(); + +export const getDebugLogger = (): DebugLoggerService => debugLoggerService; + +/** + * Creates a logger for a specific namespace + */ +export const createDebugLogger = (namespace: string) => ({ + debug: (category: LogCategory, message: string, data?: unknown) => + debugLoggerService.log('debug', category, namespace, message, data), + info: (category: LogCategory, message: string, data?: unknown) => + debugLoggerService.log('info', category, namespace, message, data), + warn: (category: LogCategory, message: string, data?: unknown) => + debugLoggerService.log('warn', category, namespace, message, data), + error: (category: LogCategory, message: string, data?: unknown) => + debugLoggerService.log('error', category, namespace, message, data), +}); diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 47c8ba29d..76d6a6d96 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -16,11 +16,13 @@ import { } from '$state/sessions'; import { getLocalStorageItem } from '$state/utils/atomWithLocalStorage'; import { createLogger } from '$utils/debug'; +import { createDebugLogger } from '$utils/debugLogger'; import { pushSessionToSW } from '../sw-session'; import { cryptoCallbacks } from './secretStorageKeys'; import { SlidingSyncConfig, SlidingSyncDiagnostics, SlidingSyncManager } from './slidingSync'; const log = createLogger('initMatrix'); +const debugLog = createDebugLogger('initMatrix'); const slidingSyncByClient = new WeakMap(); const FAST_SYNC_POLL_TIMEOUT_MS = 10000; const SLIDING_SYNC_POLL_TIMEOUT_MS = 20000; @@ -150,6 +152,7 @@ const waitForClientReady = (mx: MatrixClient, timeoutMs: number): Promise let timer = 0; let finish = () => {}; const onSync = (state: string) => { + debugLog.info('sync', `Sync state changed: ${state}`, { state, ready: isClientReadyForUi(state) }); if (isClientReadyForUi(state)) finish(); }; @@ -263,7 +266,7 @@ const buildClient = async (session: Session): Promise => { export const initClient = async (session: Session): Promise => { const storeName = getSessionStoreName(session); - log.log('initClient', { userId: session.userId, baseUrl: session.baseUrl, storeName }); + debugLog.info('sync', 'Initializing Matrix client', { userId: session.userId, baseUrl: session.baseUrl }); const isMismatch = (err: unknown): boolean => { const msg = err instanceof Error ? err.message : String(err); @@ -276,6 +279,8 @@ export const initClient = async (session: Session): Promise => { }; const wipeAllStores = async () => { + log.warn('initClient: wiping all stores for', session.userId); + debugLog.warn('sync', 'Wiping all stores due to mismatch', { userId: session.userId } log.warn('initClient: wiping all stores for', session.userId); await deleteSessionStores(storeName); try { @@ -294,17 +299,25 @@ export const initClient = async (session: Session): Promise => { }; let mx: MatrixClient; - try { - mx = await buildClient(session); - } catch (err) { - if (!isMismatch(err)) throw err; + try {{ + debugLog.error('sync', 'Failed to build client', { error: err }); + throw err; + } log.warn('initClient: mismatch on buildClient — wiping and retrying:', err); + debugLog.warn('sync', 'Client build mismatch - wiping stores and retrying', { error: err }); await wipeAllStores(); mx = await buildClient(session); } try { await mx.initRustCrypto({ cryptoDatabasePrefix: storeName.rustCryptoPrefix }); + } catch (err) { + if (!isMismatch(err)) { + debugLog.error('sync', 'Failed to initialize crypto', { error: err }); + throw err; + } + log.warn('initClient: mismatch on initRustCrypto — wiping and retrying:', err); + debugLog.warn('sync', 'Crypto init mismatch - wiping stores and retrying', { error: err }; } catch (err) { if (!isMismatch(err)) throw err; log.warn('initClient: mismatch on initRustCrypto — wiping and retrying:', err); @@ -336,16 +349,7 @@ const disposeSlidingSync = (mx: MatrixClient): void => { slidingSyncByClient.delete(mx); }; -export const stopClient = (mx: MatrixClient): void => { - disposeSlidingSync(mx); - mx.stopClient(); -}; - -export const getSlidingSyncManager = (mx: MatrixClient): SlidingSyncManager | undefined => - slidingSyncByClient.get(mx); - -export const startClient = async (mx: MatrixClient, config?: StartClientConfig) => { - log.log('startClient', mx.getUserId()); +expebugLog.info('sync', 'Starting Matrix client', { userId: mx.getUserId() }); disposeSlidingSync(mx); const slidingConfig = config?.slidingSync; const slidingEnabledOnServer = resolveSlidingEnabled(slidingConfig?.enabled); @@ -361,6 +365,25 @@ export const startClient = async (mx: MatrixClient, config?: StartClientConfig) proxyBaseUrl, hasSlidingProxy, }); + debugLog.info('sync', 'Sliding sync configuration', { + enabledOnServer: slidingEnabledOnServer, + requested: slidingRequested, + hasProxy: hasSlidingProxy, + }); + + const startClassicSync = async (fallbackFromSliding: boolean, reason: SyncTransportReason) => { + debugLog.info('sync', `Starting classic sync (reason: ${reason})`, { + fallback: fallbackFromSliding, + reason, + }); + userId: mx.getUserId(), + enabled: slidingConfig?.enabled, + enabledOnServer: slidingEnabledOnServer, + sessionOptIn: config?.sessionSlidingSyncOptIn === true, + requestedEnabled: slidingRequested, + proxyBaseUrl, + hasSlidingProxy, + }); const startClassicSync = async (fallbackFromSliding: boolean, reason: SyncTransportReason) => { syncTransportByClient.set(mx, { @@ -504,10 +527,12 @@ export const getClientSyncDiagnostics = (mx: MatrixClient): ClientSyncDiagnostic */ export const logoutClient = async (mx: MatrixClient, session?: Session) => { log.log('logoutClient', { userId: mx.getUserId(), sessionUserId: session?.userId }); + debugLog.info('general', 'Logging out client', { userId: mx.getUserId() }); pushSessionToSW(); stopClient(mx); try { await mx.logout(); + debugLog.info('general', 'Logout successful', { userId: mx.getUserId() }); } catch { // ignore } @@ -525,6 +550,7 @@ export const logoutClient = async (mx: MatrixClient, session?: Session) => { }; export const clearLoginData = async () => { + debugLog.info('general', 'Clearing all login data and reloading'); const dbs = await window.indexedDB.databases(); dbs.forEach((idbInfo) => { const { name } = idbInfo; diff --git a/src/client/slidingSync.ts b/src/client/slidingSync.ts index eacb7a472..613c0bbd7 100644 --- a/src/client/slidingSync.ts +++ b/src/client/slidingSync.ts @@ -14,10 +14,18 @@ import { MSC3575_STATE_KEY_ME, EventType, User, +import { + MatrixClient, + SlidingSync, + SlidingSyncEvent, + SlidingSyncEventHandlerMap, + SlidingSyncState, } from '$types/matrix-sdk'; import { createLogger } from '$utils/debug'; +import { createDebugLogger } from '$utils/debugLogger'; const log = createLogger('slidingSync'); +const debugLog = createDebugLogger('slidingSync'); export const LIST_JOINED = 'joined'; export const LIST_INVITES = 'invites'; @@ -359,17 +367,26 @@ export class SlidingSyncManager { ); this.onLifecycle = (state, resp, err) => { + debugLog.info('sync', `Sliding sync lifecycle: ${state}`, { state, hasError: !!err }); + if (err) { + debugLog.error('sync', 'Sliding sync error', { error: err }); + } if (this.disposed || err || !resp || state !== SlidingSyncState.Complete) return; this.expandListsToKnownCount(); }; this.onConnectionChange = () => { + const isOnline = navigator.onLine; + debugLog.info('network', `Network connectivity changed: ${isOnline ? 'online' : 'offline'}`, { online: isOnline }); if (this.disposed || !this.adaptiveTimeline) return; const nextLimit = resolveAdaptiveRoomTimelineLimit( this.configuredTimelineLimit, readAdaptiveSignals() ); if (nextLimit === this.roomTimelineLimit) return; + debugLog.info('sync', `Adaptive timeline limit updated to ${nextLimit}`, { + limit: nextLimit, + }); this.roomTimelineLimit = nextLimit; this.applyRoomTimelineLimit(nextLimit); log.log( From 4624617bbf74e090c26ef8ad1f6bda62ae920f90 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 12 Mar 2026 18:59:55 -0400 Subject: [PATCH 02/15] feat: add UI and timeline debug logging - Add 'ui' and 'timeline' log categories to debugLogger - Log Room component mount/unmount and drawer state changes - Log RoomTimeline lifecycle events (mount, unmount, initialization) - Track timeline pagination (start, complete, errors) - Monitor live timeline linking state changes - Log scroll position changes (at bottom, scrolled up) - Track jump-to-event operations - Log timeline refresh events - Add UI and Timeline category filters in DebugLogViewer - Update description to mention new categories This provides comprehensive visibility into UI component lifecycle and timeline visualization state for debugging issues with room rendering, timeline scrolling, and UI element visibility. --- src/app/features/room/Room.tsx | 27 +++++++- src/app/features/room/RoomTimeline.tsx | 66 ++++++++++++++++++- .../developer-tools/DebugLogViewer.tsx | 21 +++++- src/app/utils/debugLogger.ts | 2 + 4 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx index b7aef9107..b8ff5cc54 100644 --- a/src/app/features/room/Room.tsx +++ b/src/app/features/room/Room.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import { Box, Line } from 'folds'; import { useParams } from 'react-router-dom'; import { isKeyHotkey } from 'is-hotkey'; @@ -19,16 +19,36 @@ import { RoomViewHeader } from './RoomViewHeader'; import { MembersDrawer } from './MembersDrawer'; import { RoomView } from './RoomView'; import { CallChatView } from './CallChatView'; +import { createDebugLogger } from '$utils/debugLogger'; + +const debugLog = createDebugLogger('Room'); export function Room() { const { eventId } = useParams(); const room = useRoom(); const mx = useMatrixClient(); + // Log room mount + useEffect(() => { + debugLog.info('ui', 'Room component mounted', { roomId: room.roomId, eventId }); + return () => { + debugLog.info('ui', 'Room component unmounted', { roomId: room.roomId }); + }; + }, [room.roomId, eventId]); + const [isDrawer] = useSetting(settingsAtom, 'isPeopleDrawer'); const [isWidgetDrawerOpen] = useSetting(settingsAtom, 'isWidgetDrawer'); const [hideReads] = useSetting(settingsAtom, 'hideReads'); const screenSize = useScreenSizeContext(); + + // Log drawer state changes + useEffect(() => { + debugLog.debug('ui', 'Members drawer state changed', { roomId: room.roomId, isOpen: isDrawer }); + }, [isDrawer, room.roomId]); + + useEffect(() => { + debugLog.debug('ui', 'Widgets drawer state changed', { roomId: room.roomId, isOpen: isWidgetDrawerOpen }); + }, [isWidgetDrawerOpen, room.roomId]); const powerLevels = usePowerLevels(room); const members = useRoomMembers(mx, room.roomId); const chat = useAtomValue(callChatAtom); @@ -47,6 +67,11 @@ export function Room() { const callView = room.isCallRoom(); + // Log call view state + useEffect(() => { + debugLog.debug('ui', 'Room view mode', { roomId: room.roomId, callView, chatOpen: chat }); + }, [callView, chat, room.roomId]); + return ( diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index 39fc9d1cc..02c62feca 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -131,9 +131,12 @@ import { useRoomPermissions } from '$hooks/useRoomPermissions'; import { useGetMemberPowerTag } from '$hooks/useMemberPowerTag'; import { profilesCacheAtom } from '$state/userRoomProfile'; import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze'; +import { createDebugLogger } from '$utils/debugLogger'; import * as css from './RoomTimeline.css'; import { EncryptedContent, Event, ForwardedMessageProps, Message, Reactions } from './message'; +const debugLog = createDebugLogger('RoomTimeline'); + const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( { + debugLog.info('timeline', 'Timeline mounted', { + roomId: room.roomId, + eventId, + initialEventsCount: eventsLength, + liveTimelineLinked, + }); + return () => { + debugLog.info('timeline', 'Timeline unmounted', { roomId: room.roomId }); + }; + }, [room.roomId, eventId]); // Only log on mount/unmount + + // Log live timeline linking state changes + useEffect(() => { + debugLog.debug('timeline', 'Live timeline link state changed', { + roomId: room.roomId, + liveTimelineLinked, + eventsLength, + }); + }, [liveTimelineLinked, room.roomId, eventsLength]); const canPaginateBack = typeof timeline.linkedTimelines[0]?.getPaginationToken(Direction.Backward) === 'string'; const rangeAtStart = timeline.range.start === 0; @@ -739,6 +777,13 @@ export function RoomTimeline({ if (!alive()) return; const evLength = getTimelinesEventsCount(lTimelines); + debugLog.info('timeline', 'Loading event timeline', { + roomId: room.roomId, + eventId: evtId, + totalEvents: evLength, + focusIndex: evtAbsIndex, + }); + setAtBottom(false); setFocusItem({ index: evtAbsIndex, @@ -757,6 +802,7 @@ export function RoomTimeline({ ), useCallback(() => { if (!alive()) return; + debugLog.info('timeline', 'Resetting timeline to initial state', { roomId: room.roomId }); setTimeline(getInitialTimeline(room)); scrollToBottomRef.current.count += 1; scrollToBottomRef.current.smooth = false; @@ -830,6 +876,12 @@ export function RoomTimeline({ highlight = true, onScroll: ((scrolled: boolean) => void) | undefined = undefined ) => { + debugLog.info('timeline', 'Jumping to event', { + roomId: room.roomId, + eventId: evtId, + highlight, + }); + const evtTimeline = getEventTimeline(room, evtId); const absoluteIndex = evtTimeline && getEventIdAbsoluteIndex(timeline.linkedTimelines, evtTimeline, evtId); @@ -848,7 +900,16 @@ export function RoomTimeline({ scrollTo: !scrolled, highlight, }); + debugLog.debug('timeline', 'Event found in current timeline', { + roomId: room.roomId, + eventId: evtId, + index: absoluteIndex, + }); } else { + debugLog.debug('timeline', 'Event not in current timeline, loading timeline', { + roomId: room.roomId, + eventId: evtId, + }); loadEventTimeline(evtId); } }, @@ -880,6 +941,7 @@ export function RoomTimeline({ // "Jump to Latest" button to stick permanently. Forcing atBottom here is // correct: TimelineRefresh always reinits to the live end, so the user // should be repositioned to the bottom regardless. + debugLog.info('timeline', 'Live timeline refresh triggered', { roomId: room.roomId }); setTimeline(getInitialTimeline(room)); setAtBottom(true); scrollToBottomRef.current.count += 1; @@ -969,16 +1031,18 @@ export function RoomTimeline({ if (targetEntry.isIntersecting) { // User has reached the bottom + debugLog.debug('timeline', 'Scrolled to bottom', { roomId: room.roomId }); setAtBottom(true); if (atLiveEndRef.current && document.hasFocus()) { tryAutoMarkAsRead(); } } else { // User has intentionally scrolled up. + debugLog.debug('timeline', 'Scrolled away from bottom', { roomId: room.roomId }); setAtBottom(false); } }, - [tryAutoMarkAsRead, setAtBottom] + [tryAutoMarkAsRead, setAtBottom, room.roomId] ), useCallback( () => ({ diff --git a/src/app/features/settings/developer-tools/DebugLogViewer.tsx b/src/app/features/settings/developer-tools/DebugLogViewer.tsx index 1c9ae8365..47aa83d0d 100644 --- a/src/app/features/settings/developer-tools/DebugLogViewer.tsx +++ b/src/app/features/settings/developer-tools/DebugLogViewer.tsx @@ -270,8 +270,9 @@ export function DebugLogViewer() { - Internal debug logging captures sync state, errors, notifications, messages, and call - events. Logs are stored in memory (max 1000 entries) and cleared when you close the app. + Internal debug logging captures sync state, network events, notifications, messages, + calls, UI component lifecycle, and timeline operations. Logs are stored in memory (max + 1000 entries) and cleared when you close the app. {/* Filter Controls */} @@ -334,6 +335,22 @@ export function DebugLogViewer() { > Call + setFilterCategory('ui')} + disabled={filterCategory === 'ui'} + > + UI + + setFilterCategory('timeline')} + disabled={filterCategory === 'timeline'} + > + Timeline + Date: Thu, 12 Mar 2026 19:04:51 -0400 Subject: [PATCH 03/15] fix build issue --- src/client/initMatrix.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 76d6a6d96..60c7a6bc8 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -280,8 +280,7 @@ export const initClient = async (session: Session): Promise => { const wipeAllStores = async () => { log.warn('initClient: wiping all stores for', session.userId); - debugLog.warn('sync', 'Wiping all stores due to mismatch', { userId: session.userId } - log.warn('initClient: wiping all stores for', session.userId); + debugLog.warn('sync', 'Wiping all stores due to mismatch', { userId: session.userId }); await deleteSessionStores(storeName); try { const allDbs = await window.indexedDB.databases(); From 44029b6a5c370bbc0faefa31fca14993b0759571 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 12 Mar 2026 19:27:14 -0400 Subject: [PATCH 04/15] fix(debug-logger): Fix PopOut component pattern and state management - Change debugLoggerEnabledAtom from getter function to direct value atom to properly update UI - Convert PopOut components from children function to anchor-based pattern - Add click handlers for category and level filter menus - Fix TypeScript errors with entry.data unknown type checks - Filters should now display correctly when clicked --- ...e-debug-logs-2026-03-12T23_17_41.374Z.json | 2510 +++++++++++++++++ .../developer-tools/DebugLogViewer.tsx | 143 +- src/app/state/debugLogger.ts | 5 +- src/client/initMatrix.ts | 43 +- src/client/slidingSync.ts | 5 - 5 files changed, 2634 insertions(+), 72 deletions(-) create mode 100644 sable-debug-logs-2026-03-12T23_17_41.374Z.json diff --git a/sable-debug-logs-2026-03-12T23_17_41.374Z.json b/sable-debug-logs-2026-03-12T23_17_41.374Z.json new file mode 100644 index 000000000..7ab98c59a --- /dev/null +++ b/sable-debug-logs-2026-03-12T23_17_41.374Z.json @@ -0,0 +1,2510 @@ +{ + "exportedAt": "2026-03-12T23:17:41.373Z", + "logsCount": 228, + "logs": [ + { + "timestamp": "2026-03-12T23:16:10.029Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:10.037Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:10.329Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:10.336Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:12.062Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:12.072Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:14.564Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:14.573Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:15.081Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:15.089Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:16.057Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:16.067Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:20.839Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:20.848Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:26.378Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:26.386Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:27.979Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:27.987Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:28.648Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:28.659Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:33.629Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:33.639Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:34.132Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:34.142Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:36.870Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:36.877Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:37.925Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:37.933Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:38.192Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:38.201Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:39.241Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:39.248Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:40.395Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:40.412Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:40.661Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:40.669Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:40.906Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:40.914Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:41.542Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:41.552Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:42.881Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:42.890Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:43.701Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:43.709Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.002Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.013Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.258Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.270Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.521Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.529Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.819Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:44.829Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.186Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.198Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.453Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.462Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.701Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.710Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.955Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:46.963Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:47.296Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:47.304Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:47.983Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:47.991Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:48.241Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:48.249Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:48.497Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:48.505Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:51.397Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:51.405Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:53.969Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:53.977Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:54.479Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:54.487Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:56.575Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:16:56.585Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:02.580Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:02.589Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:03.039Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:03.047Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:03.698Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:03.708Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:03.951Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:03.962Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:08.497Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:08.509Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:09.297Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:09.306Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:10.708Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:10.719Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:10.970Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:10.979Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:12.000Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:12.010Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:12.531Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:12.539Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:13.917Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:13.926Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:14.934Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component unmounted", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:14.934Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline unmounted", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:14.938Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline mounted", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", + "initialEventsCount": 1, + "liveTimelineLinked": true + } + }, + { + "timestamp": "2026-03-12T23:17:14.938Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", + "liveTimelineLinked": true, + "eventsLength": 1 + } + }, + { + "timestamp": "2026-03-12T23:17:14.939Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component mounted", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" + } + }, + { + "timestamp": "2026-03-12T23:17:14.939Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Members drawer state changed", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:14.939Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Widgets drawer state changed", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:14.939Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Room view mode", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", + "callView": false, + "chatOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:14.952Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" + } + }, + { + "timestamp": "2026-03-12T23:17:14.952Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 1, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:15.277Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:15.278Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", + "liveTimelineLinked": true, + "eventsLength": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:15.434Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:15.455Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:15.747Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:15.755Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:16.519Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:16.528Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:16.984Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:16.993Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:17.105Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component unmounted", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" + } + }, + { + "timestamp": "2026-03-12T23:17:17.105Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline unmounted", + "data": { + "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" + } + }, + { + "timestamp": "2026-03-12T23:17:17.107Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline mounted", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", + "initialEventsCount": 1, + "liveTimelineLinked": true + } + }, + { + "timestamp": "2026-03-12T23:17:17.107Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", + "liveTimelineLinked": true, + "eventsLength": 1 + } + }, + { + "timestamp": "2026-03-12T23:17:17.108Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component mounted", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:17.108Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Members drawer state changed", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:17.108Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Widgets drawer state changed", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:17.108Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Room view mode", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", + "callView": false, + "chatOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:17.119Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 1, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:17.119Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:17.484Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:17.484Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", + "liveTimelineLinked": true, + "eventsLength": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:17.525Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:17.547Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:18.048Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:18.069Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:18.856Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:18.863Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:19.115Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:19.124Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:19.533Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component unmounted", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:19.533Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline unmounted", + "data": { + "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:19.537Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline mounted", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "initialEventsCount": 63, + "liveTimelineLinked": true + } + }, + { + "timestamp": "2026-03-12T23:17:19.537Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "liveTimelineLinked": true, + "eventsLength": 63 + } + }, + { + "timestamp": "2026-03-12T23:17:19.538Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component mounted", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:19.538Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Members drawer state changed", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:19.538Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Widgets drawer state changed", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:19.538Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Room view mode", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "callView": false, + "chatOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:19.581Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:19.827Z", + "level": "info", + "category": "network", + "namespace": "slidingSync", + "message": "Network connectivity changed: online", + "data": { + "online": true + } + }, + { + "timestamp": "2026-03-12T23:17:19.827Z", + "level": "info", + "category": "network", + "namespace": "slidingSync", + "message": "Network connectivity changed: online", + "data": { + "online": true + } + }, + { + "timestamp": "2026-03-12T23:17:19.918Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:19.935Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:20.682Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:21.290Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 63, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:21.388Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:21.396Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:21.545Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 110 + } + }, + { + "timestamp": "2026-03-12T23:17:21.557Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "liveTimelineLinked": true, + "eventsLength": 110 + } + }, + { + "timestamp": "2026-03-12T23:17:21.588Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 110, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:21.869Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 170 + } + }, + { + "timestamp": "2026-03-12T23:17:21.894Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "liveTimelineLinked": true, + "eventsLength": 170 + } + }, + { + "timestamp": "2026-03-12T23:17:23.450Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 170, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:23.703Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 230 + } + }, + { + "timestamp": "2026-03-12T23:17:23.723Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", + "liveTimelineLinked": true, + "eventsLength": 230 + } + }, + { + "timestamp": "2026-03-12T23:17:23.827Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:23.886Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:23.902Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:23.925Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:24.223Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:24.231Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:25.628Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component unmounted", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:25.628Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline unmounted", + "data": { + "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" + } + }, + { + "timestamp": "2026-03-12T23:17:25.909Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:25.918Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:26.206Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:26.214Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:26.582Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline mounted", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", + "initialEventsCount": 1, + "liveTimelineLinked": true + } + }, + { + "timestamp": "2026-03-12T23:17:26.582Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", + "liveTimelineLinked": true, + "eventsLength": 1 + } + }, + { + "timestamp": "2026-03-12T23:17:26.582Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component mounted", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:26.582Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Members drawer state changed", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:26.582Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Widgets drawer state changed", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:26.582Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Room view mode", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", + "callView": false, + "chatOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:26.589Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 1, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:26.589Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:26.869Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", + "liveTimelineLinked": true, + "eventsLength": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:26.885Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:26.909Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:27.168Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:27.186Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:27.475Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:27.484Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:27.739Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:27.747Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:28.229Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:28.237Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:28.488Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:28.498Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:28.750Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:28.768Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:28.842Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:28.851Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:30.067Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component unmounted", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:30.067Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline unmounted", + "data": { + "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:30.069Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline mounted", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", + "initialEventsCount": 1, + "liveTimelineLinked": true + } + }, + { + "timestamp": "2026-03-12T23:17:30.069Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", + "liveTimelineLinked": true, + "eventsLength": 1 + } + }, + { + "timestamp": "2026-03-12T23:17:30.069Z", + "level": "info", + "category": "ui", + "namespace": "Room", + "message": "Room component mounted", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:30.069Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Members drawer state changed", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:30.069Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Widgets drawer state changed", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", + "isOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:30.069Z", + "level": "debug", + "category": "ui", + "namespace": "Room", + "message": "Room view mode", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", + "callView": false, + "chatOpen": false + } + }, + { + "timestamp": "2026-03-12T23:17:30.077Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:30.077Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 1, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:30.373Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", + "liveTimelineLinked": true, + "eventsLength": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:30.390Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:30.412Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 61 + } + }, + { + "timestamp": "2026-03-12T23:17:30.446Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination started", + "data": { + "direction": "backward", + "eventsLoaded": 61, + "hasToken": true + } + }, + { + "timestamp": "2026-03-12T23:17:30.741Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Live timeline link state changed", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", + "liveTimelineLinked": true, + "eventsLength": 121 + } + }, + { + "timestamp": "2026-03-12T23:17:30.773Z", + "level": "info", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Timeline pagination completed", + "data": { + "direction": "backward", + "totalEventsNow": 121 + } + }, + { + "timestamp": "2026-03-12T23:17:31.437Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:31.457Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:31.711Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:31.733Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:33.795Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:33.803Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:34.635Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled away from bottom", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:35.301Z", + "level": "debug", + "category": "timeline", + "namespace": "RoomTimeline", + "message": "Scrolled to bottom", + "data": { + "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" + } + }, + { + "timestamp": "2026-03-12T23:17:37.172Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:37.180Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:40.343Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: FINISHED", + "data": { + "state": "FINISHED", + "hasError": false + } + }, + { + "timestamp": "2026-03-12T23:17:40.351Z", + "level": "info", + "category": "sync", + "namespace": "slidingSync", + "message": "Sliding sync lifecycle: COMPLETE", + "data": { + "state": "COMPLETE", + "hasError": false + } + } + ] +} \ No newline at end of file diff --git a/src/app/features/settings/developer-tools/DebugLogViewer.tsx b/src/app/features/settings/developer-tools/DebugLogViewer.tsx index 47aa83d0d..a5da49b76 100644 --- a/src/app/features/settings/developer-tools/DebugLogViewer.tsx +++ b/src/app/features/settings/developer-tools/DebugLogViewer.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState, useCallback, useMemo } from 'react'; +import { useEffect, useState, useCallback, useMemo, MouseEventHandler } from 'react'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; -import { Box, Text, Button, color, config, Badge, Menu, MenuItem, PopOut } from 'folds'; +import { Box, Text, Button, color, config, Badge, Menu, MenuItem, PopOut, RectCords } from 'folds'; import { SequenceCard } from '$components/sequence-card'; import { @@ -91,7 +91,7 @@ function LogEntryItem({ entry }: { entry: LogEntry }) { [{entry.namespace}] - {entry.data && ( + {entry.data != null && ( - )} + setFilterLevel('all')} + onClick={() => { + setFilterLevel('all'); + setLevelAnchor(undefined); + }} disabled={filterLevel === 'all'} > All Levels @@ -403,7 +446,10 @@ export function DebugLogViewer() { setFilterLevel('debug')} + onClick={() => { + setFilterLevel('debug'); + setLevelAnchor(undefined); + }} disabled={filterLevel === 'debug'} > Debug @@ -411,7 +457,10 @@ export function DebugLogViewer() { setFilterLevel('info')} + onClick={() => { + setFilterLevel('info'); + setLevelAnchor(undefined); + }} disabled={filterLevel === 'info'} > Info @@ -419,7 +468,10 @@ export function DebugLogViewer() { setFilterLevel('warn')} + onClick={() => { + setFilterLevel('warn'); + setLevelAnchor(undefined); + }} disabled={filterLevel === 'warn'} > Warning @@ -427,7 +479,10 @@ export function DebugLogViewer() { setFilterLevel('error')} + onClick={() => { + setFilterLevel('error'); + setLevelAnchor(undefined); + }} disabled={filterLevel === 'error'} > Error @@ -435,19 +490,17 @@ export function DebugLogViewer() { } > - {(targetRef) => ( - - )} + {(filterLevel !== 'all' || filterCategory !== 'all') && ( diff --git a/src/app/state/debugLogger.ts b/src/app/state/debugLogger.ts index 9c155fdd7..a0d31473b 100644 --- a/src/app/state/debugLogger.ts +++ b/src/app/state/debugLogger.ts @@ -11,9 +11,10 @@ const debugLogger = getDebugLogger(); * Atom for enabling/disabling debug logging */ export const debugLoggerEnabledAtom = atom( - () => debugLogger.isEnabled(), - (_, set, enabled: boolean) => { + debugLogger.isEnabled(), + (get, set, enabled: boolean) => { debugLogger.setEnabled(enabled); + set(debugLoggerEnabledAtom, enabled); set(debugLogsAtom); } ); diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 60c7a6bc8..6b4adbc11 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -298,7 +298,10 @@ export const initClient = async (session: Session): Promise => { }; let mx: MatrixClient; - try {{ + try { + mx = await buildClient(session); + } catch (err) { + if (!isMismatch(err)) { debugLog.error('sync', 'Failed to build client', { error: err }); throw err; } @@ -316,10 +319,7 @@ export const initClient = async (session: Session): Promise => { throw err; } log.warn('initClient: mismatch on initRustCrypto — wiping and retrying:', err); - debugLog.warn('sync', 'Crypto init mismatch - wiping stores and retrying', { error: err }; - } catch (err) { - if (!isMismatch(err)) throw err; - log.warn('initClient: mismatch on initRustCrypto — wiping and retrying:', err); + debugLog.warn('sync', 'Crypto init mismatch - wiping stores and retrying', { error: err }); mx.stopClient(); await wipeAllStores(); mx = await buildClient(session); @@ -348,7 +348,16 @@ const disposeSlidingSync = (mx: MatrixClient): void => { slidingSyncByClient.delete(mx); }; -expebugLog.info('sync', 'Starting Matrix client', { userId: mx.getUserId() }); +export const getSlidingSyncManager = (mx: MatrixClient): SlidingSyncManager | undefined => { + return slidingSyncByClient.get(mx); +}; + + +export const startClient = async ( + mx: MatrixClient, + config?: StartClientConfig +): Promise => { + debugLog.info('sync', 'Starting Matrix client', { userId: mx.getUserId() }); disposeSlidingSync(mx); const slidingConfig = config?.slidingSync; const slidingEnabledOnServer = resolveSlidingEnabled(slidingConfig?.enabled); @@ -370,20 +379,6 @@ expebugLog.info('sync', 'Starting Matrix client', { userId: mx.getUserId() }); hasProxy: hasSlidingProxy, }); - const startClassicSync = async (fallbackFromSliding: boolean, reason: SyncTransportReason) => { - debugLog.info('sync', `Starting classic sync (reason: ${reason})`, { - fallback: fallbackFromSliding, - reason, - }); - userId: mx.getUserId(), - enabled: slidingConfig?.enabled, - enabledOnServer: slidingEnabledOnServer, - sessionOptIn: config?.sessionSlidingSyncOptIn === true, - requestedEnabled: slidingRequested, - proxyBaseUrl, - hasSlidingProxy, - }); - const startClassicSync = async (fallbackFromSliding: boolean, reason: SyncTransportReason) => { syncTransportByClient.set(mx, { transport: 'classic', @@ -494,6 +489,14 @@ expebugLog.info('sync', 'Starting Matrix client', { userId: mx.getUserId() }); } }; +export const stopClient = (mx: MatrixClient): void => { + log.log('stopClient', mx.getUserId()); + debugLog.info('sync', 'Stopping client', { userId: mx.getUserId() }); + disposeSlidingSync(mx); + mx.stopClient(); + syncTransportByClient.delete(mx); +}; + export const clearCacheAndReload = async (mx: MatrixClient) => { log.log('clearCacheAndReload', mx.getUserId()); stopClient(mx); diff --git a/src/client/slidingSync.ts b/src/client/slidingSync.ts index 613c0bbd7..2b1cc548e 100644 --- a/src/client/slidingSync.ts +++ b/src/client/slidingSync.ts @@ -14,12 +14,7 @@ import { MSC3575_STATE_KEY_ME, EventType, User, -import { - MatrixClient, - SlidingSync, - SlidingSyncEvent, SlidingSyncEventHandlerMap, - SlidingSyncState, } from '$types/matrix-sdk'; import { createLogger } from '$utils/debug'; import { createDebugLogger } from '$utils/debugLogger'; From 21fc2baa40f65f06668c7ac8736a800a2d85156b Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 12 Mar 2026 19:27:37 -0400 Subject: [PATCH 05/15] chore: Remove accidentally committed debug log file --- ...e-debug-logs-2026-03-12T23_17_41.374Z.json | 2510 ----------------- 1 file changed, 2510 deletions(-) delete mode 100644 sable-debug-logs-2026-03-12T23_17_41.374Z.json diff --git a/sable-debug-logs-2026-03-12T23_17_41.374Z.json b/sable-debug-logs-2026-03-12T23_17_41.374Z.json deleted file mode 100644 index 7ab98c59a..000000000 --- a/sable-debug-logs-2026-03-12T23_17_41.374Z.json +++ /dev/null @@ -1,2510 +0,0 @@ -{ - "exportedAt": "2026-03-12T23:17:41.373Z", - "logsCount": 228, - "logs": [ - { - "timestamp": "2026-03-12T23:16:10.029Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:10.037Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:10.329Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:10.336Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:12.062Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:12.072Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:14.564Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:14.573Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:15.081Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:15.089Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:16.057Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:16.067Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:20.839Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:20.848Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:26.378Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:26.386Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:27.979Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:27.987Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:28.648Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:28.659Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:33.629Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:33.639Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:34.132Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:34.142Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:36.870Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:36.877Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:37.925Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:37.933Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:38.192Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:38.201Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:39.241Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:39.248Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:40.395Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:40.412Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:40.661Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:40.669Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:40.906Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:40.914Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:41.542Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:41.552Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:42.881Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:42.890Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:43.701Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:43.709Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.002Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.013Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.258Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.270Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.521Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.529Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.819Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:44.829Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.186Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.198Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.453Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.462Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.701Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.710Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.955Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:46.963Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:47.296Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:47.304Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:47.983Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:47.991Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:48.241Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:48.249Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:48.497Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:48.505Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:51.397Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:51.405Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:53.969Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:53.977Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:54.479Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:54.487Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:56.575Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:16:56.585Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:02.580Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:02.589Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:03.039Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:03.047Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:03.698Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:03.708Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:03.951Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:03.962Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:08.497Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:08.509Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:09.297Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:09.306Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:10.708Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:10.719Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:10.970Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:10.979Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:12.000Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:12.010Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:12.531Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:12.539Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:13.917Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:13.926Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:14.934Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component unmounted", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:14.934Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline unmounted", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:14.938Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline mounted", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", - "initialEventsCount": 1, - "liveTimelineLinked": true - } - }, - { - "timestamp": "2026-03-12T23:17:14.938Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", - "liveTimelineLinked": true, - "eventsLength": 1 - } - }, - { - "timestamp": "2026-03-12T23:17:14.939Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component mounted", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" - } - }, - { - "timestamp": "2026-03-12T23:17:14.939Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Members drawer state changed", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:14.939Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Widgets drawer state changed", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:14.939Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Room view mode", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", - "callView": false, - "chatOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:14.952Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" - } - }, - { - "timestamp": "2026-03-12T23:17:14.952Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 1, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:15.277Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:15.278Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ", - "liveTimelineLinked": true, - "eventsLength": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:15.434Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:15.455Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:15.747Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:15.755Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:16.519Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:16.528Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:16.984Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:16.993Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:17.105Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component unmounted", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" - } - }, - { - "timestamp": "2026-03-12T23:17:17.105Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline unmounted", - "data": { - "roomId": "!mJ0liHlwQViTI5Ly10BXwEqsz5jpuMm4n4bNqijUuSQ" - } - }, - { - "timestamp": "2026-03-12T23:17:17.107Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline mounted", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", - "initialEventsCount": 1, - "liveTimelineLinked": true - } - }, - { - "timestamp": "2026-03-12T23:17:17.107Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", - "liveTimelineLinked": true, - "eventsLength": 1 - } - }, - { - "timestamp": "2026-03-12T23:17:17.108Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component mounted", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:17.108Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Members drawer state changed", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:17.108Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Widgets drawer state changed", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:17.108Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Room view mode", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", - "callView": false, - "chatOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:17.119Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 1, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:17.119Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:17.484Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:17.484Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe", - "liveTimelineLinked": true, - "eventsLength": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:17.525Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:17.547Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:18.048Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:18.069Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:18.856Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:18.863Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:19.115Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:19.124Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:19.533Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component unmounted", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:19.533Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline unmounted", - "data": { - "roomId": "!Qm3G5po2ee2D8i8q8K:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:19.537Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline mounted", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "initialEventsCount": 63, - "liveTimelineLinked": true - } - }, - { - "timestamp": "2026-03-12T23:17:19.537Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "liveTimelineLinked": true, - "eventsLength": 63 - } - }, - { - "timestamp": "2026-03-12T23:17:19.538Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component mounted", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:19.538Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Members drawer state changed", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:19.538Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Widgets drawer state changed", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:19.538Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Room view mode", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "callView": false, - "chatOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:19.581Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:19.827Z", - "level": "info", - "category": "network", - "namespace": "slidingSync", - "message": "Network connectivity changed: online", - "data": { - "online": true - } - }, - { - "timestamp": "2026-03-12T23:17:19.827Z", - "level": "info", - "category": "network", - "namespace": "slidingSync", - "message": "Network connectivity changed: online", - "data": { - "online": true - } - }, - { - "timestamp": "2026-03-12T23:17:19.918Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:19.935Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:20.682Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:21.290Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 63, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:21.388Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:21.396Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:21.545Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 110 - } - }, - { - "timestamp": "2026-03-12T23:17:21.557Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "liveTimelineLinked": true, - "eventsLength": 110 - } - }, - { - "timestamp": "2026-03-12T23:17:21.588Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 110, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:21.869Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 170 - } - }, - { - "timestamp": "2026-03-12T23:17:21.894Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "liveTimelineLinked": true, - "eventsLength": 170 - } - }, - { - "timestamp": "2026-03-12T23:17:23.450Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 170, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:23.703Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 230 - } - }, - { - "timestamp": "2026-03-12T23:17:23.723Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe", - "liveTimelineLinked": true, - "eventsLength": 230 - } - }, - { - "timestamp": "2026-03-12T23:17:23.827Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:23.886Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:23.902Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:23.925Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:24.223Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:24.231Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:25.628Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component unmounted", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:25.628Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline unmounted", - "data": { - "roomId": "!a6sXbRuOyyc7MKutmy:sable.moe" - } - }, - { - "timestamp": "2026-03-12T23:17:25.909Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:25.918Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:26.206Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:26.214Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:26.582Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline mounted", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", - "initialEventsCount": 1, - "liveTimelineLinked": true - } - }, - { - "timestamp": "2026-03-12T23:17:26.582Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", - "liveTimelineLinked": true, - "eventsLength": 1 - } - }, - { - "timestamp": "2026-03-12T23:17:26.582Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component mounted", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:26.582Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Members drawer state changed", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:26.582Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Widgets drawer state changed", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:26.582Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Room view mode", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", - "callView": false, - "chatOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:26.589Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 1, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:26.589Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:26.869Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social", - "liveTimelineLinked": true, - "eventsLength": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:26.885Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:26.909Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:27.168Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:27.186Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:27.475Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:27.484Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:27.739Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:27.747Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:28.229Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:28.237Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:28.488Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:28.498Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:28.750Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:28.768Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:28.842Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:28.851Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:30.067Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component unmounted", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:30.067Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline unmounted", - "data": { - "roomId": "!SMDuoqKbydPQVvQnAn:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:30.069Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline mounted", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", - "initialEventsCount": 1, - "liveTimelineLinked": true - } - }, - { - "timestamp": "2026-03-12T23:17:30.069Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", - "liveTimelineLinked": true, - "eventsLength": 1 - } - }, - { - "timestamp": "2026-03-12T23:17:30.069Z", - "level": "info", - "category": "ui", - "namespace": "Room", - "message": "Room component mounted", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:30.069Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Members drawer state changed", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:30.069Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Widgets drawer state changed", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", - "isOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:30.069Z", - "level": "debug", - "category": "ui", - "namespace": "Room", - "message": "Room view mode", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", - "callView": false, - "chatOpen": false - } - }, - { - "timestamp": "2026-03-12T23:17:30.077Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:30.077Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 1, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:30.373Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", - "liveTimelineLinked": true, - "eventsLength": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:30.390Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:30.412Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 61 - } - }, - { - "timestamp": "2026-03-12T23:17:30.446Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination started", - "data": { - "direction": "backward", - "eventsLoaded": 61, - "hasToken": true - } - }, - { - "timestamp": "2026-03-12T23:17:30.741Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Live timeline link state changed", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social", - "liveTimelineLinked": true, - "eventsLength": 121 - } - }, - { - "timestamp": "2026-03-12T23:17:30.773Z", - "level": "info", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Timeline pagination completed", - "data": { - "direction": "backward", - "totalEventsNow": 121 - } - }, - { - "timestamp": "2026-03-12T23:17:31.437Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:31.457Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:31.711Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:31.733Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:33.795Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:33.803Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:34.635Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled away from bottom", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:35.301Z", - "level": "debug", - "category": "timeline", - "namespace": "RoomTimeline", - "message": "Scrolled to bottom", - "data": { - "roomId": "!RdmpmmKddwEWEXRCbh:cloudhub.social" - } - }, - { - "timestamp": "2026-03-12T23:17:37.172Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:37.180Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:40.343Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: FINISHED", - "data": { - "state": "FINISHED", - "hasError": false - } - }, - { - "timestamp": "2026-03-12T23:17:40.351Z", - "level": "info", - "category": "sync", - "namespace": "slidingSync", - "message": "Sliding sync lifecycle: COMPLETE", - "data": { - "state": "COMPLETE", - "hasError": false - } - } - ] -} \ No newline at end of file From c123cfefb20590d87a3e376b180bf73539bac00c Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 12 Mar 2026 19:27:55 -0400 Subject: [PATCH 06/15] fix(debug-logger): Fix TypeScript error with filters.since undefined check --- src/app/utils/debugLogger.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/utils/debugLogger.ts b/src/app/utils/debugLogger.ts index ca1f96429..3fe70eb50 100644 --- a/src/app/utils/debugLogger.ts +++ b/src/app/utils/debugLogger.ts @@ -125,7 +125,8 @@ class DebugLoggerService { } if (filters?.since) { - filtered = filtered.filter((log) => log.timestamp >= filters.since); + const since = filters.since; + filtered = filtered.filter((log) => log.timestamp >= since); } return filtered; From 9c017b85cf7b887b7ed2be118249c8850c437c14 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Thu, 12 Mar 2026 20:05:56 -0400 Subject: [PATCH 07/15] feat(debug-logging): enhance sync logging and fix filter button issue - Add comprehensive sync cycle logging in slidingSync.ts: - Track room count changes per list with deltas - Log initial sync completion with timing and room counts - Monitor list expansion progress with detailed stats - Detect slow sync cycles (>1s) and expansions (>500ms) - Track network connectivity changes with connection info - Log attach/dispose lifecycle with diagnostics - Add error logging for list operations - Fix filter button closing settings dialog in DebugLogViewer: - Add stopPropagation to category/level menu button handlers - Prevents event bubbling that was closing parent dialog - Add .envrc for automatic Node 24 environment activation via direnv - Add .nvmrc to specify Node v24.14.0 requirement --- .envrc | 6 + .../developer-tools/DebugLogViewer.tsx | 2 + src/client/slidingSync.ts | 207 +++++++++++++++++- 3 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 .envrc diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..a26cad735 --- /dev/null +++ b/.envrc @@ -0,0 +1,6 @@ +# Load nvm and use the version from .nvmrc +export NVM_DIR="$HOME/.nvm" +if [ -s "$NVM_DIR/nvm.sh" ]; then + source "$NVM_DIR/nvm.sh" + nvm use +fi diff --git a/src/app/features/settings/developer-tools/DebugLogViewer.tsx b/src/app/features/settings/developer-tools/DebugLogViewer.tsx index a5da49b76..ae29edb24 100644 --- a/src/app/features/settings/developer-tools/DebugLogViewer.tsx +++ b/src/app/features/settings/developer-tools/DebugLogViewer.tsx @@ -146,10 +146,12 @@ export function DebugLogViewer() { const [levelAnchor, setLevelAnchor] = useState(); const handleOpenCategoryMenu: MouseEventHandler = (evt) => { + evt.stopPropagation(); setCategoryAnchor(evt.currentTarget.getBoundingClientRect()); }; const handleOpenLevelMenu: MouseEventHandler = (evt) => { + evt.stopPropagation(); setLevelAnchor(evt.currentTarget.getBoundingClientRect()); }; diff --git a/src/client/slidingSync.ts b/src/client/slidingSync.ts index 2b1cc548e..e1da5045e 100644 --- a/src/client/slidingSync.ts +++ b/src/client/slidingSync.ts @@ -319,6 +319,12 @@ export class SlidingSyncManager { private listsFullyLoaded = false; + private initialSyncCompleted = false; + + private syncCount = 0; + + private previousListCounts: Map = new Map(); + public readonly slidingSync: SlidingSync; public readonly probeTimeoutMs: number; @@ -362,17 +368,114 @@ export class SlidingSyncManager { ); this.onLifecycle = (state, resp, err) => { - debugLog.info('sync', `Sliding sync lifecycle: ${state}`, { state, hasError: !!err }); + const syncStartTime = performance.now(); + this.syncCount++; + + debugLog.info('sync', `Sliding sync lifecycle: ${state} (cycle #${this.syncCount})`, { + state, + hasError: !!err, + syncNumber: this.syncCount, + isInitialSync: !this.initialSyncCompleted + }); + if (err) { - debugLog.error('sync', 'Sliding sync error', { error: err }); + debugLog.error('sync', 'Sliding sync error', { + error: err, + errorMessage: err.message, + syncNumber: this.syncCount, + state + }); + } + + if (this.disposed) { + debugLog.warn('sync', 'Sync lifecycle called after disposal', { state }); + return; + } + + if (err || !resp || state !== SlidingSyncState.Complete) return; + + // Track what changed in this sync cycle + const changes: Record = {}; + let totalRoomCount = 0; + let hasChanges = false; + + this.listKeys.forEach((key) => { + const listData = this.slidingSync.getListData(key); + const currentCount = listData?.joinedCount ?? 0; + const previousCount = this.previousListCounts.get(key) ?? 0; + + totalRoomCount += currentCount; + + if (currentCount !== previousCount) { + changes[key] = { + previous: previousCount, + current: currentCount, + delta: currentCount - previousCount + }; + this.previousListCounts.set(key, currentCount); + hasChanges = true; + } + }); + + if (hasChanges || !this.initialSyncCompleted) { + debugLog.info('sync', 'Room counts changed in sync cycle', { + syncNumber: this.syncCount, + changes, + totalRoomCount, + isInitialSync: !this.initialSyncCompleted + }); + } + + // Mark initial sync as complete after first successful cycle + if (!this.initialSyncCompleted) { + this.initialSyncCompleted = true; + debugLog.info('sync', 'Initial sync completed', { + syncNumber: this.syncCount, + totalRoomCount, + listCounts: Object.fromEntries( + this.listKeys.map(key => [ + key, + this.slidingSync.getListData(key)?.joinedCount ?? 0 + ]) + ), + timeElapsed: `${(performance.now() - syncStartTime).toFixed(2)}ms` + }); } - if (this.disposed || err || !resp || state !== SlidingSyncState.Complete) return; + this.expandListsToKnownCount(); + + const syncDuration = performance.now() - syncStartTime; + if (syncDuration > 1000) { + debugLog.warn('sync', 'Slow sync cycle detected', { + syncNumber: this.syncCount, + duration: `${syncDuration.toFixed(2)}ms`, + totalRoomCount + }); + } }; this.onConnectionChange = () => { const isOnline = navigator.onLine; - debugLog.info('network', `Network connectivity changed: ${isOnline ? 'online' : 'offline'}`, { online: isOnline }); + const connectionInfo = (typeof navigator !== 'undefined' ? (navigator as any).connection : undefined); + const effectiveType = connectionInfo?.effectiveType; + const downlink = connectionInfo?.downlink; + + debugLog.info('network', `Network connectivity changed: ${isOnline ? 'online' : 'offline'}`, { + online: isOnline, + effectiveType, + downlink: downlink ? `${downlink} Mbps` : undefined + }); + + if (!isOnline) { + debugLog.warn('network', 'Device went offline - sync paused', { + syncNumber: this.syncCount, + }); + } else { + debugLog.info('network', 'Device back online - sync will resume', { + syncNumber: this.syncCount, + }); + } + if (this.disposed || !this.adaptiveTimeline) return; const nextLimit = resolveAdaptiveRoomTimelineLimit( this.configuredTimelineLimit, @@ -381,6 +484,8 @@ export class SlidingSyncManager { if (nextLimit === this.roomTimelineLimit) return; debugLog.info('sync', `Adaptive timeline limit updated to ${nextLimit}`, { limit: nextLimit, + previousLimit: this.roomTimelineLimit, + reason: 'connection change' }); this.roomTimelineLimit = nextLimit; this.applyRoomTimelineLimit(nextLimit); @@ -391,6 +496,15 @@ export class SlidingSyncManager { } public attach(): void { + debugLog.info('sync', 'Attaching sliding sync listeners', { + proxyBaseUrl: this.proxyBaseUrl, + listPageSize: this.listPageSize, + roomTimelineLimit: this.roomTimelineLimit, + adaptiveTimeline: this.adaptiveTimeline, + maxRooms: this.maxRooms, + lists: this.listKeys + }); + this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle); const connection = ( typeof navigator !== 'undefined' ? (navigator as any).connection : undefined @@ -407,10 +521,21 @@ export class SlidingSyncManager { window.addEventListener('online', this.onConnectionChange); window.addEventListener('offline', this.onConnectionChange); } + + debugLog.info('sync', 'Sliding sync listeners attached successfully', { + hasConnectionAPI: !!connection, + hasWindowEvents: typeof window !== 'undefined' + }); } public dispose(): void { if (this.disposed) return; + + debugLog.info('sync', 'Disposing sliding sync', { + syncCount: this.syncCount, + initialSyncCompleted: this.initialSyncCompleted + }); + this.disposed = true; this.slidingSync.removeListener(SlidingSyncEvent.Lifecycle, this.onLifecycle); const connection = ( @@ -428,6 +553,10 @@ export class SlidingSyncManager { window.removeEventListener('online', this.onConnectionChange); window.removeEventListener('offline', this.onConnectionChange); } + + debugLog.info('sync', 'Sliding sync disposed successfully', { + totalSyncCycles: this.syncCount + }); } private applyRoomTimelineLimit(timelineLimit: number): void { @@ -467,10 +596,16 @@ export class SlidingSyncManager { let allListsComplete = true; let expandedAny = false; + const expansionStartTime = performance.now(); + const expansionDetails: Record = {}; + this.listKeys.forEach((key) => { const listData = this.slidingSync.getListData(key); const knownCount = listData?.joinedCount ?? 0; - if (knownCount <= 0) return; + if (knownCount <= 0) { + expansionDetails[key] = { status: 'empty', knownCount: 0 }; + return; + } const existing = this.slidingSync.getListParams(key); const currentEnd = getListEndIndex(existing); @@ -480,6 +615,7 @@ export class SlidingSyncManager { if (currentEnd >= maxEnd) { // This list is fully loaded + expansionDetails[key] = { status: 'complete', knownCount, currentEnd }; return; } @@ -492,16 +628,51 @@ export class SlidingSyncManager { const chunkSize = 100; const desiredEnd = Math.min(currentEnd + chunkSize, maxEnd); + if (desiredEnd === currentEnd) { + expansionDetails[key] = { + status: 'complete', + knownCount, + currentEnd, + desiredEnd + }; + return; + } + this.slidingSync.setListRanges(key, [[0, desiredEnd]]); expandedAny = true; - + + expansionDetails[key] = { + status: 'expanding', + knownCount, + previousEnd: currentEnd, + newEnd: desiredEnd, + roomsToLoad: desiredEnd - currentEnd + }; + + debugLog.info('sync', `Expanding list "${key}" to full range`, { + list: key, + knownCount, + previousEnd: currentEnd, + newEnd: desiredEnd, + roomsToLoad: desiredEnd - currentEnd + }); + if (knownCount > this.maxRooms) { log.warn( `Sliding Sync list "${key}" capped at ${this.maxRooms}/${knownCount} rooms for ${this.mx.getUserId()}` ); + debugLog.warn('sync', `List "${key}" exceeds maxRooms limit`, { + list: key, + knownCount, + maxRooms: this.maxRooms, + cappedCount: this.maxRooms + }); } }); + const expansionDuration = performance.now() - expansionStartTime; + const hasExpansions = Object.values(expansionDetails).some(d => d.status === 'expanding'); + // Mark as fully loaded once all lists are complete if (allListsComplete) { this.listsFullyLoaded = true; @@ -509,6 +680,23 @@ export class SlidingSyncManager { } else if (expandedAny) { log.log(`Sliding Sync lists expanding... for ${this.mx.getUserId()}`); } + + if (hasExpansions) { + debugLog.info('sync', 'List expansion completed', { + syncNumber: this.syncCount, + lists: expansionDetails, + timeElapsed: `${expansionDuration.toFixed(2)}ms` + }); + } + + if (expansionDuration > 500) { + debugLog.warn('sync', 'Slow list expansion detected', { + duration: `${expansionDuration.toFixed(2)}ms`, + expandedLists: Object.keys(expansionDetails).filter( + key => expansionDetails[key].status === 'expanding' + ) + }); + } } /** @@ -542,8 +730,13 @@ export class SlidingSyncManager { } else { this.slidingSync.setList(listKey, list); } - } catch { + } catch (error) { // ignore — the list will be re-sent on the next sync cycle + debugLog.warn('sync', `Failed to update list "${listKey}"`, { + list: listKey, + error: error instanceof Error ? error.message : String(error), + updateType: updateArgs.ranges && Object.keys(updateArgs).length === 1 ? 'ranges' : 'full' + }); } return this.slidingSync.getListParams(listKey) ?? list; } From 83262b359398049a6c1eb64ce498e59b76d24ecb Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 13 Mar 2026 11:08:58 -0400 Subject: [PATCH 08/15] feat: enhance debug logging and fix background notifications Debug logging: - Add logging to LeaveRoomPrompt, CreateRoom, Space, AccountSwitcherTab - Add build version to debug log exports - Add network/sync logging to initMatrix Background notifications: - Add explicit listener cleanup before stopping clients - Add exponential backoff retry for failed background clients (5 attempts) - Add 30s timeout to waitForSync to prevent indefinite hangs - Use useMemo for inactive sessions array - Track cleanup callbacks per client --- .../leave-room-prompt/LeaveRoomPrompt.tsx | 7 +- src/app/features/create-room/CreateRoom.tsx | 19 +++++ .../developer-tools/DebugLogViewer.tsx | 2 + .../pages/client/BackgroundNotifications.tsx | 73 +++++++++++++++++-- .../client/sidebar/AccountSwitcherTab.tsx | 5 ++ src/app/pages/client/space/Space.tsx | 4 + src/app/utils/debugLogger.ts | 1 + src/client/initMatrix.ts | 19 ++++- 8 files changed, 119 insertions(+), 11 deletions(-) diff --git a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx index 21b108e6d..36cdd89de 100644 --- a/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx +++ b/src/app/components/leave-room-prompt/LeaveRoomPrompt.tsx @@ -20,6 +20,9 @@ import { MatrixError } from '$types/matrix-sdk'; import { useMatrixClient } from '$hooks/useMatrixClient'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { stopPropagation } from '$utils/keyboard'; +import { createDebugLogger } from '$utils/debugLogger'; + +const debugLog = createDebugLogger('LeaveRoomPrompt'); type LeaveRoomPromptProps = { roomId: string; @@ -31,6 +34,7 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro const [leaveState, leaveRoom] = useAsyncCallback( useCallback(async () => { + debugLog.info('ui', 'Leave room button clicked', { roomId }); mx.leave(roomId); }, [mx, roomId]) ); @@ -41,9 +45,10 @@ export function LeaveRoomPrompt({ roomId, onDone, onCancel }: LeaveRoomPromptPro useEffect(() => { if (leaveState.status === AsyncStatus.Success) { + debugLog.info('ui', 'Successfully left room', { roomId }); onDone(); } - }, [leaveState, onDone]); + }, [leaveState, onDone, roomId]); return ( }> diff --git a/src/app/features/create-room/CreateRoom.tsx b/src/app/features/create-room/CreateRoom.tsx index 38716c457..b4da51325 100644 --- a/src/app/features/create-room/CreateRoom.tsx +++ b/src/app/features/create-room/CreateRoom.tsx @@ -42,6 +42,9 @@ import { RoomType } from '$types/matrix/room'; import { CreateRoomTypeSelector } from '$components/create-room/CreateRoomTypeSelector'; import { getRoomIconSrc } from '$utils/room'; import { ErrorCode } from '../../cs-errorcode'; +import { createDebugLogger } from '$utils/debugLogger'; + +const debugLog = createDebugLogger('CreateRoom'); const getCreateRoomAccessToIcon = (access: CreateRoomAccess, type?: CreateRoomType) => { const isVoiceRoom = type === CreateRoomType.VoiceRoom; @@ -139,6 +142,16 @@ export function CreateRoomForm({ let roomType: RoomType | undefined; if (type === CreateRoomType.VoiceRoom) roomType = RoomType.Call; + debugLog.info('ui', 'Create room button clicked', { + roomName, + access, + type, + publicRoom, + encryption, + hasParent: !!space, + parentRoomId: space?.roomId, + }); + create({ version: selectedRoomVersion, type: roomType, @@ -152,6 +165,12 @@ export function CreateRoomForm({ allowFederation: federation, additionalCreators: allowAdditionalCreators ? additionalCreators : undefined, }).then((roomId) => { + debugLog.info('ui', 'Room created successfully', { + roomId, + roomName, + access, + type, + }); if (alive()) { onCreate?.(roomId); } diff --git a/src/app/features/settings/developer-tools/DebugLogViewer.tsx b/src/app/features/settings/developer-tools/DebugLogViewer.tsx index ae29edb24..6f3de2ae3 100644 --- a/src/app/features/settings/developer-tools/DebugLogViewer.tsx +++ b/src/app/features/settings/developer-tools/DebugLogViewer.tsx @@ -202,6 +202,7 @@ export function DebugLogViewer() { jsonData = JSON.stringify( { exportedAt: new Date().toISOString(), + build: `v${APP_VERSION}${BUILD_HASH ? ` (${BUILD_HASH})` : ''}`, filters: { level: filterLevel !== 'all' ? filterLevel : 'none', category: filterCategory !== 'all' ? filterCategory : 'none', @@ -249,6 +250,7 @@ export function DebugLogViewer() { jsonData = JSON.stringify( { exportedAt: new Date().toISOString(), + build: `v${APP_VERSION}${BUILD_HASH ? ` (${BUILD_HASH})` : ''}`, filters: { level: filterLevel !== 'all' ? filterLevel : 'none', category: filterCategory !== 'all' ? filterCategory : 'none', diff --git a/src/app/pages/client/BackgroundNotifications.tsx b/src/app/pages/client/BackgroundNotifications.tsx index ffb7da763..c294aef79 100644 --- a/src/app/pages/client/BackgroundNotifications.tsx +++ b/src/app/pages/client/BackgroundNotifications.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { ClientEvent, createClient, @@ -70,21 +70,29 @@ const startBackgroundClient = async ( /** * Wait for the background client to finish its initial sync so that * push rules and account data are available before processing events. + * Rejects after 30 seconds so callers can handle a stalled client instead + * of blocking indefinitely. */ const waitForSync = (mx: MatrixClient): Promise => - new Promise((resolve) => { + new Promise((resolve, reject) => { const state = mx.getSyncState(); if (isClientReadyForNotifications(state)) { resolve(); return; } + let syncTimer: ReturnType | undefined; const onSync = (newState: SyncState) => { if (isClientReadyForNotifications(newState)) { + if (syncTimer !== undefined) clearTimeout(syncTimer); mx.removeListener(ClientEvent.Sync, onSync); resolve(); } }; mx.on(ClientEvent.Sync, onSync); + syncTimer = setTimeout(() => { + mx.removeListener(ClientEvent.Sync, onSync); + reject(new Error('background client sync timed out')); + }, 30_000); }); export function BackgroundNotifications() { @@ -122,10 +130,18 @@ export function BackgroundNotifications() { setBackgroundUnreadsRef.current = setBackgroundUnreads; const setInAppBannerRef = useRef(setInAppBanner); setInAppBannerRef.current = setInAppBanner; + // Per-client listener teardown callbacks, so we can explicitly remove event + // listeners before stopping a background client. + const clientCleanupRef = useRef void>>(new Map()); - const inactiveSessions = sessions.filter( - (s) => s.userId !== (activeSessionId ?? sessions[0]?.userId) + const inactiveSessions = useMemo( + () => sessions.filter((s) => s.userId !== (activeSessionId ?? sessions[0]?.userId)), + [sessions, activeSessionId] ); + // Ref so retry setTimeout callbacks can access the current session list + // without stale closures. + const inactiveSessionsRef = useRef(inactiveSessions); + inactiveSessionsRef.current = inactiveSessions; interface NotifyOptions { /** Title shown in the notification banner. */ @@ -194,6 +210,8 @@ export function BackgroundNotifications() { current.forEach((mx, userId) => { if (!activeIds.has(userId)) { + clientCleanupRef.current.get(userId)?.(); + clientCleanupRef.current.delete(userId); stopClient(mx); current.delete(userId); // Clear the background unread badge when this session is no longer a background account. @@ -205,11 +223,14 @@ export function BackgroundNotifications() { } }); - inactiveSessions.forEach((session) => { - const alreadyRunning = current.has(session.userId); - if (alreadyRunning) return; + // startSession handles init, listener teardown tracking, and retry-on-failure. + // Using a named function (vs. inline .then) lets the .catch() schedule a + // fresh retry referencing the latest session from inactiveSessionsRef. + const startSession = (session: Session, attempt = 0): void => { + let sessionMx: MatrixClient | undefined; startBackgroundClient(session, clientConfig.slidingSync) .then(async (mx) => { + sessionMx = mx; current.set(session.userId, mx); await waitForSync(mx); @@ -471,6 +492,12 @@ export function BackgroundNotifications() { }; mx.on(RoomEvent.Timeline, handleTimeline as unknown as (...args: unknown[]) => void); + + // Register teardown so these listeners are removed when this client is stopped. + clientCleanupRef.current.set(session.userId, () => { + mx.off(ClientEvent.AccountData as any, handleAccountData); + mx.off(RoomEvent.Timeline, handleTimeline as unknown as (...args: unknown[]) => void); + }); }) .catch((err) => { log.error('failed to start background client for', session.userId, err); @@ -478,11 +505,41 @@ export function BackgroundNotifications() { userId: session.userId, error: err, }); + + // Remove the stuck/failed client from current so future runs (or the + // retry below) can attempt a fresh start. + if (sessionMx && current.get(session.userId) === sessionMx) { + clientCleanupRef.current.get(session.userId)?.(); + clientCleanupRef.current.delete(session.userId); + current.delete(session.userId); + stopClient(sessionMx); + } + + // Retry with exponential backoff, up to 5 attempts (5s, 10s, 20s, 40s, 60s cap). + if (attempt < 5) { + const retryDelay = Math.min(5_000 * 2 ** attempt, 60_000); + setTimeout(() => { + const latestSession = inactiveSessionsRef.current.find( + (s) => s.userId === session.userId + ); + if (latestSession && !current.has(session.userId)) { + startSession(latestSession, attempt + 1); + } + }, retryDelay); + } }); + }; + + inactiveSessions.forEach((session) => { + if (!current.has(session.userId)) startSession(session); }); return () => { - current.forEach((mx) => stopClient(mx)); + current.forEach((mx, userId) => { + clientCleanupRef.current.get(userId)?.(); + clientCleanupRef.current.delete(userId); + stopClient(mx); + }); current.clear(); }; }, [ diff --git a/src/app/pages/client/sidebar/AccountSwitcherTab.tsx b/src/app/pages/client/sidebar/AccountSwitcherTab.tsx index 1b722d244..39438b114 100644 --- a/src/app/pages/client/sidebar/AccountSwitcherTab.tsx +++ b/src/app/pages/client/sidebar/AccountSwitcherTab.tsx @@ -42,10 +42,12 @@ import { useSessionProfiles } from '$hooks/useSessionProfiles'; import { Settings } from '$features/settings'; import { Modal500 } from '$components/Modal500'; import { createLogger } from '$utils/debug'; +import { createDebugLogger } from '$utils/debugLogger'; import { useClientConfig } from '$hooks/useClientConfig'; import { UnreadBadge, UnreadBadgeCenter } from '$components/unread-badge'; const log = createLogger('AccountSwitcherTab'); +const debugLog = createDebugLogger('AccountSwitcherTab'); function AccountRow({ session, @@ -246,6 +248,9 @@ export function AccountSwitcherTab() { }; const handleOpenSettings = () => { + debugLog.info('ui', 'Settings button clicked', { + userId: activeSession?.userId, + }); setMenuAnchor(undefined); setSettingsOpen(true); }; diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index bc88b9af5..aae2f2d7b 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -72,6 +72,9 @@ import { mobileOrTablet } from '$utils/user-agent'; import { lastVisitedRoomIdAtom } from '$state/room/lastRoom'; import { SwipeableOverlayWrapper } from '$components/SwipeableOverlayWrapper'; import { useCallEmbed } from '$hooks/useCallEmbed'; +import { createDebugLogger } from '$utils/debugLogger'; + +const debugLog = createDebugLogger('Space'); type SpaceMenuProps = { room: Room; @@ -122,6 +125,7 @@ const SpaceMenu = forwardRef(({ room, requestClo }; const handleOpenTimeline = () => { + debugLog.info('ui', 'Space timeline opened', { roomId: room.roomId }); navigateRoom(room.roomId); requestClose(); }; diff --git a/src/app/utils/debugLogger.ts b/src/app/utils/debugLogger.ts index 3fe70eb50..18829d9f1 100644 --- a/src/app/utils/debugLogger.ts +++ b/src/app/utils/debugLogger.ts @@ -140,6 +140,7 @@ class DebugLoggerService { return JSON.stringify( { exportedAt: new Date().toISOString(), + build: `v${APP_VERSION}${BUILD_HASH ? ` (${BUILD_HASH})` : ''}`, logsCount: this.logs.length, logs: this.logs.map((log) => ({ ...log, diff --git a/src/client/initMatrix.ts b/src/client/initMatrix.ts index 6b4adbc11..45c6fdd0f 100644 --- a/src/client/initMatrix.ts +++ b/src/client/initMatrix.ts @@ -433,8 +433,12 @@ export const startClient = async ( if (await shouldBootstrapClassicOnColdCache()) { log.log('startClient cold-cache bootstrap: using classic sync for this run', mx.getUserId()); await startClassicSync(false, 'cold_cache_bootstrap'); - waitForClientReady(mx, COLD_CACHE_BOOTSTRAP_TIMEOUT_MS).catch(() => { - /* ignore */ + waitForClientReady(mx, COLD_CACHE_BOOTSTRAP_TIMEOUT_MS).catch((err) => { + debugLog.warn('network', 'Cold cache bootstrap timed out', { + userId: mx.getUserId(), + timeout: `${COLD_CACHE_BOOTSTRAP_TIMEOUT_MS}ms`, + error: err instanceof Error ? err.message : String(err), + }); }); return; } @@ -454,6 +458,11 @@ export const startClient = async ( }); if (!supported) { log.warn('Sliding Sync unavailable, falling back to classic sync for', mx.getUserId()); + debugLog.warn('network', 'Sliding Sync probe failed, falling back to classic sync', { + userId: mx.getUserId(), + proxyBaseUrl: resolvedProxyBaseUrl, + probeTimeout: `${probeTimeoutMs}ms`, + }); await startClassicSync(true, 'probe_failed_fallback'); return; } @@ -484,6 +493,12 @@ export const startClient = async ( slidingSync: manager.slidingSync, }); } catch (err) { + debugLog.error('network', 'Failed to start client with sliding sync', { + error: err instanceof Error ? err.message : String(err), + userId: mx.getUserId(), + proxyBaseUrl: resolvedProxyBaseUrl, + stack: err instanceof Error ? err.stack : undefined, + }); disposeSlidingSync(mx); throw err; } From fb80eabc2265e77ddff305c88bc34edcef7c2b17 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 13 Mar 2026 15:06:30 -0400 Subject: [PATCH 09/15] chore: add changeset for internal debug logging --- .changeset/feat-internal-debug-logging.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/feat-internal-debug-logging.md diff --git a/.changeset/feat-internal-debug-logging.md b/.changeset/feat-internal-debug-logging.md new file mode 100644 index 000000000..30d7e68c8 --- /dev/null +++ b/.changeset/feat-internal-debug-logging.md @@ -0,0 +1,5 @@ +--- +sable: minor +--- + +Add internal debug logging system with viewer UI, realtime updates, and instrumentation across sync, timeline, and messaging From 14eaad61a0bb976c8b4ce7db4cfd690bb670e060 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 13 Mar 2026 21:50:54 -0400 Subject: [PATCH 10/15] chore: remove accidentally committed .envrc file --- .envrc | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .envrc diff --git a/.envrc b/.envrc deleted file mode 100644 index a26cad735..000000000 --- a/.envrc +++ /dev/null @@ -1,6 +0,0 @@ -# Load nvm and use the version from .nvmrc -export NVM_DIR="$HOME/.nvm" -if [ -s "$NVM_DIR/nvm.sh" ]; then - source "$NVM_DIR/nvm.sh" - nvm use -fi From 54ac93c39abdc264407373c303c35c0ccaebfda9 Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Fri, 13 Mar 2026 22:10:36 -0400 Subject: [PATCH 11/15] chore: fix formatting and linting issues for CI --- src/app/features/create-room/CreateRoom.tsx | 2 +- src/app/features/room/Room.tsx | 7 +- src/app/features/room/RoomInput.tsx | 34 ++++-- .../developer-tools/DebugLogViewer.tsx | 25 ++-- .../notifications/PushNotifications.tsx | 19 ++- src/app/hooks/useAppVisibility.ts | 6 +- src/app/pages/auth/login/loginUtil.ts | 5 +- src/app/plugins/call/CallEmbed.ts | 2 +- src/app/plugins/call/CallWidgetDriver.ts | 6 +- src/app/state/debugLogger.ts | 24 ++-- src/app/utils/debugLogger.ts | 17 +-- src/client/initMatrix.ts | 16 +-- src/client/slidingSync.ts | 113 +++++++++--------- 13 files changed, 151 insertions(+), 125 deletions(-) diff --git a/src/app/features/create-room/CreateRoom.tsx b/src/app/features/create-room/CreateRoom.tsx index b4da51325..7347ba586 100644 --- a/src/app/features/create-room/CreateRoom.tsx +++ b/src/app/features/create-room/CreateRoom.tsx @@ -41,8 +41,8 @@ import { import { RoomType } from '$types/matrix/room'; import { CreateRoomTypeSelector } from '$components/create-room/CreateRoomTypeSelector'; import { getRoomIconSrc } from '$utils/room'; -import { ErrorCode } from '../../cs-errorcode'; import { createDebugLogger } from '$utils/debugLogger'; +import { ErrorCode } from '../../cs-errorcode'; const debugLog = createDebugLogger('CreateRoom'); diff --git a/src/app/features/room/Room.tsx b/src/app/features/room/Room.tsx index b8ff5cc54..694b1c7f8 100644 --- a/src/app/features/room/Room.tsx +++ b/src/app/features/room/Room.tsx @@ -15,11 +15,11 @@ import { useRoomMembers } from '$hooks/useRoomMembers'; import { CallView } from '$features/call/CallView'; import { WidgetsDrawer } from '$features/widgets/WidgetsDrawer'; import { callChatAtom } from '$state/callEmbed'; +import { createDebugLogger } from '$utils/debugLogger'; import { RoomViewHeader } from './RoomViewHeader'; import { MembersDrawer } from './MembersDrawer'; import { RoomView } from './RoomView'; import { CallChatView } from './CallChatView'; -import { createDebugLogger } from '$utils/debugLogger'; const debugLog = createDebugLogger('Room'); @@ -47,7 +47,10 @@ export function Room() { }, [isDrawer, room.roomId]); useEffect(() => { - debugLog.debug('ui', 'Widgets drawer state changed', { roomId: room.roomId, isOpen: isWidgetDrawerOpen }); + debugLog.debug('ui', 'Widgets drawer state changed', { + roomId: room.roomId, + isOpen: isWidgetDrawerOpen, + }); }, [isWidgetDrawerOpen, room.roomId]); const powerLevels = usePowerLevels(room); const members = useRoomMembers(mx, room.roomId); diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 8627bb8e4..3f7becd55 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -424,13 +424,21 @@ export const RoomInput = forwardRef( await Promise.all( contents.map((content) => - mx.sendMessage(roomId, content as any) + mx + .sendMessage(roomId, content as any) .then((res) => { - debugLog.info('message', 'Uploaded file message sent', { roomId, eventId: res.event_id, msgtype: content.msgtype }); + debugLog.info('message', 'Uploaded file message sent', { + roomId, + eventId: res.event_id, + msgtype: content.msgtype, + }); return res; }) .catch((error: unknown) => { - debugLog.error('message', 'Failed to send uploaded file message', { roomId, error: error instanceof Error ? error.message : String(error) }); + debugLog.error('message', 'Failed to send uploaded file message', { + roomId, + error: error instanceof Error ? error.message : String(error), + }); log.error('failed to send uploaded message', { roomId }, error); throw error; }) @@ -577,14 +585,20 @@ export const RoomInput = forwardRef( } else if (editingScheduledDelayId) { try { await cancelDelayedEvent(mx, editingScheduledDelayId); - debugLog.info('message', 'Sending message after cancelling scheduled event', { roomId, scheduledDelayId: editingScheduledDelayId }); + debugLog.info('message', 'Sending message after cancelling scheduled event', { + roomId, + scheduledDelayId: editingScheduledDelayId, + }); const res = await mx.sendMessage(roomId, content as any); debugLog.info('message', 'Message sent successfully', { roomId, eventId: res.event_id }); invalidate(); setEditingScheduledDelayId(null); resetInput(); } catch (error) { - debugLog.error('message', 'Failed to send message after cancelling scheduled event', { roomId, error: error instanceof Error ? error.message : String(error) }); + debugLog.error('message', 'Failed to send message after cancelling scheduled event', { + roomId, + error: error instanceof Error ? error.message : String(error), + }); // Cancel failed — leave state intact for retry } } else { @@ -592,10 +606,16 @@ export const RoomInput = forwardRef( debugLog.info('message', 'Sending message', { roomId, msgtype: (content as any).msgtype }); mx.sendMessage(roomId, content as any) .then((res) => { - debugLog.info('message', 'Message sent successfully', { roomId, eventId: res.event_id }); + debugLog.info('message', 'Message sent successfully', { + roomId, + eventId: res.event_id, + }); }) .catch((error: unknown) => { - debugLog.error('message', 'Failed to send message', { roomId, error: error instanceof Error ? error.message : String(error) }); + debugLog.error('message', 'Failed to send message', { + roomId, + error: error instanceof Error ? error.message : String(error), + }); log.error('failed to send message', { roomId }, error); }); } diff --git a/src/app/features/settings/developer-tools/DebugLogViewer.tsx b/src/app/features/settings/developer-tools/DebugLogViewer.tsx index 6f3de2ae3..8e5ae01e1 100644 --- a/src/app/features/settings/developer-tools/DebugLogViewer.tsx +++ b/src/app/features/settings/developer-tools/DebugLogViewer.tsx @@ -3,11 +3,7 @@ import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { Box, Text, Button, color, config, Badge, Menu, MenuItem, PopOut, RectCords } from 'folds'; import { SequenceCard } from '$components/sequence-card'; -import { - debugLoggerEnabledAtom, - debugLogsAtom, - clearDebugLogsAtom, -} from '$state/debugLogger'; +import { debugLoggerEnabledAtom, debugLogsAtom, clearDebugLogsAtom } from '$state/debugLogger'; import { LogEntry, getDebugLogger, LogLevel, LogCategory } from '$utils/debugLogger'; import { SequenceCardStyle } from '$features/settings/styles.css'; @@ -225,9 +221,10 @@ export function DebugLogViewer() { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - const filterSuffix = filtered && (filterLevel !== 'all' || filterCategory !== 'all') - ? `-${filterCategory !== 'all' ? filterCategory : 'all'}-${filterLevel !== 'all' ? filterLevel : 'all'}` - : ''; + const filterSuffix = + filtered && (filterLevel !== 'all' || filterCategory !== 'all') + ? `-${filterCategory !== 'all' ? filterCategory : 'all'}-${filterLevel !== 'all' ? filterLevel : 'all'}` + : ''; a.download = `sable-debug-logs${filterSuffix}-${new Date().toISOString()}.json`; document.body.appendChild(a); a.click(); @@ -422,9 +419,7 @@ export function DebugLogViewer() { size="300" radii="300" > - - Category: {filterCategory === 'all' ? 'All' : filterCategory} - + Category: {filterCategory === 'all' ? 'All' : filterCategory} @@ -501,9 +496,7 @@ export function DebugLogViewer() { size="300" radii="300" > - - Level: {filterLevel === 'all' ? 'All' : filterLevel} - + Level: {filterLevel === 'all' ? 'All' : filterLevel} @@ -551,9 +544,7 @@ export function DebugLogViewer() { onClick={() => handleExportLogs(true)} disabled={filteredLogs.length === 0} > - - Export Filtered ({filteredLogs.length}) - + Export Filtered ({filteredLogs.length})