<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Component Dashboard — Cartographer</title>
    <style>
        *, *::before, *::after {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }

        body {
            font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
            background: #f3f4f6;
            color: #1f2937;
            min-height: 100vh;
            display: flex;
            flex-direction: column;
        }

        /* --- Filter Bar --- */
        .filter-bar {
            background: #ffffff;
            border-bottom: 1px solid #e5e7eb;
            padding: 12px 20px;
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            align-items: center;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
        }

        .filter-bar label {
            font-size: 12px;
            font-weight: 600;
            color: #6b7280;
            text-transform: uppercase;
            letter-spacing: 0.3px;
        }

        .filter-group {
            display: flex;
            flex-direction: column;
            gap: 3px;
        }

        .filter-bar input[type="text"],
        .filter-bar select {
            font-family: inherit;
            font-size: 13px;
            padding: 6px 10px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            background: #ffffff;
            color: #1f2937;
            transition: border-color 0.15s, box-shadow 0.15s;
        }

        .filter-bar input[type="text"] {
            min-width: 220px;
        }

        .filter-bar select {
            min-width: 160px;
        }

        .filter-bar input:focus,
        .filter-bar select:focus {
            outline: none;
            border-color: #3b82f6;
            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
        }

        .filter-bar .filter-actions {
            display: flex;
            align-items: flex-end;
            gap: 8px;
            margin-left: auto;
        }

        .btn {
            font-family: inherit;
            font-size: 13px;
            padding: 6px 14px;
            border-radius: 6px;
            border: 1px solid #d1d5db;
            background: #f9fafb;
            color: #374151;
            cursor: pointer;
            font-weight: 500;
            transition: background 0.15s, border-color 0.15s;
        }

        .btn:hover {
            background: #f3f4f6;
            border-color: #9ca3af;
        }

        .btn:focus {
            outline: none;
            box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
        }

        .btn-primary {
            background: #2563eb;
            color: #ffffff;
            border-color: #2563eb;
            font-weight: 600;
        }

        .btn-primary:hover {
            background: #1d4ed8;
        }

        .result-count {
            font-size: 13px;
            color: #6b7280;
            padding: 0 8px;
            white-space: nowrap;
            align-self: flex-end;
        }

        /* --- Grid --- */
        .grid-container {
            flex: 1;
            padding: 16px 20px;
            overflow-x: auto;
        }

        .data-grid {
            width: 100%;
            border-collapse: separate;
            border-spacing: 0;
            background: #ffffff;
            border-radius: 10px;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 2px 8px rgba(0, 0, 0, 0.03);
            overflow: hidden;
        }

        .data-grid thead th {
            background: #f9fafb;
            border-bottom: 2px solid #e5e7eb;
            padding: 10px 14px;
            text-align: left;
            font-size: 12px;
            font-weight: 700;
            color: #6b7280;
            text-transform: uppercase;
            letter-spacing: 0.4px;
            white-space: nowrap;
            cursor: pointer;
            user-select: none;
            position: relative;
        }

        .data-grid thead th:hover {
            background: #f3f4f6;
        }

        .data-grid thead th .sort-indicator {
            margin-left: 4px;
            font-size: 10px;
            color: #9ca3af;
        }

        .data-grid thead th .sort-indicator.active {
            color: #2563eb;
        }

        .data-grid tbody tr {
            cursor: pointer;
            transition: background 0.1s;
        }

        .data-grid tbody tr:hover {
            background: #eff6ff;
        }

        .data-grid tbody tr:not(:last-child) td {
            border-bottom: 1px solid #f3f4f6;
        }

        .data-grid tbody td {
            padding: 10px 14px;
            font-size: 13px;
            color: #374151;
            vertical-align: top;
        }

        .data-grid .col-name {
            font-weight: 600;
            color: #1f2937;
            max-width: 240px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .data-grid .col-summary {
            max-width: 320px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            color: #6b7280;
            font-size: 12px;
        }

        .badge {
            display: inline-block;
            padding: 2px 8px;
            border-radius: 12px;
            font-size: 11px;
            font-weight: 600;
            white-space: nowrap;
        }

        .badge-type {
            background: #e0e7ff;
            color: #3730a3;
        }

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

        .badge-status-crawled {
            background: #dbeafe;
            color: #1e40af;
        }

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

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

        .badge-status-removed {
            background: #f3f4f6;
            color: #6b7280;
        }

        .col-date {
            white-space: nowrap;
            color: #6b7280;
            font-size: 12px;
        }

        .col-entity {
            font-family: 'Consolas', 'Courier New', monospace;
            font-size: 12px;
            color: #6b7280;
        }

        /* --- Pagination --- */
        .pagination {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 12px 20px;
            background: #ffffff;
            border-top: 1px solid #e5e7eb;
        }

        .pagination-info {
            font-size: 13px;
            color: #6b7280;
        }

        .pagination-controls {
            display: flex;
            gap: 6px;
        }

        .pagination-controls button {
            font-family: inherit;
            font-size: 13px;
            padding: 5px 12px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            background: #ffffff;
            color: #374151;
            cursor: pointer;
        }

        .pagination-controls button:hover:not(:disabled) {
            background: #f3f4f6;
        }

        .pagination-controls button:disabled {
            color: #d1d5db;
            cursor: not-allowed;
        }

        .pagination-controls button.active {
            background: #2563eb;
            color: #ffffff;
            border-color: #2563eb;
        }

        /* --- States --- */
        .empty-state {
            text-align: center;
            padding: 60px 24px;
        }

        .empty-state-icon {
            font-size: 40px;
            color: #d1d5db;
            margin-bottom: 12px;
        }

        .empty-state h3 {
            font-size: 16px;
            color: #6b7280;
            font-weight: 600;
            margin-bottom: 6px;
        }

        .empty-state p {
            font-size: 13px;
            color: #9ca3af;
        }

        .loading-row td {
            text-align: center;
            padding: 40px !important;
            color: #6b7280;
        }

        .spinner {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 2px solid #e5e7eb;
            border-top-color: #3b82f6;
            border-radius: 50%;
            animation: spin 0.7s linear infinite;
            vertical-align: middle;
            margin-right: 8px;
        }

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

        .error-banner {
            background: #fef2f2;
            border: 1px solid #fecaca;
            color: #991b1b;
            padding: 10px 16px;
            font-size: 13px;
            border-radius: 8px;
            margin: 12px 20px 0;
            display: none;
        }

        /* --- Detail Panel --- */
        .panel-overlay {
            position: fixed; top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.3); z-index: 999; display: none;
            animation: fadeIn 0.15s ease;
        }
        .detail-panel {
            position: fixed; top: 0; right: 0; bottom: 0; width: 520px; max-width: 90vw;
            background: #fff; z-index: 1000; display: none;
            box-shadow: -4px 0 24px rgba(0,0,0,0.12);
            flex-direction: column; overflow: hidden;
            animation: slideIn 0.2s ease;
        }
        @keyframes slideIn { from { transform: translateX(100%); } to { transform: translateX(0); } }
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
        .panel-header {
            padding: 16px 20px; border-bottom: 1px solid #e5e7eb;
            display: flex; justify-content: space-between; align-items: center;
            background: #f9fafb;
        }
        .panel-header h2 { font-size: 16px; font-weight: 700; color: #111827; margin: 0; }
        .panel-close {
            background: none; border: none; font-size: 22px; cursor: pointer;
            color: #6b7280; padding: 4px 8px; border-radius: 4px; line-height: 1;
        }
        .panel-close:hover { background: #f3f4f6; color: #111827; }
        .panel-body { flex: 1; overflow-y: auto; padding: 20px; }
        .panel-section { margin-bottom: 20px; }
        .panel-section-title {
            font-size: 11px; font-weight: 700; text-transform: uppercase;
            letter-spacing: 0.5px; color: #6b7280; margin-bottom: 8px;
        }
        .panel-field { margin-bottom: 12px; }
        .panel-field-label { font-size: 11px; color: #9ca3af; margin-bottom: 2px; }
        .panel-field-value { font-size: 14px; color: #1f2937; }
        .panel-summary {
            font-size: 14px; line-height: 1.7; color: #374151;
            background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px;
            padding: 16px; white-space: pre-wrap;
        }
        .panel-summary.empty {
            background: #fefce8; border-color: #fde68a; color: #92400e;
            font-style: italic;
        }
        .panel-metadata {
            font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
            font-size: 11px; line-height: 1.5; color: #374151;
            background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px;
            padding: 12px; max-height: 300px; overflow-y: auto; white-space: pre-wrap;
            word-break: break-all;
        }
        .panel-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; margin-right: 6px; }
        .panel-actions { padding: 12px 20px; border-top: 1px solid #e5e7eb; display: flex; gap: 8px; background: #f9fafb; }
        .panel-actions button {
            font-family: inherit; font-size: 13px; padding: 7px 16px;
            border-radius: 6px; border: none; cursor: pointer; font-weight: 600;
        }
        .panel-btn-primary { background: #2563eb; color: #fff; }
        .panel-btn-primary:hover { background: #1d4ed8; }
        .panel-btn-secondary { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db !important; }
        .panel-btn-secondary:hover { background: #e5e7eb; }

        @media (max-width: 900px) {
            .filter-bar {
                padding: 10px 12px;
            }
            .filter-bar input[type="text"] {
                min-width: 160px;
            }
            .filter-bar select {
                min-width: 120px;
            }
            .grid-container {
                padding: 10px 12px;
            }
            .data-grid thead th,
            .data-grid tbody td {
                padding: 8px 10px;
            }
        }
    </style>
</head>
<body>
    <div class="filter-bar">
        <div class="filter-group">
            <label for="searchInput">Search</label>
            <input type="text" id="searchInput" placeholder="Search name or summary..." />
        </div>
        <div class="filter-group">
            <label for="solutionFilter">Solution</label>
            <select id="solutionFilter">
                <option value="">All Solutions</option>
            </select>
        </div>
        <div class="filter-group">
            <label for="typeFilter">Component Type</label>
            <select id="typeFilter">
                <option value="">All Types</option>
            </select>
        </div>
        <div class="filter-group">
            <label for="entityFilter">Entity</label>
            <select id="entityFilter">
                <option value="">All Entities</option>
            </select>
        </div>
        <div class="filter-actions">
            <span class="result-count" id="resultCount"></span>
            <button class="btn" id="btnReset">Reset</button>
        </div>
    </div>

    <div id="errorBanner" class="error-banner"></div>

    <div class="grid-container">
        <table class="data-grid">
            <thead>
                <tr>
                    <th data-field="vb_name">Name <span class="sort-indicator" id="sort-vb_name"></span></th>
                    <th data-field="vb_component_type">Type <span class="sort-indicator" id="sort-vb_component_type"></span></th>
                    <th data-field="vb_entity_logical_name">Entity <span class="sort-indicator" id="sort-vb_entity_logical_name"></span></th>
                    <th data-field="vb_crawl_status">Status <span class="sort-indicator" id="sort-vb_crawl_status"></span></th>
                    <th data-field="vb_ai_summary">AI Summary</th>
                    <th data-field="vb_crawled_on">Last Crawled <span class="sort-indicator" id="sort-vb_crawled_on"></span></th>
                </tr>
            </thead>
            <tbody id="gridBody">
                <tr class="loading-row">
                    <td colspan="6"><span class="spinner"></span>Loading components...</td>
                </tr>
            </tbody>
        </table>
    </div>

    <div class="pagination">
        <div class="pagination-info" id="paginationInfo">Loading...</div>
        <div class="pagination-controls" id="paginationControls"></div>
    </div>

    <!-- Detail Panel -->
    <div class="panel-overlay" id="panelOverlay"></div>
    <div class="detail-panel" id="detailPanel" style="display:none;">
        <div class="panel-header">
            <h2 id="panelTitle">Component</h2>
            <button class="panel-close" id="panelClose">&times;</button>
        </div>
        <div class="panel-body" id="panelBody"></div>
        <div class="panel-actions">
            <button class="panel-btn-primary" id="panelOpenRecord">Open in Maker Portal</button>
            <button class="panel-btn-secondary" id="panelGenerateAi">Generate AI Summary</button>
        </div>
    </div>

    <script>
        // Analytics - fire and forget
        function vbAnalytics(eventType, details) {
            try {
                fetch('/api/data/v9.2/vb_configurations?$select=vb_license_key,vb_org_id&$top=1', {
                    headers: { 'Accept': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' },
                    credentials: 'include'
                }).then(function(r) { return r.json(); }).then(function(d) {
                    if (!d.value || !d.value[0]) return;
                    var c = d.value[0];
                    fetch('https://cartographer-api.azurewebsites.net/api/analytics/event', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            licenseKey: c.vb_license_key,
                            orgId: c.vb_org_id || '',
                            eventType: eventType,
                            details: JSON.stringify(details || {}),
                            timestamp: new Date().toISOString()
                        })
                    });
                });
            } catch(e) { /* silent */ }
        }
        vbAnalytics('page_view', {page: 'component_dashboard'});

        (function () {
            'use strict';

            // --- Configuration ---
            var API_BASE = '/api/data/v9.2/';
            var PAGE_SIZE = 50;

            // --- Component type labels ---
            var COMPONENT_TYPE_LABELS = {
                10001001: 'Plugin Step',
                10001002: 'Cloud Flow (Automated)',
                10001003: 'Cloud Flow (Scheduled)',
                10001004: 'Cloud Flow (Instant)',
                10001005: 'Business Rule',
                10001006: 'Classic Workflow',
                10001007: 'Custom Action',
                10001008: 'Custom API',
                10001009: 'JS Web Resource',
                10001010: 'HTML Web Resource',
                10001011: 'CSS Web Resource',
                10001012: 'Form (Main)',
                10001013: 'Form (Quick Create)',
                10001014: 'Form (Quick View)',
                10001015: 'View (System)',
                10001016: 'Dashboard',
                10001017: 'Chart',
                10001018: 'Custom Page',
                10001019: 'Canvas App',
                10001020: 'Model-Driven App',
                10001021: 'Site Map',
                10001022: 'Security Role',
                10001023: 'BPF',
                10001024: 'Desktop Flow',
                10001025: 'Connection Reference',
                10001026: 'Environment Variable',
                10001027: 'Custom Connector',
                10001028: 'Table (Entity)',
                10001029: 'Column (Field)',
                10001030: 'Relationship',
                10001031: 'Option Set (Global)',
                10001032: 'Ribbon/Command Bar',
                10001033: 'Other'
            };

            // --- Crawl status labels ---
            var CRAWL_STATUS_LABELS = {
                10001000: 'Pending',
                10001001: 'Crawled',
                10001002: 'AI Summarized',
                10001003: 'Error',
                10001004: 'Removed'
            };

            var CRAWL_STATUS_CLASSES = {
                10001000: 'badge-status-pending',
                10001001: 'badge-status-crawled',
                10001002: 'badge-status-summarized',
                10001003: 'badge-status-error',
                10001004: 'badge-status-removed'
            };

            // --- DOM references ---
            var searchInput = document.getElementById('searchInput');
            var solutionFilter = document.getElementById('solutionFilter');
            var typeFilter = document.getElementById('typeFilter');
            var entityFilter = document.getElementById('entityFilter');
            var btnReset = document.getElementById('btnReset');
            var gridBody = document.getElementById('gridBody');
            var resultCount = document.getElementById('resultCount');
            var paginationInfo = document.getElementById('paginationInfo');
            var paginationControls = document.getElementById('paginationControls');
            var errorBanner = document.getElementById('errorBanner');

            // --- State ---
            var currentPage = 1;
            var totalCount = 0;
            var sortField = 'vb_name';
            var sortDir = 'asc';
            var searchTimeout = null;

            // --- Utility: Dataverse Web API fetch ---
            function apiGet(url) {
                var fullUrl = API_BASE + url;
                return fetch(fullUrl, {
                    method: 'GET',
                    headers: {
                        'Accept': 'application/json',
                        'OData-MaxVersion': '4.0',
                        'OData-Version': '4.0',
                        'Prefer': 'odata.include-annotations="*",odata.maxpagesize=' + PAGE_SIZE
                    },
                    credentials: 'include'
                }).then(function (response) {
                    if (!response.ok) {
                        return response.text().then(function (text) {
                            throw new Error('API error (' + response.status + '): ' + text);
                        });
                    }
                    return response.json();
                });
            }

            function apiGetCount(url) {
                var fullUrl = API_BASE + url;
                return fetch(fullUrl, {
                    method: 'GET',
                    headers: {
                        'Accept': 'application/json',
                        'OData-MaxVersion': '4.0',
                        'OData-Version': '4.0'
                    },
                    credentials: 'include'
                }).then(function (response) {
                    if (!response.ok) {
                        return response.text().then(function (text) {
                            throw new Error('API error (' + response.status + '): ' + text);
                        });
                    }
                    return response.text().then(function (text) {
                        return parseInt(text, 10) || 0;
                    });
                });
            }

            function showError(msg) {
                errorBanner.textContent = msg;
                errorBanner.style.display = 'block';
            }

            function clearError() {
                errorBanner.style.display = 'none';
            }

            function escapeOData(str) {
                return str.replace(/'/g, "''");
            }

            function formatDate(dateStr) {
                if (!dateStr) return '--';
                try {
                    var d = new Date(dateStr);
                    return d.toLocaleDateString(undefined, {
                        year: 'numeric',
                        month: 'short',
                        day: 'numeric'
                    }) + ' ' + d.toLocaleTimeString(undefined, {
                        hour: '2-digit',
                        minute: '2-digit'
                    });
                } catch (e) {
                    return dateStr;
                }
            }

            function truncate(str, max) {
                if (!str) return '--';
                if (str.length <= max) return str;
                return str.substring(0, max) + '...';
            }

            function htmlEncode(str) {
                if (!str) return '';
                var div = document.createElement('div');
                div.textContent = str;
                return div.innerHTML;
            }

            // --- Load filter dropdowns ---
            function loadSolutions() {
                var query = "vb_crawlconfigurations?$select=vb_crawlconfigurationid,vb_name,vb_solution_display_name" +
                    "&$filter=vb_is_active eq true&$orderby=vb_name asc";

                return apiGet(query).then(function (data) {
                    var records = data.value || [];
                    records.forEach(function (rec) {
                        var opt = document.createElement('option');
                        opt.value = rec.vb_crawlconfigurationid;
                        opt.textContent = rec.vb_solution_display_name || rec.vb_name;
                        solutionFilter.appendChild(opt);
                    });
                }).catch(function (err) {
                    showError('Failed to load solutions: ' + err.message);
                });
            }

            function loadComponentTypes() {
                Object.keys(COMPONENT_TYPE_LABELS).sort(function (a, b) {
                    return COMPONENT_TYPE_LABELS[a].localeCompare(COMPONENT_TYPE_LABELS[b]);
                }).forEach(function (key) {
                    var opt = document.createElement('option');
                    opt.value = key;
                    opt.textContent = COMPONENT_TYPE_LABELS[key];
                    typeFilter.appendChild(opt);
                });
            }

            function loadEntities() {
                var query = "vb_components?$select=vb_entity_logical_name" +
                    "&$filter=vb_entity_logical_name ne null" +
                    "&$orderby=vb_entity_logical_name asc";

                return apiGet(query).then(function (data) {
                    var records = data.value || [];
                    var seen = {};
                    records.forEach(function (rec) {
                        var name = rec.vb_entity_logical_name;
                        if (name && !seen[name]) {
                            seen[name] = true;
                            var opt = document.createElement('option');
                            opt.value = name;
                            opt.textContent = name;
                            entityFilter.appendChild(opt);
                        }
                    });
                }).catch(function (err) {
                    showError('Failed to load entities: ' + err.message);
                });
            }

            // --- Build OData filter ---
            function buildFilter() {
                var filters = [];

                var search = (searchInput.value || '').trim();
                if (search) {
                    var escaped = escapeOData(search);
                    filters.push(
                        "(contains(vb_name,'" + escaped + "') or contains(vb_ai_summary,'" + escaped + "'))"
                    );
                }

                var solutionId = solutionFilter.value;
                if (solutionId) {
                    filters.push("_vb_crawlconfiguration_value eq " + solutionId);
                }

                var typeVal = typeFilter.value;
                if (typeVal) {
                    filters.push("vb_component_type eq " + typeVal);
                }

                var entityVal = entityFilter.value;
                if (entityVal) {
                    filters.push("vb_entity_logical_name eq '" + escapeOData(entityVal) + "'");
                }

                return filters.length > 0 ? filters.join(' and ') : '';
            }

            // --- Pagination state: store nextLink per page ---
            var pageLinks = {};

            // --- Load grid data ---
            function loadData() {
                clearError();
                showLoadingState();

                var url;
                if (currentPage === 1 || !pageLinks[currentPage]) {
                    var select = "$select=vb_componentid,vb_name,vb_component_type,vb_entity_logical_name," +
                        "vb_crawl_status,vb_ai_summary,vb_crawled_on";

                    var filter = buildFilter();
                    var filterParam = filter ? '&$filter=' + filter : '';
                    var orderby = '&$orderby=' + sortField + ' ' + sortDir;

                    url = 'vb_components?' + select + filterParam + orderby +
                        '&$top=' + PAGE_SIZE + '&$count=true';
                } else {
                    // Use stored nextLink for subsequent pages
                    url = null;
                }

                var fetchUrl = url ? API_BASE + url : pageLinks[currentPage];

                fetch(fetchUrl, {
                    method: 'GET',
                    headers: {
                        'Accept': 'application/json',
                        'OData-MaxVersion': '4.0',
                        'OData-Version': '4.0',
                        'Prefer': 'odata.include-annotations="*",odata.maxpagesize=' + PAGE_SIZE
                    },
                    credentials: 'include'
                }).then(function (response) {
                    if (!response.ok) {
                        return response.text().then(function (text) {
                            throw new Error('API error (' + response.status + '): ' + text);
                        });
                    }
                    return response.json();
                }).then(function (data) {
                    if (data['@odata.count'] !== undefined) {
                        totalCount = data['@odata.count'];
                    }
                    // Store next page link if available
                    if (data['@odata.nextLink']) {
                        pageLinks[currentPage + 1] = data['@odata.nextLink'];
                    }
                    renderGrid(data.value || []);
                    renderPagination();
                    updateResultCount();
                }).catch(function (err) {
                    showError('Failed to load components: ' + err.message);
                    gridBody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:40px;color:#991b1b;">Error loading data.</td></tr>';
                    paginationInfo.textContent = '';
                    paginationControls.innerHTML = '';
                });
            }

            function showLoadingState() {
                gridBody.innerHTML = '<tr class="loading-row"><td colspan="6"><span class="spinner"></span>Loading components...</td></tr>';
            }

            // --- Render grid rows ---
            function renderGrid(records) {
                if (records.length === 0) {
                    gridBody.innerHTML =
                        '<tr><td colspan="6">' +
                        '<div class="empty-state">' +
                        '<div class="empty-state-icon">&#128204;</div>' +
                        '<h3>No components found</h3>' +
                        '<p>Try adjusting your search or filters.</p>' +
                        '</div></td></tr>';
                    return;
                }

                var html = '';
                records.forEach(function (rec) {
                    var typeLabel = COMPONENT_TYPE_LABELS[rec.vb_component_type] || 'Unknown';
                    var statusVal = rec.vb_crawl_status;
                    var statusLabel = CRAWL_STATUS_LABELS[statusVal] || 'Unknown';
                    var statusClass = CRAWL_STATUS_CLASSES[statusVal] || 'badge-status-pending';
                    var summary = truncate(rec.vb_ai_summary, 100);
                    var crawledOn = formatDate(rec.vb_crawled_on);
                    var entity = rec.vb_entity_logical_name || '--';

                    html += '<tr data-id="' + rec.vb_componentid + '">' +
                        '<td class="col-name" title="' + htmlEncode(rec.vb_name) + '">' + htmlEncode(rec.vb_name) + '</td>' +
                        '<td><span class="badge badge-type">' + htmlEncode(typeLabel) + '</span></td>' +
                        '<td class="col-entity">' + htmlEncode(entity) + '</td>' +
                        '<td><span class="badge ' + statusClass + '">' + htmlEncode(statusLabel) + '</span></td>' +
                        '<td class="col-summary" title="' + htmlEncode(rec.vb_ai_summary || '') + '">' + htmlEncode(summary) + '</td>' +
                        '<td class="col-date">' + htmlEncode(crawledOn) + '</td>' +
                        '</tr>';
                });

                gridBody.innerHTML = html;

                // Attach row click handlers — open detail panel
                var rows = gridBody.querySelectorAll('tr[data-id]');
                rows.forEach(function (row) {
                    row.addEventListener('click', function () {
                        var componentId = row.getAttribute('data-id');
                        openDetailPanel(componentId);
                    });
                });
            }

            // --- Detail Panel ---
            var panelOverlay = document.getElementById('panelOverlay');
            var detailPanel = document.getElementById('detailPanel');
            var panelTitle = document.getElementById('panelTitle');
            var panelBody = document.getElementById('panelBody');
            var panelClose = document.getElementById('panelClose');
            var panelOpenRecord = document.getElementById('panelOpenRecord');
            var panelGenerateAi = document.getElementById('panelGenerateAi');
            var currentPanelId = null;
            var currentPanelSourceId = null;
            var currentPanelComponentType = null;
            var currentPanelEntityLogicalName = null;
            var currentPanelSolutionUniqueName = null;

            // --- Maker Portal URL helpers ---
            var makerEnvId = '';
            var solutionGuidMap = {}; // solutionUniqueName → solutionid (GUID)

            function loadEnvironmentId() {
                return fetch(API_BASE + 'RetrieveCurrentOrganization(AccessType=Microsoft.Dynamics.CRM.EndpointAccessType%27Default%27)', {
                    headers: { 'Accept': 'application/json', 'OData-MaxVersion': '4.0', 'OData-Version': '4.0' },
                    credentials: 'include'
                }).then(function(r) { return r.json(); }).then(function(data) {
                    if (data && data.Detail && data.Detail.EnvironmentId) {
                        makerEnvId = data.Detail.EnvironmentId;
                    }
                }).catch(function() {});
            }

            function loadSolutionGuids() {
                // Get solution unique names from crawl configs, then resolve to solution GUIDs
                return apiGet('vb_crawlconfigurations?$select=vb_solution_unique_name&$filter=vb_is_active eq true')
                    .then(function(data) {
                        var names = (data.value || []).map(function(c) { return c.vb_solution_unique_name; }).filter(Boolean);
                        if (names.length === 0) return;
                        var filter = names.map(function(n) { return "uniquename eq '" + escapeOData(n) + "'"; }).join(' or ');
                        return apiGet('solutions?$select=solutionid,uniquename&$filter=' + filter);
                    }).then(function(data) {
                        if (data && data.value) {
                            data.value.forEach(function(s) {
                                solutionGuidMap[s.uniquename] = s.solutionid;
                            });
                        }
                    }).catch(function() {});
            }

            // Map component type to Maker Portal solution objects category
            var MAKER_OBJECT_CATEGORY = {
                10001001: 'plugin%20assemblies',        // Plugin Step
                10001002: 'cloudflows',                  // Cloud Flow (Automated)
                10001003: 'cloudflows',                  // Cloud Flow (Scheduled)
                10001004: 'cloudflows',                  // Cloud Flow (Instant)
                10001005: 'businessrules',               // Business Rule
                10001006: 'classicworkflows',            // Classic Workflow
                10001007: 'customactions',               // Custom Action
                10001008: 'customapis',                  // Custom API
                10001009: 'web%20resources/code',        // JS Web Resource
                10001010: 'web%20resources/code',        // HTML Web Resource
                10001011: 'web%20resources/code',        // CSS Web Resource
                10001012: 'forms',                       // Form (Main)
                10001013: 'forms',                       // Form (Quick Create)
                10001014: 'forms',                       // Form (Quick View)
                10001015: 'views',                       // View (System)
                10001016: 'dashboards',                  // Dashboard
                10001017: 'charts',                      // Chart
                10001018: 'canvasapps',                  // Custom Page
                10001019: 'canvasapps',                  // Canvas App
                10001020: 'apps',                        // Model-Driven App
                10001021: 'site%20map',                  // Site Map
                10001022: 'securityroles',               // Security Role
                10001023: 'businessprocessflows',        // BPF
                10001024: 'desktopflows',                // Desktop Flow
                10001025: 'connectionreferences',        // Connection Reference
                10001026: 'environmentvariables',        // Environment Variable
                10001027: 'customconnectors',            // Custom Connector
                10001028: 'entities',                    // Table
                10001029: 'entities',                    // Column (go to tables)
                10001030: 'entities',                    // Relationship (go to tables)
                10001031: 'optionsets',                  // Option Set
                10001032: 'commandbar',                  // Ribbon/Command Bar
                10001033: 'objects'                      // Other
            };

            // Build URL: environment/{envId}/solutions/{solutionId}/objects/{category}
            function getComponentUrl(componentType, sourceId, entityLogicalName, solutionUniqueName) {
                if (!makerEnvId) return null;

                var makerBase = 'https://make.powerapps.com/environments/' + makerEnvId;
                var solutionId = solutionUniqueName ? solutionGuidMap[solutionUniqueName] : null;
                var category = MAKER_OBJECT_CATEGORY[componentType] || 'objects';

                if (solutionId) {
                    return makerBase + '/solutions/' + solutionId + '/objects/' + category;
                }

                // No solution context — go to solutions list
                return makerBase + '/solutions';
            }

            function closePanel() {
                detailPanel.style.display = 'none';
                panelOverlay.style.display = 'none';
                currentPanelId = null;
            }
            panelClose.addEventListener('click', closePanel);
            panelOverlay.addEventListener('click', closePanel);
            document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePanel(); });

            panelOpenRecord.addEventListener('click', function () {
                if (!currentPanelId) return;

                var url = getComponentUrl(currentPanelComponentType, currentPanelSourceId, currentPanelEntityLogicalName, currentPanelSolutionUniqueName);
                if (url) {
                    window.open(url, '_blank');
                } else {
                    // Ultimate fallback: Maker Portal solutions list
                    if (makerEnvId) {
                        window.open('https://make.powerapps.com/environments/' + makerEnvId + '/solutions', '_blank');
                    }
                }
            });

            panelGenerateAi.addEventListener('click', function () {
                if (!currentPanelId) return;
                panelGenerateAi.disabled = true;
                panelGenerateAi.textContent = 'Generating...';

                fetch(API_BASE + 'vb_GenerateAISummary', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json',
                               'OData-MaxVersion': '4.0', 'OData-Version': '4.0' },
                    credentials: 'include',
                    body: JSON.stringify({ ComponentId: currentPanelId })
                }).then(function (r) { return r.json(); })
                .then(function (result) {
                    panelGenerateAi.disabled = false;
                    panelGenerateAi.textContent = 'Generate AI Summary';
                    if (result.Success) {
                        // Refresh the panel
                        openDetailPanel(currentPanelId);
                        // Also refresh the grid
                        loadData();
                    } else {
                        var errMsg = (result.error && result.error.message) || 'Failed to generate summary.';
                        alert(errMsg);
                    }
                }).catch(function (err) {
                    panelGenerateAi.disabled = false;
                    panelGenerateAi.textContent = 'Generate AI Summary';
                    alert('Error: ' + err.message);
                });
            });

            function openDetailPanel(componentId) {
                currentPanelId = componentId;
                panelBody.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;"><span class="spinner"></span> Loading...</div>';
                panelTitle.textContent = 'Component';
                detailPanel.style.display = 'flex';
                panelOverlay.style.display = 'block';

                // Fetch full component record with crawl config for solution context
                apiGet('vb_components(' + componentId + ')?$select=vb_name,vb_component_type,vb_entity_logical_name,vb_unique_name,vb_crawl_status,vb_ai_summary,vb_ai_summary_generated_on,vb_crawled_on,vb_source_modified_on,vb_execution_stage,vb_is_managed,vb_solution_name,vb_trigger_attributes,vb_connectors_used,vb_error_details,vb_raw_metadata,vb_source_id&$expand=vb_CrawlConfiguration($select=vb_solution_unique_name)')
                .then(function (comp) {
                    panelTitle.textContent = comp.vb_name || 'Component';

                    // Store component info for the "Open in Maker Portal" button
                    currentPanelSourceId = comp.vb_source_id || null;
                    currentPanelComponentType = comp.vb_component_type || null;
                    currentPanelEntityLogicalName = comp.vb_entity_logical_name || null;
                    currentPanelSolutionUniqueName = (comp.vb_CrawlConfiguration && comp.vb_CrawlConfiguration.vb_solution_unique_name) || null;

                    var typeLabel = COMPONENT_TYPE_LABELS[comp.vb_component_type] || 'Unknown';
                    var statusVal = comp.vb_crawl_status;
                    var statusLabel = CRAWL_STATUS_LABELS[statusVal] || 'Unknown';
                    var statusClass = CRAWL_STATUS_CLASSES[statusVal] || 'badge-status-pending';

                    var html = '';

                    // Info section
                    html += '<div class="panel-section">';
                    html += '<div class="panel-section-title">Details</div>';
                    html += '<div style="display:flex;gap:6px;margin-bottom:12px;">';
                    html += '<span class="panel-badge badge-type">' + htmlEncode(typeLabel) + '</span>';
                    html += '<span class="panel-badge ' + statusClass + '">' + htmlEncode(statusLabel) + '</span>';
                    if (comp.vb_is_managed) html += '<span class="panel-badge" style="background:#e5e7eb;color:#374151;">Managed</span>';
                    html += '</div>';

                    if (comp.vb_entity_logical_name) {
                        html += '<div class="panel-field"><div class="panel-field-label">Table</div><div class="panel-field-value" style="font-family:monospace;">' + htmlEncode(comp.vb_entity_logical_name) + '</div></div>';
                    }
                    if (comp.vb_unique_name) {
                        html += '<div class="panel-field"><div class="panel-field-label">Unique Name</div><div class="panel-field-value" style="font-family:monospace;font-size:12px;">' + htmlEncode(comp.vb_unique_name) + '</div></div>';
                    }
                    if (comp.vb_solution_name) {
                        html += '<div class="panel-field"><div class="panel-field-label">Solution</div><div class="panel-field-value">' + htmlEncode(comp.vb_solution_name) + '</div></div>';
                    }
                    if (comp.vb_connectors_used) {
                        html += '<div class="panel-field"><div class="panel-field-label">Connectors</div><div class="panel-field-value">' + htmlEncode(comp.vb_connectors_used) + '</div></div>';
                    }
                    if (comp.vb_trigger_attributes) {
                        html += '<div class="panel-field"><div class="panel-field-label">Trigger Attributes</div><div class="panel-field-value" style="font-family:monospace;font-size:12px;">' + htmlEncode(comp.vb_trigger_attributes) + '</div></div>';
                    }

                    var dates = [];
                    if (comp.vb_crawled_on) dates.push('<span class="panel-field-label">Crawled:</span> ' + formatDate(comp.vb_crawled_on));
                    if (comp.vb_source_modified_on) dates.push('<span class="panel-field-label">Source Modified:</span> ' + formatDate(comp.vb_source_modified_on));
                    if (comp.vb_ai_summary_generated_on) dates.push('<span class="panel-field-label">AI Generated:</span> ' + formatDate(comp.vb_ai_summary_generated_on));
                    if (dates.length) {
                        html += '<div class="panel-field" style="font-size:12px;color:#6b7280;">' + dates.join(' &nbsp;·&nbsp; ') + '</div>';
                    }
                    html += '</div>';

                    // AI Summary section
                    html += '<div class="panel-section">';
                    html += '<div class="panel-section-title">AI Summary</div>';
                    if (comp.vb_ai_summary) {
                        html += '<div class="panel-summary">' + htmlEncode(comp.vb_ai_summary) + '</div>';
                    } else {
                        html += '<div class="panel-summary empty">No AI summary yet. Click "Generate AI Summary" below.</div>';
                    }
                    html += '</div>';

                    // Error details if any
                    if (comp.vb_error_details) {
                        html += '<div class="panel-section">';
                        html += '<div class="panel-section-title">Error Details</div>';
                        html += '<div class="panel-metadata" style="color:#991b1b;background:#fef2f2;border-color:#fecaca;">' + htmlEncode(comp.vb_error_details) + '</div>';
                        html += '</div>';
                    }

                    // Raw metadata (collapsed by default)
                    if (comp.vb_raw_metadata) {
                        var metaPreview = comp.vb_raw_metadata.length > 500
                            ? comp.vb_raw_metadata.substring(0, 500) + '...'
                            : comp.vb_raw_metadata;
                        html += '<div class="panel-section">';
                        html += '<div class="panel-section-title" style="cursor:pointer;" onclick="var el=this.nextElementSibling;el.style.display=el.style.display===\'none\'?\'block\':\'none\';">Raw Metadata (' + comp.vb_raw_metadata.length.toLocaleString() + ' chars) ▸</div>';
                        html += '<div style="display:none;"><div class="panel-metadata">' + htmlEncode(comp.vb_raw_metadata) + '</div></div>';
                        html += '</div>';
                    }

                    panelBody.innerHTML = html;

                    // Update AI button state
                    panelGenerateAi.style.display = comp.vb_ai_summary ? 'none' : 'inline-block';
                    if (statusVal === 10001002) { // Already summarized
                        panelGenerateAi.textContent = 'Regenerate AI Summary';
                        panelGenerateAi.style.display = 'inline-block';
                    }

                }).catch(function (err) {
                    panelBody.innerHTML = '<div style="padding:20px;color:#991b1b;">Failed to load component: ' + htmlEncode(err.message) + '</div>';
                });
            }

            // --- Pagination ---
            function renderPagination() {
                var totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
                var start = Math.min((currentPage - 1) * PAGE_SIZE + 1, totalCount);
                var end = Math.min(currentPage * PAGE_SIZE, totalCount);

                if (totalCount === 0) {
                    paginationInfo.textContent = 'No results';
                    paginationControls.innerHTML = '';
                    return;
                }

                paginationInfo.textContent = 'Showing ' + start + '-' + end + ' of ' + totalCount;

                var html = '';

                // Previous button
                html += '<button ' + (currentPage <= 1 ? 'disabled' : '') +
                    ' data-page="' + (currentPage - 1) + '">&laquo; Prev</button>';

                // Page number buttons (show up to 7 pages centered around current)
                var startPage = Math.max(1, currentPage - 3);
                var endPage = Math.min(totalPages, startPage + 6);
                if (endPage - startPage < 6) {
                    startPage = Math.max(1, endPage - 6);
                }

                for (var p = startPage; p <= endPage; p++) {
                    html += '<button data-page="' + p + '"' +
                        (p === currentPage ? ' class="active"' : '') + '>' + p + '</button>';
                }

                // Next button
                html += '<button ' + (currentPage >= totalPages ? 'disabled' : '') +
                    ' data-page="' + (currentPage + 1) + '">Next &raquo;</button>';

                paginationControls.innerHTML = html;

                // Attach click handlers
                var buttons = paginationControls.querySelectorAll('button:not(:disabled)');
                buttons.forEach(function (btn) {
                    btn.addEventListener('click', function () {
                        var page = parseInt(btn.getAttribute('data-page'), 10);
                        if (page >= 1 && page <= totalPages && page !== currentPage) {
                            currentPage = page;
                            loadData();
                        }
                    });
                });
            }

            function updateResultCount() {
                resultCount.textContent = totalCount + ' component' + (totalCount !== 1 ? 's' : '');
            }

            // --- Column sorting ---
            var sortableHeaders = document.querySelectorAll('.data-grid thead th[data-field]');
            sortableHeaders.forEach(function (th) {
                th.addEventListener('click', function () {
                    var field = th.getAttribute('data-field');
                    if (!field || field === 'vb_ai_summary') return; // Don't sort on summary

                    if (sortField === field) {
                        sortDir = sortDir === 'asc' ? 'desc' : 'asc';
                    } else {
                        sortField = field;
                        sortDir = 'asc';
                    }

                    updateSortIndicators();
                    currentPage = 1;
                    pageLinks = {};
                    loadData();
                });
            });

            function updateSortIndicators() {
                sortableHeaders.forEach(function (th) {
                    var field = th.getAttribute('data-field');
                    var indicator = document.getElementById('sort-' + field);
                    if (!indicator) return;

                    if (field === sortField) {
                        indicator.textContent = sortDir === 'asc' ? '\u25B2' : '\u25BC';
                        indicator.classList.add('active');
                    } else {
                        indicator.textContent = '';
                        indicator.classList.remove('active');
                    }
                });
            }

            // --- Filter event handlers ---
            searchInput.addEventListener('input', function () {
                clearTimeout(searchTimeout);
                searchTimeout = setTimeout(function () {
                    currentPage = 1;
                    pageLinks = {};
                    loadData();
                }, 400);
            });

            solutionFilter.addEventListener('change', function () {
                currentPage = 1;
                loadData();
            });

            typeFilter.addEventListener('change', function () {
                currentPage = 1;
                loadData();
            });

            entityFilter.addEventListener('change', function () {
                currentPage = 1;
                loadData();
            });

            btnReset.addEventListener('click', function () {
                searchInput.value = '';
                solutionFilter.value = '';
                typeFilter.value = '';
                entityFilter.value = '';
                sortField = 'vb_name';
                sortDir = 'asc';
                currentPage = 1;
                updateSortIndicators();
                loadData();
            });

            // --- Initialize ---
            loadComponentTypes();
            updateSortIndicators();

            Promise.all([
                loadSolutions(),
                loadEntities(),
                loadEnvironmentId(),
                loadSolutionGuids()
            ]).then(function () {
                loadData();
            }).catch(function () {
                loadData();
            });
        })();
    </script>
</body>
</html>
