PageSourceSearch

https://neloura.com/static/catalog_viewer.js?v=20260201-02

js neloura.com collected 2026-10-07 01:45:29 UTC 93,189 bytes, 1,891 lines download raw bytes

1// Cache for raw binary catalogs (keyed by exact filename incl. .fits)
2const catalogBinaryCache = window.catalogBinaryCache || (window.catalogBinaryCache = {});
3// Single-flight guard to avoid duplicate parallel loads for the same catalog
4const catalogBinaryInflight = window.catalogBinaryInflight || (window.catalogBinaryInflight = {});
5
6function stripCatalogPrefix(name) {
7  return name && name.startsWith('catalogs/') ? name.slice('catalogs/'.length) : name;
8}
9function namesMatch(a, b) {
10  return stripCatalogPrefix(String(a)) === stripCatalogPrefix(String(b));
11}
12
13
14async function loadBinaryCatalogRawIntoCache(catalogName) {
15    const key = stripCatalogPrefix(catalogName);
16    if (catalogBinaryCache[key]) return catalogBinaryCache[key];
17    if (catalogBinaryInflight[key]) return catalogBinaryInflight[key];
18
19    const inflight = (async () => {
20      const metaResp = await apiFetch(`/catalog-metadata/${encodeURIComponent(key)}?ts=${Date.now()}`);
21      if (!metaResp.ok) throw new Error(`Failed to load catalog metadata: ${metaResp.statusText}`);
22      const meta = await metaResp.json();
23
24      // Only fetch the first page once to warm minimal cache; no multi-page prefetch
25      const url = `/catalog-binary-raw/${encodeURIComponent(key)}?page=1&limit=500&ts=${Date.now()}`;
26      const resp = await apiFetch(url);
27      if (!resp.ok) {
28        let msg = `${resp.status} ${resp.statusText}`;
29        try { msg = (await resp.json()).error || msg; } catch (_) {}
30        throw new Error(`Failed to load raw binary page 1: ${msg}`);
31      }
32      const arrayBuf = await resp.arrayBuffer();
33      const parsed = (typeof parseBinaryCatalog === 'function')
34        ? parseBinaryCatalog(arrayBuf)
35        : (window.parseBinaryCatalog ? window.parseBinaryCatalog(arrayBuf) : null);
36      if (!parsed || !parsed.records) throw new Error('Binary parse failed (raw)');
37
38      const columns = meta.column_names || (parsed.records[0] ? Object.keys(parsed.records[0]) : []);
39      const cacheEntry = {
40        header: meta,
41        records: parsed.records,
42        columns,
43        total_rows: meta.total_rows || parsed.records.length,
44        total_columns: columns.length
45      };
46      catalogBinaryCache[key] = cacheEntry;
47      return cacheEntry;
48    })();
49
50    catalogBinaryInflight[key] = inflight;
51    try {
52      const result = await inflight;
53      return result;
54    } finally {
55      delete catalogBinaryInflight[key];
56    }
57  }
58  
59  async function ensureBinaryCatalogLoaded(catalogName) {
60    const key = stripCatalogPrefix(catalogName);
61    if (catalogBinaryCache[key]) return catalogBinaryCache[key];
62    return loadBinaryCatalogRawIntoCache(key);
63  }
64  
65  // Metadata for viewer (from RAW cache)
66// Viewer API
67async function fetchCatalogMetadata(catalogName) {
68    const key = stripCatalogPrefix(catalogName);
69    const resp = await apiFetch(`/catalog-metadata/${encodeURIComponent(key)}?ts=${Date.now()}`);
70    if (!resp.ok) {
71      throw new Error(`Failed to load catalog metadata: ${resp.status} ${resp.statusText}`);
72    }
73    const meta = await resp.json();
74
75    // Normalize columns to the shape this viewer expects
76    let columnsMeta = [];
77    const rawCols = Array.isArray(meta.columns) ? meta.columns : (Array.isArray(meta.column_names) ? meta.column_names : []);
78    if (rawCols.length && typeof rawCols[0] === 'object') {
79      columnsMeta = rawCols;
80    } else {
81      columnsMeta = rawCols.map(name => ({ name, dtype: 'unknown', unit: null, is_numeric: false, is_boolean: false }));
82    }
83
84    const columnNames = columnsMeta.map(c => c.name);
85    const totalRows = meta.total_rows ?? meta.num_records ?? meta.row_count ?? 0;
86
87    return {
88      total_rows: totalRows,
89      total_columns: columnNames.length,
90      column_names: columnNames,
91      columns: columnsMeta,
92      raw: meta
93    };
94  }
95  
96 
97
98
99  async function fetchCatalogData(catalogName, options = {}) {
100    const {
101      page = 1,
102      limit = 100,
103      search = null,        // client-side (not applied when using raw binary)
104      sortBy = null,        // client-side (not applied when using raw binary)
105      sortOrder = 'asc',    // client-side (not applied when using raw binary)
106      columns = null,
107      filters = null,       // client-side (not applied when using raw binary)
108      stats = false
109    } = options;
110
111    const key = stripCatalogPrefix(catalogName);
112    const hasClientOps = (filters && Object.keys(filters).length > 0) || (search && String(search).trim()) || !!sortBy;
113
114    const colParam = columns ? (Array.isArray(columns) ? columns.join(',') : String(columns)) : '';
115    const effLimit = hasClientOps ? Math.min(Number(limit)||500, 500) : limit;
116    const url = `/catalog-binary-raw/${encodeURIComponent(key)}?page=${page}&limit=${effLimit}`
117      + (colParam ? `&columns=${encodeURIComponent(colParam)}` : '')
118      + (hasClientOps ? `&filters=${encodeURIComponent(JSON.stringify(filters||{}))}` : '')
119      + (search ? `&search=${encodeURIComponent(String(search))}` : '')
120      + (sortBy ? `&sort_by=${encodeURIComponent(String(sortBy))}&sort_order=${encodeURIComponent(String(sortOrder||'asc'))}` : '')
121      + `&ts=${Date.now()}`;
122    const fetchOpts = {};
123    if (options && options.signal) fetchOpts.signal = options.signal;
124    const resp = await apiFetch(url, fetchOpts);
125    if (!resp.ok) {
126      let msg = `${resp.status} ${resp.statusText}`;
127      try { msg = (await resp.json()).error || msg; } catch (_) {}
128      throw new Error(`Failed to load data page ${page}: ${msg}`);
129    }
130
131    const arrayBuf = await resp.arrayBuffer();
132    const parsed = (typeof parseBinaryCatalog === 'function')
133      ? parseBinaryCatalog(arrayBuf)
134      : (window.parseBinaryCatalog ? window.parseBinaryCatalog(arrayBuf) : null);
135    if (!parsed || !parsed.records || !parsed.header) throw new Error('Binary parse failed');
136
137    const pg = parsed.header.pagination || {};
138    const totalItems = pg.total_items ?? parsed.header.total_rows ?? 0;
139    const totalPages = pg.total_pages ?? Math.max(1, Math.ceil(totalItems / limit));
140    const showingStart = pg.showing_start ?? (totalItems ? (page - 1) * limit + 1 : 0);
141    const showingEnd = pg.showing_end ?? Math.min(page * limit, totalItems);
142
143    return {
144      catalog_data: parsed.records,
145      pagination: {
146        page,
147        limit,
148        total_items: totalItems,
149        total_pages: totalPages,
150        has_prev: !!pg.has_prev || page > 1,
151        has_next: !!pg.has_next || page < totalPages,
152        showing_start: showingStart,
153        showing_end: showingEnd
154      },
155      column_stats: stats ? null : null
156    };
157  }
158
159
160
161
162// Prefer the already-loaded overlay dataset for this catalog (keeps content consistent)
163function tryGetLocalActiveCatalog(catalogName) {
164  const key = stripCatalogPrefix(catalogName);
165  const active = stripCatalogPrefix(window.currentCatalogName || window.activeCatalog || '');
166  if (active && active === key && Array.isArray(window.catalogDataWithFlags) && window.catalogDataWithFlags.length > 0) {
167    const columns =
168      (window.catalogMetadata && window.catalogMetadata.column_names) ||
169      Object.keys(window.catalogDataWithFlags[0]);
170    return {
171      header: window.catalogMetadata || {},
172      records: window.catalogDataWithFlags,
173      columns,
174      total_rows:
175        (window.catalogMetadata && (window.catalogMetadata.num_records || window.catalogMetadata.total_rows)) ||
176        window.catalogDataWithFlags.length,
177      total_columns: columns.length,
178    };
179  }
180  return null;
181}
182
183
184
185function showCatalogViewer(catalogName) {
186    showNotification(true, `Loading catalog metadata for ${catalogName}...`);
187    
188    // First, get metadata to understand the catalog structure
189    fetchCatalogMetadata(catalogName)
190        .then(metadata => {
191            showNotification(false);
192            createAdvancedCatalogViewer(catalogName, metadata);
193        })
194        .catch(error => {
195            showNotification(false);
196            showNotification(`Error loading catalog: ${error.message}`, 4000, 'error');
197            console.error("Error in showCatalogViewer:", error);
198        });
199}
200
201
202
203
204/**
205 * Create advanced catalog viewer with TopCat-like interface
206 */
207function createAdvancedCatalogViewer(catalogName, metadata) {
208    const existingPopup = document.getElementById('catalog-viewer-popup');
209    if (existingPopup) {
210        existingPopup.remove();
211    }
212
213    const popup = document.createElement('div');
214    popup.id = 'catalog-viewer-popup';
215    Object.assign(popup.style, {
216        position: 'fixed', top: '5%', left: '5%', 
217        width: '90vw', height: '90vh', backgroundColor: '#2d2d2d',
218        // Keep above Plotter/Region Style (z-index ~3600 range)
219        border: '1px solid #444', borderRadius: '8px', zIndex: '3705',
220        display: 'flex', flexDirection: 'column',
221        boxShadow: '0 8px 16px rgba(0,0,0,0.5)',
222        color: 'white', fontFamily: 'Arial, sans-serif'
223    });
224
225    // State management
226    const state = {
227        currentPage: 1,
228        pageSize: 500,
229        totalItems: metadata.total_rows,
230        search: '',
231        sortBy: null,
232        sortOrder: 'asc',
233        loading: false,
234        data: [],
235        columns: metadata.column_names,
236        metadata: metadata,
237        activeFilters: {},
238        // Show all columns by default
239        selectedColumns: Array.isArray(metadata.column_names) ? [...metadata.column_names] : [],
240        selectedRows: new Set()
241    };
242
243    // Create layout
244    const header = createAdvancedHeader(catalogName, metadata, popup);
245    let loadTimer = null;
246    const scheduleLoad = () => {
247        if (loadTimer) clearTimeout(loadTimer);
248        loadTimer = setTimeout(() => loadData(true), 250);
249    };
250    const toolbar = createToolbar(state, scheduleLoad);
251    const mainContent = createMainContent(state);
252    const statusBar = createStatusBar(state);
253
254    popup.appendChild(header);
255    popup.appendChild(toolbar);
256    popup.appendChild(mainContent);
257    popup.appendChild(statusBar);
258    document.body.appendChild(popup);
259    
260    if (typeof makeDraggable === 'function') {
261        makeDraggable(popup, header);
262    }
263
264    // Load initial data
265    scheduleLoad();
266
267    async function loadData(immediate) {
268        if (!immediate) return; // Only run when scheduled
269        if (state.loading) {
270            try { if (state._abort && typeof state._abort.abort === 'function') state._abort.abort(); } catch(_){}
271        }
272        
273        state.loading = true;
274        // Setup abort controller to cancel older requests
275        try { if (state._abort && typeof state._abort.abort === 'function') state._abort.abort(); } catch(_){}
276        state._abort = (typeof AbortController !== 'undefined') ? new AbortController() : null;
277        updateLoadingState(true);
278        
279        try {
280            const options = {
281                page: state.currentPage,
282                limit: state.pageSize,
283                search: state.search || null,
284                sortBy: state.sortBy,
285                sortOrder: state.sortOrder,
286                columns: state.selectedColumns.join(','),
287                filters: Object.keys(state.activeFilters).length > 0 ? state.activeFilters : null,
288                stats: true,
289                signal: state._abort ? state._abort.signal : undefined
290            };
291
292            const response = await fetchCatalogData(catalogName, options);
293            
294            state.data = response.catalog_data;
295            state.totalItems = response.pagination.total_items;
296            
297            renderTable(state.data, state.selectedColumns);
298            updatePaginationInfo(response.pagination);
299            updateStatsDisplay(response.column_stats);
300            
301        } catch (error) {
302            showNotification(`Error loading data: ${error.message}`, 4000, 'error');
303            console.error('Error loading catalog data:', error);
304        } finally {
305            state.loading = false;
306            updateLoadingState(false);
307        }
308    }
309
310    function createAdvancedHeader(catalogName, metadata, popup) {
311        const header = document.createElement('div');
312        Object.assign(header.style, {
313            padding: '10px 15px', borderBottom: '1px solid #444',
314            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
315            backgroundColor: '#333', borderTopLeftRadius: '8px', borderTopRightRadius: '8px',
316            cursor: 'move'
317        });
318
319        const titleContainer = document.createElement('div');
320        
321        const title = document.createElement('h2');
322        title.textContent = `Catalog: ${catalogName.split('/').pop()}`;
323        Object.assign(title.style, { margin: '0 0 5px 0', fontSize: '18px', fontWeight: 'bold' });
324
325        const info = document.createElement('div');
326        info.innerHTML = `
327            <span style="color: #aaa; font-size: 12px;">
328                ${metadata.total_rows.toLocaleString()} rows × ${metadata.total_columns} columns
329                ${metadata.memory_usage_mb ? `• ${metadata.memory_usage_mb.toFixed(1)} MB` : ''}
330            </span>
331        `;
332
333        titleContainer.appendChild(title);
334        titleContainer.appendChild(info);
335
336        const buttonContainer = document.createElement('div');
337        Object.assign(buttonContainer.style, { display: 'flex', gap: '10px', alignItems: 'center' });
338
339        // Info button
340        
341        // Export button
342        const exportBtn = createHeaderButton('Export', 'Export', () => showExportDialog(state));
343        
344        // Settings button
345        const settingsBtn = createHeaderButton('Settings', 'Settings', () => showSettingsDialog(state, loadData));
346
347        const closeButton = document.createElement('button');
348        closeButton.innerHTML = '×';
349        Object.assign(closeButton.style, {
350            background: 'none', border: 'none', color: 'white',
351            fontSize: '24px', cursor: 'pointer', fontWeight: 'bold',
352            marginLeft: '10px'
353        });
354        closeButton.onclick = () => popup.remove();
355
356        // buttonContainer.appendChild(infoBtn);
357        buttonContainer.appendChild(exportBtn);
358        buttonContainer.appendChild(settingsBtn);
359        buttonContainer.appendChild(closeButton);
360        
361        header.appendChild(titleContainer);
362        header.appendChild(buttonContainer);
363        
364        return header;
365    }
366
367    function createHeaderButton(text, title, onclick) {
368        const button = document.createElement('button');
369        button.textContent = text;
370        button.title = title;
371        button.onclick = onclick;
372        Object.assign(button.style, {
373            background: '#444', border: '1px solid #555', color: 'white',
374            fontSize: '14px', cursor: 'pointer', borderRadius: '4px',
375            padding: '5px 8px', height: '32px'
376        });
377        button.onmouseenter = () => button.style.backgroundColor = '#555';
378        button.onmouseleave = () => button.style.backgroundColor = '#444';
379        return button;
380    }
381
382    function createToolbar(state, onUpdate) {
383        const toolbar = document.createElement('div');
384        Object.assign(toolbar.style, { 
385            padding: '10px 15px',
386            display: 'flex',
387            gap: '10px',
388            alignItems: 'center',
389            flexWrap: 'wrap',
390            borderBottom: '1px solid #444',
391            backgroundColor: '#333'
392        });
393
394        // Search input with advanced options
395        const searchContainer = document.createElement('div');
396        Object.assign(searchContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' });
397        
398        const searchInput = document.createElement('input');
399        searchInput.type = 'text';
400        searchInput.placeholder = 'Search all columns...';
401        Object.assign(searchInput.style, {
402            minWidth: '200px',
403            padding: '8px',
404            backgroundColor: '#444',
405            color: 'white',
406            border: '1px solid #555',
407            borderRadius: '4px'
408        });
409
410        const searchButton = document.createElement('button');
411        searchButton.textContent = 'Advanced';
412        searchButton.title = 'Advanced Search';
413        Object.assign(searchButton.style, {
414            padding: '8px', backgroundColor: '#444', color: 'white',
415            border: '1px solid #555', borderRadius: '4px', cursor: 'pointer'
416        });
417        searchButton.onclick = () => showAdvancedSearchDialog(state, onUpdate);
418
419        searchContainer.appendChild(searchInput);
420        // searchContainer.appendChild(searchButton);
421
422        // Filter indicator
423        const filterIndicator = document.createElement('div');
424        filterIndicator.className = 'filter-indicator';
425        Object.assign(filterIndicator.style, {
426            padding: '5px 10px', backgroundColor: '#555', borderRadius: '15px',
427            fontSize: '12px', display: 'none'
428        });
429
430        // Page size selector
431        const pageSizeSelect = document.createElement('select');
432        [100, 500, 1000, 2000, 5000].forEach(size => {
433            const option = document.createElement('option');
434            option.value = size;
435            option.textContent = `${size} rows`;
436            option.selected = size === state.pageSize;
437            pageSizeSelect.appendChild(option);
438        });
439        Object.assign(pageSizeSelect.style, {
440            padding: '8px', backgroundColor: '#444', color: 'white',
441            border: '1px solid #555', borderRadius: '4px'
442        });
443
444        // Column selector
445        const columnButton = createToolbarButton('Columns', 'Select Columns', () => showColumnSelector(state, onUpdate));
446        
447        // Filter button
448        const filterButton = createToolbarButton('Filters', 'Filters', () => showFilterPanel(state, onUpdate));
449
450        // Selection info
451        const selectionInfo = document.createElement('div');
452        selectionInfo.className = 'selection-info';
453        Object.assign(selectionInfo.style, {
454            fontSize: '12px', color: '#aaa', marginLeft: 'auto'
455        });
456
457        // Debounced search
458        let searchTimeout;
459        searchInput.addEventListener('input', (e) => {
460            clearTimeout(searchTimeout);
461            searchTimeout = setTimeout(() => {
462                state.search = e.target.value;
463                state.currentPage = 1;
464                onUpdate();
465            }, 300);
466        });
467
468        pageSizeSelect.addEventListener('change', (e) => {
469            state.pageSize = parseInt(e.target.value);
470            state.currentPage = 1;
471            onUpdate();
472        });
473
474        toolbar.appendChild(searchContainer);
475        toolbar.appendChild(filterIndicator);
476        toolbar.appendChild(pageSizeSelect);
477        // toolbar.appendChild(columnButton);
478        toolbar.appendChild(filterButton);
479        toolbar.appendChild(selectionInfo);
480        
481        return toolbar;
482    }
483
484    function createToolbarButton(text, title, onclick) {
485        const button = document.createElement('button');
486        button.textContent = text;
487        button.title = title;
488        button.onclick = onclick;
489        Object.assign(button.style, {
490            padding: '8px 12px', backgroundColor: '#444', color: 'white',
491            border: '1px solid #555', borderRadius: '4px', cursor: 'pointer'
492        });
493        button.onmouseenter = () => button.style.backgroundColor = '#555';
494        button.onmouseleave = () => button.style.backgroundColor = '#444';
495        return button;
496    }
497
498    function createMainContent(state) {
499        const mainContent = document.createElement('div');
500        Object.assign(mainContent.style, {
501            display: 'flex', flexGrow: '1', overflow: 'hidden'
502        });
503
504        // Main table area
505        const tableArea = document.createElement('div');
506        Object.assign(tableArea.style, {
507            flexGrow: '1', display: 'flex', flexDirection: 'column',
508            overflow: 'hidden'
509        });
510
511        // Table container
512        const tableContainer = document.createElement('div');
513        tableContainer.className = 'table-container';
514        Object.assign(tableContainer.style, { 
515            flexGrow: '1', overflow: 'auto', position: 'relative'
516        });
517        
518        const table = document.createElement('table');
519        table.id = 'catalog-table';
520        Object.assign(table.style, {
521            width: '100%', borderCollapse: 'collapse'
522        });
523        
524        tableContainer.appendChild(table);
525
526        // Pagination controls
527        const pagination = createPaginationControls(state, scheduleLoad);
528
529        tableArea.appendChild(tableContainer);
530        tableArea.appendChild(pagination);
531
532        mainContent.appendChild(tableArea);
533        
534        return mainContent;
535    }
536
537    function createPaginationControls(state, onUpdate) {
538        const pagination = document.createElement('div');
539        pagination.className = 'pagination-controls';
540        Object.assign(pagination.style, {
541            padding: '10px 15px', borderTop: '1px solid #444',
542            display: 'flex', justifyContent: 'space-between', alignItems: 'center',
543            backgroundColor: '#333', flexShrink: '0'
544        });
545
546        // Navigation buttons
547        const navContainer = document.createElement('div');
548        Object.assign(navContainer.style, { display: 'flex', gap: '5px', alignItems: 'center' });
549
550        const firstBtn = createPaginationButton('<<', () => { state.currentPage = 1; onUpdate(); });
551        const prevBtn = createPaginationButton('<', () => { if (state.currentPage > 1) { state.currentPage--; onUpdate(); } });
552        
553        // Page input
554        const pageInput = document.createElement('input');
555        pageInput.type = 'number';
556        pageInput.min = '1';
557        pageInput.value = state.currentPage;
558        Object.assign(pageInput.style, {
559            width: '60px', padding: '5px', backgroundColor: '#444', color: 'white',
560            border: '1px solid #555', borderRadius: '3px', textAlign: 'center'
561        });
562        pageInput.addEventListener('change', (e) => {
563            const newPage = parseInt(e.target.value);
564            const maxPage = Math.ceil(state.totalItems / state.pageSize);
565            if (newPage >= 1 && newPage <= maxPage) {
566                state.currentPage = newPage;
567                onUpdate();
568            }
569        });
570
571        const pageInfo = document.createElement('span');
572        pageInfo.className = 'page-info';
573        Object.assign(pageInfo.style, { margin: '0 10px', fontSize: '14px' });
574
575        const nextBtn = createPaginationButton('>', () => { 
576            const maxPage = Math.ceil(state.totalItems / state.pageSize);
577            if (state.currentPage < maxPage) { state.currentPage++; onUpdate(); }
578        });
579        const lastBtn = createPaginationButton('>>', () => { 
580            state.currentPage = Math.ceil(state.totalItems / state.pageSize); 
581            onUpdate(); 
582        });
583
584        navContainer.appendChild(firstBtn);
585        navContainer.appendChild(prevBtn);
586        navContainer.appendChild(pageInput);
587        navContainer.appendChild(pageInfo);
588        navContainer.appendChild(nextBtn);
589        navContainer.appendChild(lastBtn);
590
591        // Items info and selection actions
592        const rightContainer = document.createElement('div');
593        Object.assign(rightContainer.style, { display: 'flex', gap: '15px', alignItems: 'center' });
594
595        const itemsInfo = document.createElement('span');
596        itemsInfo.className = 'items-info';
597        Object.assign(itemsInfo.style, { fontSize: '12px', color: '#aaa' });
598
599        const selectionActions = document.createElement('div');
600        selectionActions.className = 'selection-actions';
601        Object.assign(selectionActions.style, { display: 'none', gap: '5px' });
602
603        rightContainer.appendChild(itemsInfo);
604        rightContainer.appendChild(selectionActions);
605
606        pagination.appendChild(navContainer);
607        pagination.appendChild(rightContainer);
608
609        return pagination;
610    }
611
612    function createPaginationButton(text, onclick) {
613        const button = document.createElement('button');
614        button.textContent = text;
615        button.onclick = onclick;
616        Object.assign(button.style, {
617            padding: '5px 10px', backgroundColor: '#444', color: 'white',
618            border: '1px solid #555', borderRadius: '3px', cursor: 'pointer'
619        });
620        return button;
621    }
622
623    function createStatusBar(state) {
624        const statusBar = document.createElement('div');
625        statusBar.className = 'status-bar';
626        Object.assign(statusBar.style, {
627            padding: '5px 15px', borderTop: '1px solid #444',
628            backgroundColor: '#2a2a2a', fontSize: '12px', color: '#aaa',
629            display: 'flex', justifyContent: 'space-between', alignItems: 'center'
630        });
631
632        const leftStatus = document.createElement('div');
633        const rightStatus = document.createElement('div');
634
635        statusBar.appendChild(leftStatus);
636        statusBar.appendChild(rightStatus);
637
638        return statusBar;
639    }
640
641    function renderTable(data, columns) {
642        const table = document.getElementById('catalog-table');
643        table.innerHTML = '';
644        
645        if (data.length === 0) {
646            const emptyRow = document.createElement('tr');
647            const emptyCell = document.createElement('td');
648            emptyCell.textContent = 'No data found';
649            emptyCell.colSpan = columns.length + 1;
650            Object.assign(emptyCell.style, { 
651                textAlign: 'center', padding: '20px', color: '#aaa'
652            });
653            emptyRow.appendChild(emptyCell);
654            table.appendChild(emptyRow);
655            return;
656        }
657
658        // Create header
659        const thead = document.createElement('thead');
660        const headerRow = document.createElement('tr');
661        Object.assign(headerRow.style, {
662            backgroundColor: '#3a3a3a', position: 'sticky', top: '0', zIndex: '1'
663        });
664
665        // Selection column header
666        const selectTh = document.createElement('th');
667        selectTh.style.width = '40px';
668        const selectAllCb = document.createElement('input');
669        selectAllCb.type = 'checkbox';
670        selectAllCb.onchange = (e) => toggleSelectAll(e.target.checked);
671        selectTh.appendChild(selectAllCb);
672        headerRow.appendChild(selectTh);
673
674        // Data columns
675        columns.forEach(col => {
676            const th = document.createElement('th');
677            
678            const headerContent = document.createElement('div');
679            Object.assign(headerContent.style, {
680                display: 'flex', justifyContent: 'space-between', alignItems: 'center'
681            });
682
683            const colName = document.createElement('span');
684            colName.textContent = col;
685            colName.style.cursor = 'pointer';
686            colName.onclick = () => {
687                if (state.sortBy === col) {
688                    state.sortOrder = state.sortOrder === 'asc' ? 'desc' : 'asc';
689                } else {
690                    state.sortBy = col;
691                    state.sortOrder = 'asc';
692                }
693                state.currentPage = 1;
694                scheduleLoad();
695            };
696
697            const colActions = document.createElement('div');
698            Object.assign(colActions.style, { display: 'flex', gap: '2px' });
699
700            // // Column info button
701            // const infoBtn = document.createElement('button');
702            // infoBtn.textContent = 'Info';
703            // infoBtn.title = 'Column Info';
704            // Object.assign(infoBtn.style, {
705            //     background: 'none', border: 'none', color: '#aaa',
706            //     fontSize: '12px', cursor: 'pointer', padding: '2px'
707            // });
708            // infoBtn.onclick = (e) => {
709            //     e.stopPropagation();
710            //     showColumnInfo(catalogName, col);
711            // };
712
713            // Filter button
714            const filterBtn = document.createElement('button');
715            filterBtn.textContent = 'v';
716            filterBtn.title = 'Filter Column';
717            Object.assign(filterBtn.style, {
718                background: 'none', border: 'none', color: '#aaa',
719                fontSize: '10px', cursor: 'pointer', padding: '2px'
720            });
721            filterBtn.onclick = (e) => {
722                e.stopPropagation();
723                showColumnFilter(col, state, scheduleLoad);
724            };
725
726            // colActions.appendChild(infoBtn);
727            // colActions.appendChild(filterBtn);
728
729            headerContent.appendChild(colName);
730            headerContent.appendChild(colActions);
731
732            // Add sort indicator
733            if (state.sortBy === col) {
734                const indicator = document.createElement('span');
735                indicator.textContent = state.sortOrder === 'asc' ? ' ^' : ' v';
736                colName.appendChild(indicator);
737            }
738
739            Object.assign(th.style, {
740                padding: '10px', textAlign: 'left', userSelect: 'none',
741                borderBottom: '2px solid #555', minWidth: '100px'
742            });
743
744            th.appendChild(headerContent);
745            headerRow.appendChild(th);
746        });
747        thead.appendChild(headerRow);
748        table.appendChild(thead);
749
750        // Create body with row selection
751        const tbody = document.createElement('tbody');
752        data.forEach((row, index) => {
753            const tr = document.createElement('tr');
754            tr.style.borderBottom = '1px solid #444';
755            
756            // Add hover effect
757            tr.onmouseenter = () => tr.style.backgroundColor = '#383838';
758            tr.onmouseleave = () => tr.style.backgroundColor = state.selectedRows.has(index) ? '#444' : 'transparent';
759
760            // Selection checkbox
761            const selectTd = document.createElement('td');
762            const selectCb = document.createElement('input');
763            selectCb.type = 'checkbox';
764            selectCb.checked = state.selectedRows.has(index);
765            selectCb.onchange = (e) => toggleRowSelection(index, e.target.checked);
766            selectTd.appendChild(selectCb);
767            tr.appendChild(selectTd);
768
769            // Data cells
770            columns.forEach(col => {
771                const td = document.createElement('td');
772                const value = row[col];
773                
774                // Format cell based on data type
775                if (typeof value === 'number') {
776                    td.textContent = formatNumber(value);
777                    td.style.textAlign = 'right';
778                } else {
779                    td.textContent = value || '';
780                }
781                
782                td.style.padding = '8px 10px';
783                
784                // Add double-click to view cell details
785                td.ondblclick = () => showCellDetails(col, value, row);
786                
787                tr.appendChild(td);
788            });
789            
790            tbody.appendChild(tr);
791        });
792        table.appendChild(tbody);
793
794        updateSelectionInfo();
795    }
796
797    function toggleSelectAll(checked) {
798        if (checked) {
799            for (let i = 0; i < state.data.length; i++) {
800                state.selectedRows.add(i);
801            }
802        } else {
803            state.selectedRows.clear();
804        }
805        renderTable(state.data, state.selectedColumns);
806    }
807
808    function toggleRowSelection(index, checked) {
809        if (checked) {
810            state.selectedRows.add(index);
811        } else {
812            state.selectedRows.delete(index);
813        }
814        updateSelectionInfo();
815    }
816
817    function updateSelectionInfo() {
818        const selectionInfo = document.querySelector('.selection-info');
819        const selectedCount = state.selectedRows.size;
820        
821        if (selectedCount > 0) {
822            selectionInfo.textContent = `${selectedCount} row${selectedCount === 1 ? '' : 's'} selected`;
823            selectionInfo.style.color = '#4CAF50';
824        } else {
825            selectionInfo.textContent = '';
826        }
827
828        // Show/hide selection actions
829        const selectionActions = document.querySelector('.selection-actions');
830        if (selectedCount > 0) {
831            selectionActions.style.display = 'flex';
832            selectionActions.innerHTML = `
833                <button onclick="exportSelectedRows()" style="padding: 3px 8px; background: #4CAF50; color: white; border: none; border-radius: 3px; cursor: pointer;">Export Selected</button>
834                <button onclick="clearSelection()" style="padding: 3px 8px; background: #f44336; color: white; border: none; border-radius: 3px; cursor: pointer;">Clear</button>
835            `;
836        } else {
837            selectionActions.style.display = 'none';
838        }
839    }
840
841    function formatNumber(value) {
842        if (value === null || value === undefined) return '';
843        if (Math.abs(value) < 0.001 || Math.abs(value) >= 1000000) {
844            return value.toExponential(3);
845        }
846        return value.toFixed(3);
847    }
848
849    function updatePaginationInfo(pagination) {
850        const totalPages = pagination.total_pages;
851        const currentPage = pagination.page;
852        
853        // Update page input
854        const pageInput = document.querySelector('.pagination-controls input[type="number"]');
855        if (pageInput) pageInput.value = currentPage;
856        
857        // Update page info
858        const pageInfo = document.querySelector('.page-info');
859        if (pageInfo) pageInfo.textContent = `of ${totalPages}`;
860        
861        // Update items info
862        const itemsInfo = document.querySelector('.items-info');
863        if (itemsInfo) itemsInfo.textContent = `${pagination.showing_start}-${pagination.showing_end}
863 of ${pagination.total_items.toLocaleString()}`;
864        
865        // Update button states
866        const paginationContainer = document.querySelector('.pagination-controls');
867        if (paginationContainer) {
868            const buttons = paginationContainer.querySelectorAll('button');
869            if (buttons.length >= 4) {
870                buttons[0].disabled = !pagination.has_prev; // First
871                buttons[1].disabled = !pagination.has_prev; // Previous
872                buttons[2].disabled = !pagination.has_next; // Next
873                buttons[3].disabled = !pagination.has_next; // Last
874            }
875        }
876
877        // Update status bar
878        const statusBar = document.querySelector('.status-bar div:first-child');
879        if (statusBar) {
880            statusBar.textContent = `Page ${currentPage}/${totalPages} • ${pagination.total_items.toLocaleString()} total rows`;
881        }
882    }
883
884    function updateStatsDisplay(stats) {
885        if (!stats) return;
886        
887        const statusBar = document.querySelector('.status-bar div:last-child');
888        if (statusBar) {
889            const numericCols = Object.values(stats).filter(s => s.type === 'numeric').length;
890            const categoricalCols = Object.values(stats).filter(s => s.type === 'categorical').length;
891            statusBar.textContent = `${numericCols} numeric, ${categoricalCols} categorical columns`;
892        }
893    }
894
895    function updateLoadingState(loading) {
896        const table = document.getElementById('catalog-table');
897        
898        if (loading) {
899            // Add loading overlay
900            let loadingOverlay = document.getElementById('loading-overlay');
901            if (!loadingOverlay) {
902                loadingOverlay = document.createElement('div');
903                loadingOverlay.id = 'loading-overlay';
904                Object.assign(loadingOverlay.style, {
905                    position: 'absolute', top: '0', left: '0', right: '0', bottom: '0',
906                    backgroundColor: 'rgba(45, 45, 45, 0.8)', zIndex: '100',
907                    display: 'flex', alignItems: 'center', justifyContent: 'center'
908                });
909                
910                const spinner = document.createElement('div');
911                Object.assign(spinner.style, {
912                    width: '40px', height: '40px',
913                    border: '4px solid #555', borderTop: '4px solid #4CAF50',
914                    borderRadius: '50%', animation: 'spin 1s linear infinite'
915                });
916                
917                loadingOverlay.appendChild(spinner);
918                table.parentNode.style.position = 'relative';
919                table.parentNode.appendChild(loadingOverlay);
920                
921                // Add animation if not exists
922                if (!document.getElementById('spin-animation')) {
923                    const style = document.createElement('style');
924                    style.id = 'spin-animation';
925                    style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
926                    document.head.appendChild(style);
927                }
928            }
929        } else {
930            const loadingOverlay = document.getElementById('loading-overlay');
931            if (loadingOverlay) {
932                loadingOverlay.remove();
933            }
934        }
935    }
936
937
938    async function showColumnInfo(catalogName, columnName) {
939        try {
940            showNotification(`Loading column analysis for ${columnName}...`, 2000, 'info');
941            
942            const catalogNameForApi = catalogName.startsWith('catalogs/') 
943                ? catalogName.replace('catalogs/', '') 
944                : catalogName;
945            
946            const response = await fetch(`/catalog-column-analysis/${encodeURIComponent(catalogNameForApi)}/${encodeURIComponent(columnName)}`);
947            const analysis = await response.json();
948            
949            createDialog('column-info-dialog', `Column: ${columnName}`, (content) => {
950                content.innerHTML = `
951                    <div style="padding: 20px; max-height: 500px; overflow-y: auto;">
952                        <h3 style="margin-top: 0; color: #4CAF50;">Basic Information</h3>
953                        <table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
954                            <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Column:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.column_name}</td></tr>
955                            <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Data Type:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.dtype}</td></tr>
956                            <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Unit:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.unit || 'None'}</td></tr>
957                            <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Total Rows:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">
957${analysis.total_rows.toLocaleString()}</td></tr>
958                            <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Sample Size:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.sample_size.toLocaleString()}</td></tr>
959                        </table>
960                        
961                        ${analysis.numeric_stats ? `
962                            <h3 style="color: #4CAF50;">Numeric Statistics</h3>
963                            <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 20px;">
964                                <table style="border-collapse: collapse;">
965                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Count:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.count.toLocaleString()}</td></tr>
966                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Null:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.null_count.toLocaleString()}</td></tr>
967                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Min:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.min.toFixed(6)}</td></tr>
968                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Max:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.max.toFixed(6)}</td></tr>
969                                </table>
970                                <table style="border-collapse: collapse;">
971                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Mean:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.mean.toFixed(6)}</td></tr>
972                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Median:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.median.toFixed(6)}</td></tr>
973                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Std Dev:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.numeric_stats.std.toFixed(6)}</td></tr>
974                                    <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Range:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${(analysis.numeric_stats.max - analysis.numeric_stats.min).toFixed(6)}</td></tr>
975                                </table>
976                            </div>
977                        ` : ''}
978                        
979                        ${analysis.categorical_stats ? `
980                            <h3 style="color: #4CAF50;">Categorical Statistics</h3>
981                            <table style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
982                                <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Unique Values:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.categorical_stats.unique_count}</td></tr>
983                                <tr><td style="padding: 5px; border-bottom: 1px solid #444;"><strong>Diversity Index:</strong></td><td style="padding: 5px; border-bottom: 1px solid #444;">${analysis.categorical_stats.diversity_index.toFixed(3)}</td></tr>
984                            </table>
985                            
986                            <h4 style="color: #4CAF50;">Most Common Values</h4>
987                            <div style="max-height: 200px; overflow-y: auto; border: 1px solid #444; border-radius: 4px;">
988                                <table style="width: 100%; border-collapse: collapse;">
989                                    <thead style="position: sticky; top: 0; background: #3a3a3a;">
990                                        <tr>
991                                            <th style="padding: 8px; text-align: left; border-bottom: 1px solid #444;">Value</th>
992                                            <th style="padding: 8px; text-align: right; border-bottom: 1px solid #444;">Count</th>
993                                            <th style="padding: 8px; text-align: right; border-bottom: 1px solid #444;">%</th>
994                                        </tr>
995                                    </thead>
996                                    <tbody>
997                                        ${analysis.categorical_stats.most_common.map(item => `
998                                            <tr>
999                                                <td style="padding: 5px; border-bottom: 1px solid #444; font-family: monospace;">${item.value}</td>
1000                                                <td style="padding: 5px; border-bottom: 1px solid #444; text-align: right;">${item.count}</td>
1001                                                <td style="padding: 5px; border-bottom: 1px solid #444; text-align: right;">${((item.count / analysis.sample_size) * 100).toFixed(1)}%</td>
1002                                            </tr>
1003                                        `).join('')}
1004                                    </tbody>
1005                                </table>
1006                            </div>
1007                        ` : ''}
1008                    </div>
1009                `;
1010            });
1011            
1012        } catch (error) {
1013            showNotification(`Error loading column analysis: ${error.message}`, 4000, 'error');
1014        }
1015    }
1016
1017    function showColumnFilter(columnName, state, onUpdate) {
1018        const columnMeta = state.metadata.columns.find(col => col.name === columnName);
1019        const isNumeric = columnMeta && columnMeta.is_numeric;
1020        
1021        createDialog('column-filter-dialog', `Filter: ${columnName}`, (content) => {
1022            content.innerHTML = `
1023                <div style="padding: 20px;">
1024                    <div style="margin-bottom: 15px;">
1025                        <label style="display: block; margin-bottom: 5px; font-weight: bold;">Filter Type:</label>
1026                        <select id="filter-type" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1027                            <option value="contains">Contains</option>
1028                            <option value="equals">Equals</option>
1029                            ${isNumeric ? `
1030                                <option value="greater_than">Greater Than</option>
1031                                <option value="less_than">Less Than</option>
1032                                <option value="range">Range</option>
1033                            ` : ''}
1034                        </select>
1035                    </div>
1036                    
1037                    <div id="filter-value-container">
1038                        <label style="display: block; margin-bottom: 5px; font-weight: bold;">Value:</label>
1039                        <input type="text" id="filter-value" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1040                    </div>
1041                    
1042                    <div id="range-container" style="display: none; margin-top: 15px;">
1043                        <label style="display: block; margin-bottom: 5px; font-weight: bold;">Range:</label>
1044                        <div style="display: flex; gap: 10px; align-items: center;">
1045                            <input type="number" id="range-min" placeholder="Min" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1046                            <span>to</span>
1047                            <input type="number" id="range-max" placeholder="Max" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1048                        </div>
1049                    </div>
1050                    
1051                    <div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;">
1052                        <button onclick="clearColumnFilter('${columnName}')" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Clear Filter</button>
1053                        <button onclick="applyColumnFilter('${columnName}')" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Apply Filter</button>
1054                    </div>
1055                </div>
1056                
1057                <script>
1058                    document.getElementById('filter-type').addEventListener('change', function() {
1059                        const rangeContainer = document.getElementById('range-container');
1060                        const valueContainer = document.getElementById('filter-value-container');
1061                        if (this.value === 'range') {
1062                            rangeContainer.style.display = 'block';
1063                            valueContainer.style.display = 'none';
1064                        } else {
1065                            rangeContainer.style.display = 'none';
1066                            valueContainer.style.display = 'block';
1067                        }
1068                    });
1069                    
1070                    const existingFilter = ${JSON.stringify(state.activeFilters[columnName] || null)};
1071                    if (existingFilter) {
1072                        document.getElementById('filter-type').value = existingFilter.type;
1073                        if (existingFilter.type === 'range') {
1074                            document.getElementById('range-min').value = existingFilter.min || '';
1075                            document.getElementById('range-max').value = existingFilter.max || '';
1076                            document.getElementById('range-container').style.display = 'block';
1077                            document.getElementById('filter-value-container').style.display = 'none';
1078                        } else {
1079                            document.getElementById('filter-value').value = existingFilter.value || '';
1080                        }
1081                    }
1082                </script>
1083            `;
1084        });
1085        
1086        window.applyColumnFilter = function(columnName) {
1087            const filterType = document.getElementById('filter-type').value;
1088            
1089            if (filterType === 'range') {
1090                const min = document.getElementById('range-min').value;
1091                const max = document.getElementById('range-max').value;
1092                if (min || max) {
1093                    state.activeFilters[columnName] = { type: 'range', min: min || null, max: max || null };
1094                }
1095            } else {
1096                const value = document.getElementById('filter-value').value.trim();
1097                if (value) {
1098                    state.activeFilters[columnName] = { type: filterType, value: value };
1099                }
1100            }
1101            
1102            state.currentPage = 1;
1103            document.getElementById('column-filter-dialog').remove();
1104            updateFilterIndicator();
1105            onUpdate();
1106        };
1107        
1108        window.clearColumnFilter = function(columnName) {
1109            delete state.activeFilters[columnName];
1110            state.currentPage = 1;
1111            document.getElementById('column-filter-dialog').remove();
1112            updateFilterIndicator();
1113            onUpdate();
1114        };
1115    }
1116
1117    function showAdvancedSearchDialog(state, onUpdate) {
1118        createDialog('advanced-search-dialog', 'Advanced Search', (content) => {
1119            content.innerHTML = `
1120                <div style="padding: 20px; width: 600px;">
1121                    <div style="margin-bottom: 15px;">
1122                        <label style="display: block; margin-bottom: 5px; font-weight: bold;">Search Mode:</label>
1123                        <select id="search-mode" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1124                            <option value="simple">Simple (contains)</option>
1125                            <option value="regex">Regular Expression</option>
1126                            <option value="exact">Exact Match</option>
1127                        </select>
1128                    </div>
1129                    
1130                    <div style="margin-bottom: 15px;">
1131                        <label style="display: block; margin-bottom: 5px; font-weight: bold;">Search Term:</label>
1132                        <input type="text" id="search-term" placeholder="Enter search term..." style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1133                    </div>
1134                    
1135                    <div style="margin-bottom: 15px;">
1136                        <label style="display: block; margin-bottom: 5px; font-weight: bold;">Search In Columns:</label>
1137                        <div style="max-height: 200px; overflow-y: auto; border: 1px solid #555; border-radius: 4px; padding: 10px; background: #444;">
1138                            ${state.columns.map(col => `
1139                                <label style="display: block; margin-bottom: 5px;">
1140                                    <input type="checkbox" value="${col}" checked style="margin-right: 8px;">
1141                                    ${col}
1142                                </label>
1143                            `).join('')}
1144                        </div>
1145                    </div>
1146                    
1147                    <div style="margin-bottom: 15px;">
1148                        <label style="display: flex; align-items: center;">
1149                            <input type="checkbox" id="case-sensitive" style="margin-right: 8px;">
1150                            Case Sensitive
1151                        </label>
1152                    </div>
1153                    
1154                    <div style="display: flex; gap: 10px; justify-content: flex-end;">
1155                        <button onclick="document.getElementById('advanced-search-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
1156                        <button onclick="applyAdvancedSearch()" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Search</button>
1157                    </div>
1158                </div>
1159            `;
1160        });
1161        
1162        window.applyAdvancedSearch = function() {
1163            const searchTerm = document.getElementById('search-term').value.trim();
1164            if (searchTerm) {
1165                state.search = searchTerm;
1166                state.currentPage = 1;
1167                onUpdate();
1168            }
1169            document.getElementById('advanced-search-dialog').remove();
1170        };
1171    }
1172
1173    function showColumnSelector(state, onUpdate) {
1174        createDialog('column-selector-dialog', 'Select Columns', (content) => {
1175            content.innerHTML = `
1176                <div style="padding: 20px;">
1177                    <div style="margin-bottom: 15px; display: flex; gap: 10px;">
1178                        <button id="select-all-btn" style="padding: 6px 12px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Select All</button>
1179                        <button id="select-none-btn" style="padding: 6px 12px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Select None</button>
1180                        <button id="select-numeric-btn" style="padding: 6px 12px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Numeric Only</button>
1181                    </div>
1182                    
1183                    <div style="max-height: 400px; overflow-y: auto; border: 1px solid #555; border-radius: 4px; padding: 10px; background: #444;">
1184                        ${state.metadata.columns.map(col => `
1185                            <label style="display: flex; align-items: center; margin-bottom: 8px; padding: 5px; border-radius: 4px;" 
1186                                   class="column-option" onmouseenter="this.style.background='#555'" onmouseleave="this.style.background='transparent'">
1187                                <input type="checkbox" value="${col.name}" ${state.selectedColumns.includes(col.name) ? 'checked' : ''} 
1188                                       style="margin-right: 10px;" class="column-checkbox">
1189                                <div style="flex: 1;">
1190                                    <div style="font-weight: bold; color: ${col.is_numeric ? '#4CAF50' : col.is_boolean ? '#FF9800' : '#2196F3'};">
1191                                        ${col.name}
1192                                    </div>
1193                                    <div style="font-size: 12px; color: #aaa;">
1194                                        ${col.dtype} ${col.unit ? `(${col.unit})` : ''}
1195                                    </div>
1196                                </div>
1197                            </label>
1198                        `).join('')}
1199                    </div>
1200                    
1201                    <div style="margin-top: 15px; padding: 10px; background: #333; border-radius: 4px;">
1202                        <div style="font-size: 12px; color: #aaa;">Selected: <span id="selected-count">0</span> columns</div>
1203                    </div>
1204                    
1205                    <div style="margin-top: 20px; display: flex; gap: 10px; justify-content: flex-end;">
1206                        <button onclick="document.getElementById('column-selector-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
1207                        <button id="apply-selection-btn" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Apply</button>
1208                    </div>
1209                </div>
1210            `;
1211            
1212            // Add event listeners after content is added
1213            setTimeout(() => {
1214                function updateColumnPreview() {
1215                    const checked = document.querySelectorAll('.column-checkbox:checked');
1216                    document.getElementById('selected-count').textContent = checked.length;
1217                }
1218                
1219                document.getElementById('select-all-btn').onclick = function() {
1220                    document.querySelectorAll('.column-checkbox').forEach(cb => cb.checked = true);
1221                    updateColumnPreview();
1222                };
1223                
1224                document.getElementById('select-none-btn').onclick = function() {
1225                    document.querySelectorAll('.column-checkbox').forEach(cb => cb.checked = false);
1226                    updateColumnPreview();
1227                };
1228                
1229                document.getElementById('select-numeric-btn').onclick = function() {
1230                    document.querySelectorAll('.column-checkbox').forEach(cb => {
1231                        const colMeta = state.metadata.columns.find(col => col.name === cb.value);
1232                        cb.checked = colMeta && colMeta.is_numeric;
1233                    });
1234                    updateColumnPreview();
1235                };
1236                
1237                document.querySelectorAll('.column-checkbox').forEach(cb => {
1238                    cb.addEventListener('change', updateColumnPreview);
1239                });
1240                
1241                document.getElementById('apply-selection-btn').onclick = function() {
1242                    const selectedColumns = Array.from(document.querySelectorAll('.column-checkbox:checked'))
1243                        .map(cb => cb.value);
1244                    
1245                    if (selectedColumns.length === 0) {
1246                        alert('Please select at least one column.');
1247                        return;
1248                    }
1249                    
1250                    state.selectedColumns = selectedColumns;
1251                    state.currentPage = 1;
1252                    document.getElementById('column-selector-dialog').remove();
1253                    onUpdate();
1254                };
1255                
1256                updateColumnPreview();
1257            }, 50);
1258        });
1259    }
1260
1261    function showFilterPanel(state, onUpdate) {
1262        createDialog('filter-panel', 'Advanced Filters', (content) => {
1263            content.innerHTML = `
1264                <div style="padding: 20px;">
1265                    <div style="margin-bottom: 20px;">
1266                        <h3 style="margin-top: 0; color: #4CAF50;">Active Filters</h3>
1267                        <div id="active-filters" style="min-height: 100px; border: 1px solid #555; border-radius: 4px; padding: 10px; background: #444;">
1268                            ${Object.keys(state.activeFilters).length === 0 ? 
1269                                '<div style="color: #aaa; text-align: center; padding: 20px;">No active filters</div>' :
1270                                Object.entries(state.activeFilters).map(([col, filter]) => `
1271                                    <div style="background: #333; padding: 8px; margin-bottom: 5px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
1272                                        <span><strong>${col}:</strong> ${filter.type} "${filter.value || (filter.min || '') + ' - ' + (filter.max || '')}"</span>
1273                                        <button onclick="removeFilter('${col}')" style="background: #f44336; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer;">×</button>
1274                                    </div>
1275                                `).join('')
1276                            }
1277                        </div>
1278                    </div>
1279                    
1280                    <div style="margin-bottom: 20px;">
1281                        <h3 style="color: #4CAF50;">Add New Filter</h3>
1282                        <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px;">
1283                            <div>
1284                                <label style="display: block; margin-bottom: 5px;">Column:</label>
1285                                <select id="filter-column" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1286                                    <option value="">Select column...</option>
1287                                    ${state.columns.map(col => `<option value="${col}">${col}</option>`).join('')}
1288                                </select>
1289                            </div>
1290                            <div>
1291                                <label style="display: block; margin-bottom: 5px;">Operation:</label>
1292                                <select id="filter-operation" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1293                                    <option value="contains">Contains</option>
1294                                    <option value="equals">Equals</option>
1295                                    <option value="greater_than">Greater Than</option>
1296                                    <option value="less_than">Less Than</option>
1297                                    <option value="range">Range</option>
1298                                </select>
1299                            </div>
1300                        </div>
1301                        
1302                        <div id="filter-value-input" style="margin-bottom: 10px;">
1303                            <label style="display: block; margin-bottom: 5px;">Value:</label>
1304                            <input type="text" id="filter-value" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1305                        </div>
1306                        
1307                        <div id="filter-range-input" style="display: none; margin-bottom: 10px;">
1308                            <label style="display: block; margin-bottom: 5px;">Range:</label>
1309                            <div style="display: flex; gap: 10px;">
1310                                <input type="number" id="filter-min" placeholder="Min" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1311                                <input type="number" id="filter-max" placeholder="Max" style="flex: 1; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1312                            </div>
1313                        </div>
1314                        
1315                        <button id="add-filter-btn" style="width: 100%; padding: 10px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Add Filter</button>
1316                    </div>
1317                    
1318                    <div style="display: flex; gap: 10px; justify-content: flex-end;">
1319                        <button id="clear-all-btn" style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Clear All</button>
1320                        <button onclick="document.getElementById('filter-panel').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button>
1321                    </div>
1322                </div>
1323            `;
1324            
1325            // Add event listeners after content is added
1326            setTimeout(() => {
1327                document.getElementById('filter-operation').addEventListener('change', function() {
1328                    const valueInput = document.getElementById('filter-value-input');
1329                    const rangeInput = document.getElementById('filter-range-input');
1330                    if (this.value === 'range') {
1331                        valueInput.style.display = 'none';
1332                        rangeInput.style.display = 'block';
1333                    } else {
1334                        valueInput.style.display = 'block';
1335                        rangeInput.style.display = 'none';
1336                    }
1337                });
1338                
1339                document.getElementById('add-filter-btn').onclick = function() {
1340                    const column = document.getElementById('filter-column').value;
1341                    const operation = document.getElementById('filter-operation').value;
1342                    
1343                    if (!column) {
1344                        alert('Please select a column');
1345                        return;
1346                    }
1347                    
1348                    if (operation === 'range') {
1349                        const min = document.getElementById('filter-min').value;
1350                        const max = document.getElementById('filter-max').value;
1351                        if (!min && !max) {
1352                            alert('Please enter min or max value');
1353                            return;
1354                        }
1355                        state.activeFilters[column] = { type: 'range', min: min || null, max: max || null };
1356                    } else {
1357                        const value = document.getElementById('filter-value').value.trim();
1358                        if (!value) {
1359                            alert('Please enter a value');
1360                            return;
1361                        }
1362                        state.activeFilters[column] = { type: operation, value: value };
1363                    }
1364                    
1365                    state.currentPage = 1;
1366                    document.getElementById('filter-panel').remove();
1367                    updateFilterIndicator();
1368                    onUpdate();
1369                };
1370                
1371                document.getElementById('clear-all-btn').onclick = function() {
1372                    state.activeFilters = {};
1373                    state.currentPage = 1;
1374                    document.getElementById('filter-panel').remove();
1375                    updateFilterIndicator();
1376                    onUpdate();
1377                };
1378                
1379                window.removeFilter = function(columnName) {
1380                    delete state.activeFilters[columnName];
1381                    state.currentPage = 1;
1382                    document.getElementById('filter-panel').remove();
1383                    updateFilterIndicator();
1384                    onUpdate();
1385                };
1386            }, 50);
1387        });
1388    }
1389
1390    function showSettingsDialog(state, onUpdate) {
1391        createDialog('settings-dialog', 'Settings', (content) => {
1392            content.innerHTML = `
1393                <div style="padding: 20px;">
1394                    <div style="margin-bottom: 20px;">
1395                        <h3 style="margin-top: 0; color: #4CAF50;">Display Settings</h3>
1396                        
1397                        <div style="margin-bottom: 15px;">
1398                            <label style="display: block; margin-bottom: 5px;">Decimal Places:</label>
1399                            <input type="number" id="decimal-places" min="0" max="10" value="3" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1400                        </div>
1401                        
1402                        <div style="margin-bottom: 15px;">
1403                            <label style="display: flex; align-items: center;">
1404                                <input type="checkbox" id="show-units" checked style="margin-right: 8px;">
1405                                Show Column Units
1406                            </label>
1407                        </div>
1408                        
1409                        <div style="margin-bottom: 15px;">
1410                            <label style="display: flex; align-items: center;">
1411                                <input type="checkbox" id="highlight-selection" checked style="margin-right: 8px;">
1412                                Highlight Selected Rows
1413                            </label>
1414                        </div>
1415                    </div>
1416                    
1417                    <div style="margin-bottom: 20px;">
1418                        <h3 style="color: #4CAF50;">Performance Settings</h3>
1419                        
1420                        <div style="margin-bottom: 15px;">
1421                            <label style="display: block; margin-bottom: 5px;">Default Page Size:</label>
1422                            <select id="default-page-size" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1423                                <option value="100">100 rows</option>
1424                                <option value="500">500 rows</option>
1425                                <option value="1000" selected>1000 rows</option>
1426                                <option value="2000">2000 rows</option>
1427                            </select>
1428                        </div>
1429                    </div>
1430                    
1431                    <div style="margin-bottom: 20px;">
1432                        <h3 style="color: #4CAF50;">Export Settings</h3>
1433                        
1434                        <div style="margin-bottom: 15px;">
1435                            <label style="display: block; margin-bottom: 5px;">Default Export Format:</label>
1436                            <select id="export-format" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1437                                <option value="csv">CSV</option>
1438                                <option value="tsv">TSV</option>
1439                            </select>
1440                        </div>
1441                        
1442                        <div style="margin-bottom: 15px;">
1443                            <label style="display: flex; align-items: center;">
1444                                <input type="checkbox" id="include-metadata" checked style="margin-right: 8px;">
1445                                Include Metadata in Export
1446                            </label>
1447                        </div>
1448                    </div>
1449                    
1450                    <div style="display: flex; gap: 10px; justify-content: flex-end;">
1451                        <button onclick="resetSettings()" style="padding: 8px 16px; background: #f44336; color: white; border: none; border-radius: 4px; cursor: pointer;">Reset to Defaults</button>
1452                        <button onclick="document.getElementById('settings-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
1453                        <button onclick="applySettings()" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Apply</button>
1454                    </div>
1455                </div>
1456            `;
1457        });
1458        
1459        window.applySettings = function() {
1460            showNotification('Settings applied successfully', 2000, 'success');
1461            document.getElementById('settings-dialog').remove();
1462        };
1463        
1464        window.resetSettings = function() {
1465            document.getElementById('decimal-places').value = '3';
1466            document.getElementById('show-units').checked = true;
1467            document.getElementById('highlight-selection').checked = true;
1468            document.getElementById('default-page-size').value = '100';
1469            document.getElementById('export-format').value = 'csv';
1470            document.getElementById('include-metadata').checked = true;
1471        };
1472    }
1473
1474    function showExportDialog(state) {
1475        createDialog('export-dialog', 'Export Data', (content) => {
1476            content.innerHTML = `
1477                <div style="padding: 20px;">
1478                    <div style="margin-bottom: 20px;">
1479                        <h3 style="margin-top: 0; color: #4CAF50;">Export Options</h3>
1480                        
1481                        <div style="margin-bottom: 15px;">
1482                            <label style="display: block; margin-bottom: 5px; font-weight: bold;">Export Scope:</label>
1483                            <div>
1484                                <label style="display: block; margin-bottom: 5px;">
1485                                    <input type="radio" name="export-scope" value="current" checked style="margin-right: 8px;">
1486                                    Current Page (${state.data.length} rows)
1487                                </label>
1488                                <label style="display: block; margin-bottom: 5px;">
1489                                    <input type="radio" name="export-scope" value="filtered" style="margin-right: 8px;">
1490                                    All Filtered Data (${state.totalItems.toLocaleStr
1490ing()} rows)
1491                                </label>
1492                                <label style="display: block; margin-bottom: 5px;">
1493                                    <input type="radio" name="export-scope" value="selected" ${state.selectedRows.size === 0 ? 'disabled' : ''} style="margin-right: 8px;">
1494                                    Selected Rows (${state.selectedRows.size} rows)
1495                                </label>
1496                            </div>
1497                        </div>
1498                        
1499                        <div style="margin-bottom: 15px;">
1500                            <label style="display: block; margin-bottom: 5px; font-weight: bold;">Export Format:</label>
1501                            <select id="export-format" style="width: 100%; padding: 8px; background: #444; color: white; border: 1px solid #555; border-radius: 4px;">
1502                                <option value="csv">CSV (Comma Separated Values)</option>
1503                                <option value="tsv">TSV (Tab Separated Values)</option>
1504                            </select>
1505                        </div>
1506                        
1507                        <div style="margin-bottom: 15px;">
1508                            <label style="display: block; margin-bottom: 5px; font-weight: bold;">Columns to Export:</label>
1509                            <div>
1510                                <label style="display: block; margin-bottom: 5px;">
1511                                    <input type="radio" name="export-columns" value="visible" checked style="margin-right: 8px;">
1512                                    Visible Columns (${state.selectedColumns.length} columns)
1513                                </label>
1514                                <label style="display: block; margin-bottom: 5px;">
1515                                    <input type="radio" name="export-columns" value="all" style="margin-right: 8px;">
1516                                    All Columns (${state.columns.length} columns)
1517                                </label>
1518                            </div>
1519                        </div>
1520                        
1521                        <div style="margin-bottom: 15px;">
1522                            <label style="display: flex; align-items: center;">
1523                                <input type="checkbox" id="include-headers" checked style="margin-right: 8px;">
1524                                Include Column Headers
1525                            </label>
1526                        </div>
1527                    </div>
1528                    
1529                    <div style="background: #333; padding: 15px; border-radius: 4px; margin-bottom: 20px;">
1530                        <div style="font-size: 14px; font-weight: bold; margin-bottom: 5px;">Export Preview:</div>
1531                        <div id="export-preview" style="font-size: 12px; color: #aaa;">
1532                            Will export current page (${state.data.length} rows) with visible columns (${state.selectedColumns.length} columns) in CSV format
1533                        </div>
1534                    </div>
1535                    
1536                    <div style="display: flex; gap: 10px; justify-content: flex-end;">
1537                        <button onclick="document.getElementById('export-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Cancel</button>
1538                        <button onclick="performExport()" style="padding: 8px 16px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Export</button>
1539                    </div>
1540                </div>
1541            `;
1542        });
1543        
1544        window.performExport = function() {
1545            const scope = document.querySelector('input[name="export-scope"]:checked').value;
1546            const format = document.getElementById('export-format').value;
1547            const columnMode = document.querySelector('input[name="export-columns"]:checked').value;
1548            const includeHeaders = document.getElementById('include-headers').checked;
1549            
1550            let exportData = [];
1551            let exportColumns = [];
1552            
1553            // Determine data to export
1554            switch(scope) {
1555                case 'current':
1556                    exportData = state.data;
1557                    break;
1558                case 'selected':
1559                    exportData = Array.from(state.selectedRows).map(index => state.data[index]);
1560                    break;
1561                case 'filtered':
1562                    alert('Exporting all filtered data requires server-side export. This would be implemented with an API call.');
1563                    return;
1564            }
1565            
1566            // Determine columns to export
1567            switch(columnMode) {
1568                case 'visible':
1569                    exportColumns = state.selectedColumns;
1570                    break;
1571                case 'all':
1572                    exportColumns = state.columns;
1573                    break;
1574            }
1575            
1576            // Perform export
1577            try {
1578                let content = '';
1579                const delimiter = format === 'csv' ? ',' : '\t';
1580                
1581                if (includeHeaders) {
1582                    content += exportColumns.join(delimiter) + '\n';
1583                }
1584                
1585                exportData.forEach(row => {
1586                    const values = exportColumns.map(col => {
1587                        let val = row[col] || '';
1588                        // Escape quotes and wrap in quotes if contains delimiter
1589                        if (typeof val === 'string' && (val.includes(delimiter) || val.includes('"') || val.includes('\n'))) {
1590                            val = '"' + val.replace(/"/g, '""') + '"';
1591                        }
1592                        return val;
1593                    });
1594                    content += values.join(delimiter) + '\n';
1595                });
1596                
1597                // Download file
1598                const blob = new Blob([content], { type: 'text/plain' });
1599                const url = URL.createObjectURL(blob);
1600                const a = document.createElement('a');
1601                a.href = url;
1602                a.download = catalogName.split('/').pop().replace('.fits', '') + '_export.' + format;
1603                document.body.appendChild(a);
1604                a.click();
1605                document.body.removeChild(a);
1606                URL.revokeObjectURL(url);
1607                
1608                showNotification('Export completed successfully!', 3000, 'success');
1609                document.getElementById('export-dialog').remove();
1610                
1611            } catch (error) {
1612                showNotification('Export failed: ' + error.message, 4000, 'error');
1613            }
1614        };
1615    }
1616
1617    function showCellDetails(column, value, row) {
1618        createDialog('cell-details-dialog', `Cell Details: ${column}`, (content) => {
1619            content.innerHTML = `
1620                <div style="padding: 20px;">
1621                    <div style="margin-bottom: 20px;">
1622                        <h3 style="margin-top: 0; color: #4CAF50;">Value Information</h3>
1623                        <table style="width: 100%; border-collapse: collapse;">
1624                            <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Column:</td><td style="padding: 8px; border-bottom: 1px solid #444; font-family: monospace;">${column}</td></tr>
1625                            <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Value:</td><td style="padding: 8px; border-bottom: 1px solid #444; font-family: monospace;">${value}</td></tr>
1626                            <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Type:</td><td style="padding: 8px; border-bottom: 1px solid #444;">${typeof value}</td></tr>
1627                            <tr><td style="padding: 8px; border-bottom: 1px solid #444; font-weight: bold;">Length:</td><td style="padding: 8px; border-bottom: 1px solid #444;">${String(value).length} characters</td></tr>
1628                        </table>
1629                    </div>
1630                    
1631                    <div style="margin-bottom: 20px;">
1632                        <h3 style="color: #4CAF50;">Formatted Views</h3>
1633                        <div style="background: #333; padding: 15px; border-radius: 4px;">
1634                            <div style="margin-bottom: 10px;">
1635                                <strong>Raw:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">${JSON.stringify(value)}</code>
1636                            </div>
1637                            ${typeof value === 'number' ? `
1638                                <div style="margin-bottom: 10px;">
1639                                    <strong>Scientific:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">${value.toExponential(6)}</code>
1640                                </div>
1641                                <div style="margin-bottom: 10px;">
1642                                    <strong>Fixed:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">${value.toFixed(6)}</code>
1643                                </div>
1644                            ` : ''}
1645                            <div>
1646                                <strong>String:</strong> <code style="background: #444; padding: 2px 4px; border-radius: 2px;">"${String(value)}"</code>
1647                            </div>
1648                        </div>
1649                    </div>
1650                    
1651                    <div style="margin-bottom: 20px;">
1652                        <h3 style="color: #4CAF50;">Row Context</h3>
1653                        <div style="max-height: 200px; overflow-y: auto; background: #333; padding: 15px; border-radius: 4px; font-family: monospace; font-size: 12px;">
1654                            <pre style="margin: 0; white-space: pre-wrap;">${JSON.stringify(row, null, 2)}</pre>
1655                        </div>
1656                    </div>
1657                    
1658                    <div style="display: flex; gap: 10px; justify-content: flex-end;">
1659                        <button onclick="copyCellValue('${String(value).replace(/'/g, "\\'")}', this)" style="padding: 8px 16px; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Copy Value</button>
1660                        <button onclick="document.getElementById('cell-details-dialog').remove()" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Close</button>
1661                    </div>
1662                </div>
1663            `;
1664        });
1665        
1666        window.copyCellValue = function(value, button) {
1667            navigator.clipboard.writeText(String(value)).then(() => {
1668                const originalText = button.textContent;
1669                button.textContent = 'Copied!';
1670                button.style.background = '#4CAF50';
1671                setTimeout(() => {
1672                    button.textContent = originalText;
1673                    button.style.background = '#2196F3';
1674                }, 1000);
1675            }).catch(err => {
1676                showNotification('Failed to copy value', 2000, 'error');
1677            });
1678        };
1679    }
1680
1681    // Utility function to create modal dialogs
1682    function createDialog(id, title, contentCallback) {
1683        // Remove existing dialog if any
1684        const existing = document.getElementById(id);
1685        if (existing) {
1686            existing.remove();
1687        }
1688        
1689        // Create overlay
1690        const overlay = document.createElement('div');
1691        overlay.id = id;
1692        Object.assign(overlay.style, {
1693            position: 'fixed', top: '0', left: '0', right: '0', bottom: '0',
1694            // Must be above #catalog-viewer-popup (z-index ~3705)
1695            backgroundColor: 'rgba(0, 0, 0, 0.7)', zIndex: '3805',
1696            display: 'flex', alignItems: 'center', justifyContent: 'center'
1697        });
1698        
1699        // Create dialog
1700        const dialog = document.createElement('div');
1701        Object.assign(dialog.style, {
1702            backgroundColor: '#2d2d2d', borderRadius: '8px',
1703            border: '1px solid #444', maxWidth: '80vw', maxHeight: '80vh',
1704            color: 'white', fontFamily: 'Arial, sans-serif',
1705            boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8)',
1706            overflow: 'hidden', display: 'flex', flexDirection: 'column'
1707        });
1708        
1709        // Create header
1710        const header = document.createElement('div');
1711        Object.assign(header.style, {
1712            padding: '15px 20px', borderBottom: '1px solid #444',
1713            backgroundColor: '#333', display: 'flex',
1714            justifyContent: 'space-between', alignItems: 'center'
1715        });
1716        
1717        const titleElement = document.createElement('h3');
1718        titleElement.textContent = title;
1719        titleElement.style.margin = '0';
1720        
1721        const closeButton = document.createElement('button');
1722        closeButton.innerHTML = '×';
1723        Object.assign(closeButton.style, {
1724            background: 'none', border: 'none', color: 'white',
1725            fontSize: '24px', cursor: 'pointer', fontWeight: 'bold'
1726        });
1727        closeButton.onclick = () => overlay.remove();
1728        
1729        header.appendChild(titleElement);
1730        header.appendChild(closeButton);
1731        
1732        // Create content container
1733        const content = document.createElement('div');
1734        Object.assign(content.style, {
1735            overflow: 'auto', flexGrow: '1'
1736        });
1737        
1738        dialog.appendChild(header);
1739        dialog.appendChild(content);
1740        overlay.appendChild(dialog);
1741        
1742        // Close on overlay click
1743        overlay.onclick = (e) => {
1744            if (e.target === overlay) {
1745                overlay.remove();
1746            }
1747        };
1748        
1749        // Close on Escape key
1750        const handleEscape = (e) => {
1751            if (e.key === 'Escape') {
1752                overlay.remove();
1753                document.removeEventListener('keydown', handleEscape);
1754            }
1755        };
1756        document.addEventListener('keydown', handleEscape);
1757        
1758        document.body.appendChild(overlay);
1759        
1760        // Call content callback
1761        contentCallback(content);
1762        
1763        return { overlay, dialog, content };
1764    }
1765
1766    // Update filter indicator
1767    function updateFilterIndicator() {
1768        const filterIndicator = document.querySelector('.filter-indicator');
1769        if (filterIndicator) {
1770            const activeFilterCount = Object.keys(state.activeFilters).length;
1771            
1772            if (activeFilterCount > 0) {
1773                filterIndicator.textContent = `${activeFilterCount} filter${activeFilterCount === 1 ? '' : 's'} active`;
1774                filterIndicator.style.display = 'block';
1775            } else {
1776                filterIndicator.style.display = 'none';
1777            }
1778        }
1779    }
1780
1781    // Enhanced notification function
1782    function showNotification(message, duration = 3000, type = 'info') {
1783        // Remove existing notifications
1784        const existing = document.querySelectorAll('.notification');
1785        existing.forEach(n => n.remove());
1786        
1787        if (message === true || message === false) {
1788            // Handle loading states
1789            if (message === true) {
1790                const loader = document.createElement('div');
1791                loader.className = 'notification loading';
1792                loader.innerHTML = `
1793                    <div style="display: flex; align-items: center; gap: 10px;">
1794                        <div style="width: 20px; height: 20px; border: 2px solid #4CAF50; border-top: 2px solid transparent; border-radius: 50%; animation: spin 1s linear infinite;"></div>
1795                        <span>Loading...</span>
1796                    </div>
1797                `;
1798                Object.assign(loader.style, {
1799                    position: 'fixed', top: '20px', right: '20px', zIndex: '4000',
1800                    backgroundColor: '#333', color: 'white', padding: '15px 20px',
1801                    borderRadius: '6px', border: '1px solid #555',
1802                    boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
1803                    fontFamily: 'Arial, sans-serif'
1804                });
1805                
1806                // Add spin animation if not exists
1807                if (!document.getElementById('spin-animation')) {
1808                    const style = document.createElement('style');
1809                    style.id = 'spin-animation';
1810                    style.textContent = '@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
1811                    document.head.appendChild(style);
1812                }
1813                
1814                document.body.appendChild(loader);
1815            }
1816            return;
1817        }
1818        
1819        const notification = document.createElement('div');
1820        notification.className = 'notification';
1821        notification.textContent = message;
1822        
1823        const colors = {
1824            info: '#2196F3',
1825            success: '#4CAF50',
1826            warning: '#FF9800',
1827            error: '#f44336'
1828        };
1829        
1830        Object.assign(notification.style, {
1831            position: 'fixed', top: '20px', right: '20px', zIndex: '4000',
1832            backgroundColor: colors[type] || colors.info, color: 'white',
1833            padding: '15px 20px', borderRadius: '6px',
1834            boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
1835            fontFamily: 'Arial, sans-serif', maxWidth: '400px',
1836            transform: 'translateX(100%)', transition: 'transform 0.3s ease'
1837        });
1838        
1839        document.body.appendChild(notification);
1840        
1841        // Animate in
1842        setTimeout(() => {
1843            notification.style.transform = 'translateX(0)';
1844        }, 10);
1845        
1846        // Auto remove
1847        if (duration > 0) {
1848            setTimeout(() => {
1849                notification.style.transform = 'translateX(100%)';
1850                setTimeout(() => {
1851                    if (notification.parentNode) {
1852                        notification.remove();
1853                    }
1854                }, 300);
1855            }, duration);
1856        }
1857        
1858        // Click to dismiss
1859        notification.onclick = () => {
1860            notification.style.transform = 'translateX(100%)';
1861            setTimeout(() => notification.remove(), 300);
1862        };
1863    }
1864    
1865    // Global functions for selection actions
1866    window.exportSelectedRows = function() {
1867        if (state.selectedRows.size === 0) {
1868            showNotification('No rows selected for export', 2000, 'warning');
1869            return;
1870        }
1871        
1872        showExportDialog(state);
1873        
1874        // Set the selected scope after dialog creation
1875        setTimeout(() => {
1876            const selectedRadio = document.querySelector('input[name="export-scope"][value="selected"]');
1877            if (selectedRadio) {
1878                selectedRadio.checked = true;
1879            }
1880        }, 100);
1881    };
1882
1883    window.clearSelection = function() {
1884        state.selectedRows.clear();
1885        renderTable(state.data, state.selectedColumns);
1886        showNotification('Selection cleared', 1500, 'info');
1887    };
1888
1889    // Initialize filter indicator
1890    updateFilterIndicator();
1891}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.