DebugLogModal.svelte
1 <script lang="ts"> 2 import type { DebugLogEntry, DebugEventType } from '../../lib/debug/types'; 3 import { Modal, Button } from '../common'; 4 5 interface Props { 6 open: boolean; 7 onclose: () => void; 8 } 9 10 let { open = $bindable(), onclose }: Props = $props(); 11 12 let entries = $state<DebugLogEntry[]>([]); 13 let loading = $state(false); 14 let filterType = $state<DebugEventType | 'all'>('all'); 15 16 const eventTypes: (DebugEventType | 'all')[] = [ 17 'all', 18 'auto_save_triggered', 19 'auto_save_completed', 20 'auto_save_skipped', 21 'auto_save_cancelled', 22 'capture_tabs', 23 'update_tabs', 24 'zero_tab_blocked', 25 'window_removed', 26 'tab_detached', 27 'tab_removed', 28 'sync_merge', 29 ]; 30 31 $effect(() => { 32 if (open) { 33 loadLogs(); 34 } 35 }); 36 37 async function loadLogs() { 38 loading = true; 39 try { 40 const response = await browser.runtime.sendMessage({ type: 'GET_DEBUG_LOGS' }); 41 if (response.success) { 42 entries = response.entries; 43 } 44 } catch { 45 // Silently fail 46 } finally { 47 loading = false; 48 } 49 } 50 51 async function clearLogs() { 52 try { 53 await browser.runtime.sendMessage({ type: 'CLEAR_DEBUG_LOGS' }); 54 entries = []; 55 } catch { 56 // Silently fail 57 } 58 } 59 60 async function exportLogs() { 61 try { 62 const response = await browser.runtime.sendMessage({ type: 'EXPORT_DEBUG_LOGS' }); 63 if (response.success) { 64 const blob = new Blob([response.data], { type: 'application/json' }); 65 const url = URL.createObjectURL(blob); 66 const a = document.createElement('a'); 67 a.href = url; 68 a.download = `mnemonic-debug-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`; 69 a.click(); 70 URL.revokeObjectURL(url); 71 } 72 } catch { 73 // Silently fail 74 } 75 } 76 77 function formatTime(timestamp: string): string { 78 const date = new Date(timestamp); 79 return date.toLocaleTimeString(undefined, { 80 hour: '2-digit', 81 minute: '2-digit', 82 second: '2-digit', 83 }); 84 } 85 86 function formatDate(timestamp: string): string { 87 const date = new Date(timestamp); 88 return date.toLocaleDateString(undefined, { 89 month: 'short', 90 day: 'numeric', 91 }); 92 } 93 94 function formatData(data: Record<string, unknown>): string { 95 const parts: string[] = []; 96 for (const [key, value] of Object.entries(data)) { 97 parts.push(`${key}=${JSON.stringify(value)}`); 98 } 99 return parts.join(', '); 100 } 101 102 function getEventColor(event: DebugEventType): string { 103 switch (event) { 104 case 'zero_tab_blocked': 105 return 'var(--status-error)'; 106 case 'auto_save_cancelled': 107 case 'auto_save_skipped': 108 return 'var(--status-warning)'; 109 case 'auto_save_completed': 110 case 'update_tabs': 111 return 'var(--status-success)'; 112 default: 113 return 'var(--text-muted)'; 114 } 115 } 116 117 let filteredEntries = $derived( 118 filterType === 'all' ? entries : entries.filter((e) => e.event === filterType) 119 ); 120 </script> 121 122 <Modal bind:open title="Debug Logs" {onclose} size="lg"> 123 <div class="debug-log-viewer"> 124 <div class="toolbar"> 125 <select class="filter-select" bind:value={filterType}> 126 {#each eventTypes as type} 127 <option value={type}>{type === 'all' ? 'All Events' : type}</option> 128 {/each} 129 </select> 130 <span class="entry-count">{filteredEntries.length} entries</span> 131 <div class="toolbar-actions"> 132 <Button variant="ghost" size="sm" onclick={loadLogs} title="Refresh"> 133 <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 134 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> 135 </svg> 136 </Button> 137 <Button variant="ghost" size="sm" onclick={exportLogs} title="Export JSON"> 138 Export 139 </Button> 140 <Button variant="ghost" size="sm" onclick={clearLogs} title="Clear all logs"> 141 Clear 142 </Button> 143 </div> 144 </div> 145 146 {#if loading} 147 <div class="loading-state">Loading...</div> 148 {:else if filteredEntries.length === 0} 149 <div class="empty-state"> 150 <p class="text-text-muted text-sm">No debug log entries{filterType !== 'all' ? ` for "${filterType}"` : ''}.</p> 151 </div> 152 {:else} 153 <div class="log-entries"> 154 {#each filteredEntries as entry} 155 <div class="log-entry" class:highlight={entry.event === 'zero_tab_blocked'}> 156 <div class="entry-header"> 157 <span class="entry-time">{formatDate(entry.timestamp)} {formatTime(entry.timestamp)}</span> 158 <span class="entry-event" style="color: {getEventColor(entry.event)}">{entry.event}</span> 159 </div> 160 {#if Object.keys(entry.data).length > 0} 161 <div class="entry-data">{formatData(entry.data)}</div> 162 {/if} 163 </div> 164 {/each} 165 </div> 166 {/if} 167 </div> 168 169 <svelte:fragment slot="footer"> 170 <Button variant="ghost" onclick={onclose}>Close</Button> 171 </svelte:fragment> 172 </Modal> 173 174 <style> 175 .debug-log-viewer { 176 min-height: 300px; 177 } 178 179 .toolbar { 180 display: flex; 181 align-items: center; 182 gap: 0.75rem; 183 margin-bottom: 0.75rem; 184 padding-bottom: 0.75rem; 185 border-bottom: 1px solid var(--border-primary); 186 } 187 188 .filter-select { 189 background: var(--bg-secondary); 190 color: var(--text-primary); 191 border: 1px solid var(--border-primary); 192 border-radius: 0.25rem; 193 padding: 0.25rem 0.5rem; 194 font-size: 0.8125rem; 195 } 196 197 .entry-count { 198 font-size: 0.75rem; 199 color: var(--text-muted); 200 } 201 202 .toolbar-actions { 203 display: flex; 204 gap: 0.25rem; 205 margin-left: auto; 206 } 207 208 .loading-state, 209 .empty-state { 210 display: flex; 211 align-items: center; 212 justify-content: center; 213 padding: 2rem; 214 color: var(--text-muted); 215 } 216 217 .log-entries { 218 max-height: 400px; 219 overflow-y: auto; 220 } 221 222 .log-entry { 223 padding: 0.5rem 0.75rem; 224 border-bottom: 1px solid var(--border-secondary); 225 font-size: 0.8125rem; 226 } 227 228 .log-entry.highlight { 229 background: rgba(var(--status-error-rgb), 0.1); 230 border-left: 3px solid var(--status-error); 231 } 232 233 .entry-header { 234 display: flex; 235 align-items: center; 236 gap: 0.75rem; 237 } 238 239 .entry-time { 240 font-size: 0.75rem; 241 color: var(--text-muted); 242 font-variant-numeric: tabular-nums; 243 } 244 245 .entry-event { 246 font-weight: 500; 247 font-family: monospace; 248 font-size: 0.75rem; 249 } 250 251 .entry-data { 252 margin-top: 0.25rem; 253 font-size: 0.75rem; 254 color: var(--text-secondary); 255 font-family: monospace; 256 word-break: break-all; 257 } 258 </style>