/ src / components / debug / DebugLogModal.svelte
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>