← back to NEW SKU Viewer

public/index.html

887 lines

<!DOCTYPE html>
<html lang="en">
<head>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg">

    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>NEW-SKU Vendor Viewer | Live Product Scanner</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            color: #333;
        }

        .header {
            background: rgba(255, 255, 255, 0.95);
            backdrop-filter: blur(10px);
            padding: 20px 0;
            box-shadow: 0 2px 20px rgba(0,0,0,0.1);
        }

        .container {
            max-width: 1400px;
            margin: 0 auto;
            padding: 0 20px;
        }

        .header-content {
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 20px;
        }

        .title {
            color: #2d3748;
        }

        .title h1 {
            font-size: 2.5rem;
            margin-bottom: 5px;
            background: linear-gradient(45deg, #667eea, #764ba2);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }

        .title p {
            color: #666;
            font-size: 1.1rem;
        }

        .controls {
            display: flex;
            gap: 15px;
            align-items: center;
            flex-wrap: wrap;
        }

        .stats {
            display: flex;
            gap: 20px;
            margin: 20px 0;
            flex-wrap: wrap;
        }

        .stat-card {
            background: rgba(255, 255, 255, 0.9);
            padding: 15px 25px;
            border-radius: 12px;
            text-align: center;
            box-shadow: 0 4px 15px rgba(0,0,0,0.1);
            backdrop-filter: blur(10px);
            min-width: 120px;
        }

        .stat-value {
            font-size: 2rem;
            font-weight: bold;
            color: #667eea;
        }

        .stat-label {
            color: #666;
            font-size: 0.9rem;
            margin-top: 5px;
        }

        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-weight: 500;
            transition: all 0.3s ease;
            text-decoration: none;
            display: inline-flex;
            align-items: center;
            gap: 8px;
        }

        .btn-primary {
            background: linear-gradient(45deg, #667eea, #764ba2);
            color: white;
        }

        .btn-secondary {
            background: rgba(255, 255, 255, 0.9);
            color: #667eea;
            border: 2px solid #667eea;
        }

        .btn:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 15px rgba(0,0,0,0.2);
        }

        .btn:disabled {
            opacity: 0.6;
            cursor: not-allowed;
            transform: none;
        }

        .content {
            padding: 20px 0;
        }

        .vendor-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(var(--card-min, 350px), 1fr));
            gap: 20px;
            margin-top: 20px;
        }

        .grid-controls {
            display: flex;
            gap: 12px;
            align-items: center;
            flex-wrap: wrap;
            margin-bottom: 16px;
            padding: 10px 14px;
            background: rgba(255,255,255,0.85);
            backdrop-filter: blur(10px);
            border-radius: 10px;
        }

        .grid-controls label {
            font-size: 0.85rem;
            color: #4a5568;
            font-weight: 500;
        }

        .grid-controls select {
            padding: 6px 10px;
            border-radius: 6px;
            border: 1px solid #cbd5e0;
            background: #fff;
            font-size: 0.9rem;
            color: #2d3748;
        }

        .grid-controls input[type="range"] {
            width: 160px;
        }

        .vendor-card {
            background: rgba(255, 255, 255, 0.95);
            border-radius: 12px;
            padding: 20px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
            backdrop-filter: blur(10px);
            transition: all 0.3s ease;
            border-left: 4px solid transparent;
        }

        .vendor-card:hover {
            transform: translateY(-5px);
            box-shadow: 0 8px 30px rgba(0,0,0,0.15);
        }

        .vendor-card.success {
            border-left-color: #10b981;
        }

        .vendor-card.error {
            border-left-color: #ef4444;
        }

        .vendor-card.warning {
            border-left-color: #f59e0b;
        }

        .vendor-header {
            display: flex;
            justify-content: space-between;
            align-items: flex-start;
            margin-bottom: 15px;
        }

        .vendor-name {
            font-size: 1.2rem;
            font-weight: 600;
            color: #2d3748;
            margin-bottom: 5px;
        }

        .vendor-id {
            color: #666;
            font-size: 0.9rem;
            font-family: monospace;
        }

        .status-badge {
            padding: 4px 8px;
            border-radius: 6px;
            font-size: 0.8rem;
            font-weight: 500;
            text-transform: uppercase;
        }

        .status-success {
            background: #d1fae5;
            color: #065f46;
        }

        .status-error {
            background: #fee2e2;
            color: #991b1b;
        }

        .status-warning {
            background: #fef3c7;
            color: #92400e;
        }

        .product-count {
            font-size: 2rem;
            font-weight: bold;
            margin: 10px 0;
        }

        .product-count.has-products {
            color: #10b981;
        }

        .product-count.no-products {
            color: #6b7280;
        }

        .vendor-url {
            color: #666;
            font-size: 0.9rem;
            word-break: break-all;
            margin-bottom: 10px;
        }

        .error-message {
            color: #ef4444;
            font-size: 0.9rem;
            margin-top: 10px;
            padding: 8px;
            background: #fee2e2;
            border-radius: 6px;
        }

        .vendor-actions {
            display: flex;
            gap: 10px;
            margin-top: 15px;
        }

        .btn-small {
            padding: 6px 12px;
            font-size: 0.8rem;
        }

        .loading {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 60px 20px;
            color: white;
        }

        .spinner {
            width: 50px;
            height: 50px;
            border: 3px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            border-top-color: white;
            animation: spin 1s ease-in-out infinite;
            margin-bottom: 20px;
        }

        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        .last-update {
            color: rgba(255,255,255,0.8);
            font-size: 0.9rem;
            margin-top: 10px;
        }

        .filters {
            display: flex;
            gap: 10px;
            margin-bottom: 20px;
            flex-wrap: wrap;
        }

        .filter-btn {
            padding: 8px 16px;
            border: 2px solid rgba(255,255,255,0.3);
            background: rgba(255,255,255,0.1);
            color: white;
            border-radius: 20px;
            cursor: pointer;
            transition: all 0.3s ease;
        }

        .filter-btn.active {
            background: rgba(255,255,255,0.9);
            color: #667eea;
        }

        .search-box {
            margin-bottom: 20px;
        }

        .search-input {
            width: 100%;
            max-width: 400px;
            padding: 12px 20px;
            border: none;
            border-radius: 25px;
            background: rgba(255,255,255,0.9);
            backdrop-filter: blur(10px);
            font-size: 1rem;
            outline: none;
        }

        @media (max-width: 768px) {
            .header-content {
                flex-direction: column;
                text-align: center;
            }
            
            .title h1 {
                font-size: 2rem;
            }
            
            .vendor-grid {
                grid-template-columns: 1fr;
            }
            
            .stats {
                justify-content: center;
            }
        }
    </style>
</head>
<body>
    <div class="header">
        <div class="container">
            <div class="header-content">
                <div class="title">
                    <h1><i class="fas fa-search"></i> NEW-SKU Vendor Viewer</h1>
                    <p>Live product scanner for all wallcovering vendors</p>
                </div>
                <div class="controls">
                    <button class="btn btn-primary" onclick="startScan()" id="scanBtn">
                        <i class="fas fa-sync"></i> Scan All Vendors
                    </button>
                    <button class="btn btn-secondary" onclick="refreshData()">
                        <i class="fas fa-refresh"></i> Refresh
                    </button>
                </div>
            </div>
        </div>
    </div>

    <div class="container">
        <div class="stats" id="stats">
            <div class="stat-card">
                <div class="stat-value" id="totalVendors">-</div>
                <div class="stat-label">Total Vendors</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="successfulVendors">-</div>
                <div class="stat-label">Working</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="vendorsWithProducts">-</div>
                <div class="stat-label">With Products</div>
            </div>
            <div class="stat-card">
                <div class="stat-value" id="totalProducts">-</div>
                <div class="stat-label">Total Products</div>
            </div>
        </div>

        <div class="content">
            <div class="search-box">
                <input type="text" class="search-input" placeholder="Search vendors..." id="searchInput" oninput="filterVendors()">
            </div>

            <div class="filters">
                <div class="filter-btn active" onclick="setFilter('all')">All Vendors</div>
                <div class="filter-btn" onclick="setFilter('success')">Working</div>
                <div class="filter-btn" onclick="setFilter('with-products')">With Products</div>
                <div class="filter-btn" onclick="setFilter('errors')">Errors</div>
            </div>

            <div class="grid-controls">
                <label for="sortSelect">Sort:</label>
                <select id="sortSelect" onchange="onSortChange()">
                    <option value="newest">Newest</option>
                    <option value="products">Most Products</option>
                    <option value="vendor">Vendor (SKU prefix) A→Z</option>
                    <option value="series">Series / Name A→Z</option>
                    <option value="sku">SKU A→Z</option>
                    <option value="title">Title A→Z</option>
                    <option value="status">Status (working first)</option>
                </select>
                <label for="densityRange" style="margin-left:12px;">Density:</label>
                <input type="range" id="densityRange" min="220" max="520" step="10" value="350" oninput="onDensityChange()">
                <span id="densityVal" style="font-size:0.8rem;color:#666;min-width:48px;">350px</span>
            </div>

            <div id="content">
                <div class="loading">
                    <div class="spinner"></div>
                    <h3>Loading vendor data...</h3>
                    <p>Scanning all NEW-SKU scrapers for available products</p>
                </div>
            </div>
        </div>
    </div>

    <script>
        let vendorData = {};
        let currentFilter = 'all';
        let searchQuery = '';

        // --- sort + density persistence (rule: hydrate BEFORE first API call) ---
        const LS_SORT = 'newSkuViewer.grid.sort';
        const LS_DENSITY = 'newSkuViewer.grid.density';

        function getSavedSort() {
            try { return localStorage.getItem(LS_SORT) || 'newest'; } catch (e) { return 'newest'; }
        }
        function getSavedDensity() {
            try { return parseInt(localStorage.getItem(LS_DENSITY) || '350', 10) || 350; } catch (e) { return 350; }
        }

        function hydrateGridControls() {
            const savedSort = getSavedSort();
            const savedDensity = getSavedDensity();
            const sortEl = document.getElementById('sortSelect');
            const rangeEl = document.getElementById('densityRange');
            const valEl = document.getElementById('densityVal');
            if (sortEl) sortEl.value = savedSort;
            if (rangeEl) rangeEl.value = String(savedDensity);
            if (valEl) valEl.textContent = savedDensity + 'px';
            document.documentElement.style.setProperty('--card-min', savedDensity + 'px');
        }

        function onSortChange() {
            const v = document.getElementById('sortSelect').value;
            try { localStorage.setItem(LS_SORT, v); } catch (e) {}
            loadVendorData();
        }

        function onDensityChange() {
            const v = parseInt(document.getElementById('densityRange').value, 10) || 350;
            try { localStorage.setItem(LS_DENSITY, String(v)); } catch (e) {}
            document.getElementById('densityVal').textContent = v + 'px';
            document.documentElement.style.setProperty('--card-min', v + 'px');
        }

        async function loadVendorData() {
            try {
                const sort = encodeURIComponent(getSavedSort());
                const response = await fetch('/api/vendors?sort=' + sort);
                const data = await response.json();
                
                vendorData = data;
                updateStats(data.stats);
                renderVendors(data.vendors);
                
                if (data.lastUpdate) {
                    updateLastUpdateTime(data.lastUpdate);
                }
                
                // Update scan button state
                const scanBtn = document.getElementById('scanBtn');
                if (data.isScanning) {
                    scanBtn.disabled = true;
                    scanBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Scanning...';
                } else {
                    scanBtn.disabled = false;
                    scanBtn.innerHTML = '<i class="fas fa-sync"></i> Scan All Vendors';
                }
                
            } catch (error) {
                console.error('Error loading vendor data:', error);
                document.getElementById('content').innerHTML = 
                    '<div class="error">Failed to load vendor data. Please try again.</div>';
            }
        }

        function updateStats(stats) {
            document.getElementById('totalVendors').textContent = stats.total || 0;
            document.getElementById('successfulVendors').textContent = stats.successful || 0;
            document.getElementById('vendorsWithProducts').textContent = stats.withProducts || 0;
            document.getElementById('totalProducts').textContent = stats.totalProducts || 0;
        }

        function renderVendors(vendors) {
            const filteredVendors = filterVendorData(vendors);
            
            if (Object.keys(filteredVendors).length === 0) {
                document.getElementById('content').innerHTML = 
                    '<div style="text-align: center; padding: 40px; color: white;"><h3>No vendors found matching your criteria</h3></div>';
                return;
            }

            // Server already sorted via ?sort= — preserve that order.
            const vendorCards = Object.entries(filteredVendors)
                .map(([vendorId, vendor]) => createVendorCard(vendorId, vendor))
                .join('');

            document.getElementById('content').innerHTML = 
                `<div class="vendor-grid">${vendorCards}</div>`;
        }

        function createVendorCard(vendorId, vendor) {
            const statusClass = vendor.success ? 
                (vendor.products > 0 ? 'success' : 'warning') : 'error';
            
            const statusBadge = vendor.success ? 
                (vendor.products > 0 ? 
                    `<span class="status-badge status-success">Active</span>` : 
                    `<span class="status-badge status-warning">No Products</span>`) :
                `<span class="status-badge status-error">Error</span>`;

            const productCountClass = vendor.products > 0 ? 'has-products' : 'no-products';
            
            const viewProductsBtn = vendor.products > 0 ? 
                `<button class="btn btn-primary btn-small" onclick="viewProducts('${vendorId}')">
                    <i class="fas fa-eye"></i> View Products
                </button>` : '';
                
            const importBtn = vendor.products > 0 ? 
                `<button class="btn btn-secondary btn-small" onclick="importProducts('${vendorId}')">
                    <i class="fas fa-download"></i> Import
                </button>` : '';

            return `
                <div class="vendor-card ${statusClass}">
                    <div class="vendor-header">
                        <div>
                            <div class="vendor-name">${vendor.vendorName || vendorId}</div>
                            <div class="vendor-id">${vendorId}</div>
                        </div>
                        ${statusBadge}
                    </div>
                    
                    <div class="product-count ${productCountClass}">
                        ${vendor.products || 0} products
                    </div>
                    
                    <div class="vendor-url">
                        <i class="fas fa-link"></i> 
                        ${vendor.url ? vendor.url.substring(0, 50) + '...' : 'No URL'}
                    </div>
                    
                    ${vendor.scraperUsed ? 
                        `<div style="font-size: 0.8rem; color: #666; margin-top: 5px;">
                            <i class="fas fa-robot"></i> ${vendor.scraperUsed}
                        </div>` : ''}
                    
                    ${vendor.error ? `<div class="error-message">${vendor.error}</div>` : ''}
                    
                    <div class="vendor-actions">
                        <button class="btn btn-secondary btn-small" onclick="testVendor('${vendorId}')">
                            <i class="fas fa-refresh"></i> Retest
                        </button>
                        ${viewProductsBtn}
                        ${importBtn}
                    </div>
                </div>
            `;
        }

        function filterVendorData(vendors) {
            let filtered = { ...vendors };
            
            // Apply filter
            if (currentFilter !== 'all') {
                filtered = Object.fromEntries(
                    Object.entries(filtered).filter(([id, vendor]) => {
                        switch (currentFilter) {
                            case 'success':
                                return vendor.success;
                            case 'with-products':
                                return vendor.products > 0;
                            case 'errors':
                                return !vendor.success;
                            default:
                                return true;
                        }
                    })
                );
            }
            
            // Apply search
            if (searchQuery) {
                const query = searchQuery.toLowerCase();
                filtered = Object.fromEntries(
                    Object.entries(filtered).filter(([id, vendor]) => 
                        id.toLowerCase().includes(query) ||
                        (vendor.vendorName && vendor.vendorName.toLowerCase().includes(query))
                    )
                );
            }
            
            return filtered;
        }

        function setFilter(filter) {
            currentFilter = filter;
            
            // Update filter buttons
            document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
            event.target.classList.add('active');
            
            renderVendors(vendorData.vendors || {});
        }

        function filterVendors() {
            searchQuery = document.getElementById('searchInput').value;
            renderVendors(vendorData.vendors || {});
        }

        async function startScan() {
            try {
                const response = await fetch('/api/scan', { method: 'POST' });
                const result = await response.json();
                
                if (result.success) {
                    document.getElementById('scanBtn').disabled = true;
                    document.getElementById('scanBtn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> Scanning...';
                    
                    // Poll for updates every 5 seconds during scan
                    const pollInterval = setInterval(async () => {
                        await loadVendorData();
                        if (!vendorData.isScanning) {
                            clearInterval(pollInterval);
                        }
                    }, 5000);
                }
            } catch (error) {
                console.error('Error starting scan:', error);
            }
        }

        async function testVendor(vendorId) {
            try {
                const response = await fetch(`/api/test/${vendorId}`, { method: 'POST' });
                const result = await response.json();
                
                // Update the vendor in our local data
                if (vendorData.vendors) {
                    vendorData.vendors[vendorId] = result;
                    renderVendors(vendorData.vendors);
                }
            } catch (error) {
                console.error('Error testing vendor:', error);
            }
        }

        function viewProducts(vendorId) {
            const vendor = vendorData.vendors[vendorId];
            if (vendor && vendor.products > 0) {
                // Create a detailed product view modal
                showProductModal(vendor);
            } else {
                alert('No products available for this vendor');
            }
        }

        function showProductModal(vendor) {
            const modal = document.createElement('div');
            modal.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0,0,0,0.8);
                display: flex;
                justify-content: center;
                align-items: center;
                z-index: 1000;
            `;
            
            const content = document.createElement('div');
            content.style.cssText = `
                background: white;
                border-radius: 12px;
                padding: 30px;
                max-width: 800px;
                max-height: 80vh;
                overflow-y: auto;
                position: relative;
                box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            `;
            
            const sampleProducts = vendor.sampleProducts || [];
            
            content.innerHTML = `
                <button onclick="this.parentElement.parentElement.remove()" style="
                    position: absolute;
                    top: 15px;
                    right: 15px;
                    background: none;
                    border: none;
                    font-size: 24px;
                    cursor: pointer;
                    color: #666;
                ">×</button>
                
                <h2 style="color: #333; margin-bottom: 20px;">
                    <i class="fas fa-boxes"></i> ${vendor.vendorName || vendor.vendor} Products
                </h2>
                
                <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
                    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 15px; text-align: center;">
                        <div>
                            <div style="font-size: 24px; font-weight: bold; color: #667eea;">${vendor.products}</div>
                            <div style="font-size: 14px; color: #666;">Total Products</div>
                        </div>
                        <div>
                            <div style="font-size: 24px; font-weight: bold; color: #10b981;">${sampleProducts.length}</div>
                            <div style="font-size: 14px; color: #666;">Sample Shown</div>
                        </div>
                        <div>
                            <div style="font-size: 16px; font-weight: bold; color: #f59e0b;">✅</div>
                            <div style="font-size: 14px; color: #666;">${vendor.scraperUsed || 'Scraper'}</div>
                        </div>
                    </div>
                </div>
                
                ${sampleProducts.length > 0 ? `
                    <h3 style="color: #333; margin-bottom: 15px;">Sample Products (${sampleProducts.length} of ${vendor.products})</h3>
                    <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;">
                        ${sampleProducts.map((product, index) => `
                            <div style="
                                border: 1px solid #e5e7eb;
                                border-radius: 8px;
                                padding: 15px;
                                background: #fafafa;
                                transition: all 0.3s ease;
                            " onmouseover="this.style.transform='translateY(-2px)'; this.style.boxShadow='0 4px 12px rgba(0,0,0,0.1)'" 
                               onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='none'">
                                <div style="font-weight: bold; color: #333; margin-bottom: 8px; font-size: 14px;">
                                    ${product.title || product.name || `Product ${index + 1}`}
                                </div>
                                ${product.color && product.color !== 'Multiple Colors' ? `
                                    <div style="color: #667eea; font-size: 12px; margin-bottom: 8px;">
                                        <i class="fas fa-palette"></i> ${product.color}
                                    </div>
                                ` : ''}
                                <div style="font-family: monospace; font-size: 11px; color: #666; margin-bottom: 8px;">
                                    Code: ${product.code || 'N/A'}
                                </div>
                                <a href="${product.url}" target="_blank" rel="noopener noreferrer" style="
                                    display: inline-block;
                                    background: #667eea;
                                    color: white;
                                    text-decoration: none;
                                    padding: 6px 12px;
                                    border-radius: 4px;
                                    font-size: 12px;
                                    transition: background 0.3s ease;
                                " onmouseover="this.style.background='#5a6fd8'" onmouseout="this.style.background='#667eea'">
                                    <i class="fas fa-external-link-alt"></i> View Product
                                </a>
                            </div>
                        `).join('')}
                    </div>
                ` : `
                    <div style="text-align: center; padding: 40px; color: #666;">
                        <i class="fas fa-inbox" style="font-size: 48px; margin-bottom: 15px; color: #ccc;"></i>
                        <p>No product details available</p>
                    </div>
                `}
                
                <div style="margin-top: 20px; text-align: center; border-top: 1px solid #e5e7eb; padding-top: 20px;">
                    <button onclick="window.open('${vendor.url}', '_blank')" style="
                        background: #10b981;
                        color: white;
                        border: none;
                        padding: 10px 20px;
                        border-radius: 6px;
                        margin-right: 10px;
                        cursor: pointer;
                        font-weight: 500;
                    ">
                        <i class="fas fa-globe"></i> Visit Vendor Site
                    </button>
                    <button onclick="importProducts('${vendor.vendor}')" style="
                        background: #667eea;
                        color: white;
                        border: none;
                        padding: 10px 20px;
                        border-radius: 6px;
                        cursor: pointer;
                        font-weight: 500;
                    ">
                        <i class="fas fa-download"></i> Import to Shopify
                    </button>
                </div>
            `;
            
            modal.appendChild(content);
            document.body.appendChild(modal);
            
            // Close on background click
            modal.addEventListener('click', (e) => {
                if (e.target === modal) {
                    modal.remove();
                }
            });
        }

        function importProducts(vendorId) {
            const vendor = vendorData.vendors[vendorId];
            if (vendor && vendor.url) {
                const importUrl = `http://45.61.58.125:9830/bulk-import-new?vendor=${vendorId}&url=${encodeURIComponent(vendor.url)}`;
                window.open(importUrl, '_blank');
            } else {
                alert('No import URL available for this vendor');
            }
        }

        function refreshData() {
            loadVendorData();
        }

        function updateLastUpdateTime(timestamp) {
            const date = new Date(timestamp);
            const timeStr = date.toLocaleString();
            
            let updateElement = document.querySelector('.last-update');
            if (!updateElement) {
                updateElement = document.createElement('div');
                updateElement.className = 'last-update';
                document.querySelector('.container').appendChild(updateElement);
            }
            
            updateElement.textContent = `Last updated: ${timeStr}`;
        }

        // Initialize the application — hydrate sort/density from localStorage
        // BEFORE the first API call so the initial fetch uses the saved sort.
        hydrateGridControls();
        loadVendorData();

        // Refresh data every 30 seconds
        setInterval(loadVendorData, 30000);
    </script>
</body>
</html>