<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Compass — 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;
        }

        /* ===== Top Header ===== */
        .app-header {
            background: #ffffff;
            border-bottom: 1px solid #e5e7eb;
            padding: 0 24px;
            display: flex;
            align-items: center;
            gap: 24px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.06);
        }

        .app-title {
            font-size: 17px;
            font-weight: 700;
            color: #111827;
            padding: 14px 0;
            white-space: nowrap;
        }

        .app-title span {
            color: #2563eb;
        }

        /* ===== Tab Navigation ===== */
        .tab-nav {
            display: flex;
            gap: 0;
            overflow-x: auto;
            flex: 1;
        }

        .tab-btn {
            font-family: inherit;
            font-size: 13px;
            font-weight: 600;
            color: #6b7280;
            background: none;
            border: none;
            border-bottom: 2px solid transparent;
            padding: 14px 18px;
            cursor: pointer;
            white-space: nowrap;
            transition: color 0.15s, border-color 0.15s;
        }

        .tab-btn:hover {
            color: #374151;
        }

        .tab-btn.active {
            color: #2563eb;
            border-bottom-color: #2563eb;
        }

        .refresh-btn {
            font-family: inherit;
            font-size: 12px;
            font-weight: 600;
            color: #6b7280;
            background: #f9fafb;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            padding: 6px 14px;
            cursor: pointer;
            white-space: nowrap;
            transition: all 0.15s;
        }

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

        /* ===== Tab Content ===== */
        .tab-content {
            flex: 1;
            display: none;
            padding: 20px 24px;
            overflow-y: auto;
        }

        .tab-content.active {
            display: block;
        }

        /* ===== Summary Cards ===== */
        .summary-cards {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
            gap: 16px;
            margin-bottom: 24px;
        }

        .summary-card {
            background: #ffffff;
            border: 1px solid #e5e7eb;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.04);
        }

        .summary-card-label {
            font-size: 12px;
            font-weight: 600;
            color: #6b7280;
            text-transform: uppercase;
            letter-spacing: 0.4px;
            margin-bottom: 8px;
        }

        .summary-card-value {
            font-size: 28px;
            font-weight: 700;
            color: #111827;
            line-height: 1;
            margin-bottom: 8px;
        }

        .summary-card-detail {
            font-size: 12px;
            color: #6b7280;
            display: flex;
            gap: 12px;
        }

        .summary-card-detail .active-count { color: #059669; font-weight: 600; }
        .summary-card-detail .inactive-count { color: #dc2626; font-weight: 600; }
        .summary-card-detail .set-count { color: #059669; font-weight: 600; }
        .summary-card-detail .notset-count { color: #dc2626; font-weight: 600; }

        .summary-card.loading .summary-card-value {
            color: #d1d5db;
        }

        /* ===== Sections / Panels ===== */
        .section {
            background: #ffffff;
            border: 1px solid #e5e7eb;
            border-radius: 10px;
            margin-bottom: 20px;
            overflow: hidden;
            box-shadow: 0 1px 3px rgba(0,0,0,0.04);
        }

        .section-header {
            padding: 14px 20px;
            border-bottom: 1px solid #e5e7eb;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }

        .section-header h3 {
            font-size: 15px;
            font-weight: 600;
            color: #111827;
        }

        .section-body {
            padding: 0;
        }

        /* ===== Data Tables ===== */
        .data-table {
            width: 100%;
            border-collapse: separate;
            border-spacing: 0;
        }

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

        .data-table tbody tr {
            transition: background 0.1s;
        }

        .data-table tbody tr:hover {
            background: #f9fafb;
        }

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

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

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

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

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

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

        .badge-on { background: #d1fae5; color: #065f46; }
        .badge-off { background: #fee2e2; color: #991b1b; }
        .badge-enabled { background: #d1fae5; color: #065f46; }
        .badge-disabled { background: #fee2e2; color: #991b1b; }
        .badge-active { background: #d1fae5; color: #065f46; }
        .badge-inactive { background: #fee2e2; color: #991b1b; }
        .badge-sync { background: #dbeafe; color: #1e40af; }
        .badge-async { background: #e0e7ff; color: #3730a3; }
        .badge-set { background: #d1fae5; color: #065f46; }
        .badge-default { background: #fef3c7; color: #92400e; }
        .badge-notset { background: #fee2e2; color: #991b1b; }
        .badge-type { background: #e0e7ff; color: #3730a3; }
        .badge-stage { background: #f3f4f6; color: #374151; }
        .badge-platform { background: #fef3c7; color: #92400e; }
        .badge-custom { background: #e0e7ff; color: #3730a3; }

        /* ===== Buttons ===== */
        .btn {
            font-family: inherit;
            font-size: 12px;
            padding: 5px 12px;
            border-radius: 6px;
            border: 1px solid #d1d5db;
            background: #f9fafb;
            color: #374151;
            cursor: pointer;
            font-weight: 600;
            transition: all 0.15s;
            white-space: nowrap;
        }

        .btn:hover:not(:disabled) { background: #f3f4f6; border-color: #9ca3af; }
        .btn:focus { outline: none; box-shadow: 0 0 0 3px rgba(59,130,246,0.12); }
        .btn:disabled { opacity: 0.5; cursor: not-allowed; }

        .btn-primary { background: #2563eb; color: #fff; border-color: #2563eb; }
        .btn-primary:hover:not(:disabled) { background: #1d4ed8; }

        .btn-danger { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
        .btn-danger:hover:not(:disabled) { background: #fecaca; }

        .btn-success { background: #d1fae5; color: #065f46; border-color: #a7f3d0; }
        .btn-success:hover:not(:disabled) { background: #a7f3d0; }

        .btn-toggle { min-width: 70px; text-align: center; }
        .btn-toggle.is-on { background: #fee2e2; color: #991b1b; border-color: #fecaca; }
        .btn-toggle.is-off { background: #d1fae5; color: #065f46; border-color: #a7f3d0; }

        /* ===== Filter / Search Bar ===== */
        .toolbar {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            padding: 14px 20px;
            align-items: center;
            border-bottom: 1px solid #e5e7eb;
        }

        .toolbar-search {
            font-family: inherit;
            font-size: 13px;
            padding: 7px 12px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            background: #fff;
            color: #1f2937;
            min-width: 240px;
            transition: border-color 0.15s, box-shadow 0.15s;
        }

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

        .toolbar-select {
            font-family: inherit;
            font-size: 13px;
            padding: 7px 12px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            background: #fff;
            color: #1f2937;
            min-width: 140px;
        }

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

        .toolbar-count {
            font-size: 12px;
            color: #6b7280;
            margin-left: auto;
        }

        /* ===== Spinner ===== */
        .spinner {
            display: inline-block;
            width: 16px;
            height: 16px;
            border: 2px solid #e5e7eb;
            border-top-color: #3b82f6;
            border-radius: 50%;
            animation: spin 0.7s linear infinite;
            vertical-align: middle;
        }

        .spinner-sm {
            width: 12px;
            height: 12px;
            border-width: 2px;
            margin-right: 4px;
        }

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

        /* ===== Empty / Loading States ===== */
        .empty-state {
            text-align: center;
            padding: 48px 24px;
            color: #9ca3af;
        }

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

        .empty-state p {
            font-size: 13px;
        }

        .loading-state {
            text-align: center;
            padding: 40px;
            color: #6b7280;
            font-size: 13px;
        }

        /* ===== Toast Notifications ===== */
        .toast-container {
            position: fixed;
            top: 16px;
            right: 16px;
            z-index: 10000;
            display: flex;
            flex-direction: column;
            gap: 8px;
            pointer-events: none;
        }

        .toast {
            background: #ffffff;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
            padding: 12px 18px;
            box-shadow: 0 4px 16px rgba(0,0,0,0.12);
            font-size: 13px;
            color: #374151;
            max-width: 380px;
            pointer-events: auto;
            animation: toastIn 0.25s ease;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .toast.success { border-left: 4px solid #059669; }
        .toast.error { border-left: 4px solid #dc2626; }

        .toast-icon { font-size: 16px; flex-shrink: 0; }
        .toast.success .toast-icon { color: #059669; }
        .toast.error .toast-icon { color: #dc2626; }

        .toast.fade-out { animation: toastOut 0.3s ease forwards; }

        @keyframes toastIn { from { opacity: 0; transform: translateX(60px); } to { opacity: 1; transform: translateX(0); } }
        @keyframes toastOut { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(60px); } }

        /* ===== Confirmation Dialog ===== */
        .confirm-overlay {
            position: fixed;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.4);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9000;
            animation: fadeIn 0.15s ease;
        }

        .confirm-box {
            background: #ffffff;
            border-radius: 12px;
            padding: 24px;
            max-width: 440px;
            width: 90%;
            box-shadow: 0 20px 60px rgba(0,0,0,0.2);
        }

        .confirm-box h3 {
            font-size: 16px;
            font-weight: 700;
            color: #111827;
            margin-bottom: 8px;
        }

        .confirm-box p {
            font-size: 13px;
            color: #4b5563;
            margin-bottom: 20px;
            line-height: 1.6;
        }

        .confirm-actions {
            display: flex;
            gap: 8px;
            justify-content: flex-end;
        }

        .confirm-warning {
            background: #fef3c7;
            border: 1px solid #fde68a;
            color: #92400e;
            border-radius: 8px;
            padding: 10px 12px;
            font-size: 12px;
            line-height: 1.5;
            margin-bottom: 16px;
        }

        .confirm-typed-wrap {
            margin-bottom: 20px;
        }

        .confirm-typed-wrap label {
            display: block;
            font-size: 12px;
            color: #6b7280;
            margin-bottom: 6px;
        }

        .confirm-typed-input {
            font-family: inherit;
            font-size: 13px;
            padding: 7px 12px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            width: 100%;
            box-sizing: border-box;
        }

        .confirm-typed-input:focus {
            outline: none;
            border-color: #dc2626;
            box-shadow: 0 0 0 3px rgba(220,38,38,0.12);
        }

        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

        /* ===== Run History Dialog (reuses .confirm-overlay for the backdrop) ===== */
        .history-box {
            max-width: 640px;
        }

        .history-box h3 {
            font-size: 16px;
            font-weight: 700;
            color: #111827;
            margin-bottom: 14px;
        }

        .history-body {
            max-height: 400px;
            overflow-y: auto;
            margin-bottom: 16px;
            border: 1px solid #e5e7eb;
            border-radius: 8px;
        }

        .history-body .data-table thead th {
            position: sticky;
            top: 0;
        }

        .badge-run-ok { background: #d1fae5; color: #065f46; }
        .badge-run-fail { background: #fee2e2; color: #991b1b; }
        .badge-run-active { background: #dbeafe; color: #1e40af; }
        .badge-run-other { background: #f3f4f6; color: #374151; }

        /* ===== Inline Edit ===== */
        .inline-edit-group {
            display: flex;
            gap: 6px;
            align-items: center;
        }

        .inline-edit-input {
            font-family: inherit;
            font-size: 13px;
            padding: 5px 10px;
            border: 1px solid #3b82f6;
            border-radius: 6px;
            background: #fff;
            color: #1f2937;
            min-width: 200px;
            box-shadow: 0 0 0 3px rgba(59,130,246,0.12);
        }

        .inline-edit-input:focus { outline: none; }

        /* ===== Component Breakdown (Dashboard) ===== */
        .breakdown-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
            gap: 8px;
            padding: 16px 20px;
        }

        .breakdown-item {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 8px 12px;
            background: #f9fafb;
            border-radius: 6px;
            font-size: 13px;
        }

        .breakdown-item .type-name {
            color: #374151;
            font-weight: 500;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            margin-right: 8px;
        }

        .breakdown-item .type-count {
            font-weight: 700;
            color: #2563eb;
            flex-shrink: 0;
        }

        /* ===== Recent Items List ===== */
        .recent-list {
            list-style: none;
        }

        .recent-item {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px 20px;
            border-bottom: 1px solid #f3f4f6;
            font-size: 13px;
        }

        .recent-item:last-child { border-bottom: none; }

        .recent-item .name {
            font-weight: 600;
            color: #1f2937;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            max-width: 300px;
        }

        .recent-item .meta {
            font-size: 12px;
            color: #6b7280;
            display: flex;
            gap: 12px;
            flex-shrink: 0;
        }

        /* ===== Two-Column Layout (Dashboard) ===== */
        .mc-grid {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }

        @media (max-width: 900px) {
            .mc-grid { grid-template-columns: 1fr; }
            .toolbar-search { min-width: 180px; }
            .summary-cards { grid-template-columns: repeat(2, 1fr); }
        }

        @media (max-width: 600px) {
            .app-header { padding: 0 12px; gap: 12px; }
            .tab-btn { padding: 12px 12px; font-size: 12px; }
            .tab-content { padding: 12px; }
            .summary-cards { grid-template-columns: 1fr; }
        }
    </style>
</head>
<body>

    <!-- Header with tabs -->
    <div class="app-header">
        <div class="app-title"><span>Compass</span></div>
        <div class="tab-nav">
            <button class="tab-btn active" data-tab="mission-control">Dashboard</button>
            <button class="tab-btn" data-tab="cloud-flows">Cloud Flows</button>
            <button class="tab-btn" data-tab="plugin-steps">Plugin Steps</button>
            <button class="tab-btn" data-tab="env-variables">Environment Variables</button>
            <button class="tab-btn" data-tab="business-rules">Business Rules</button>
        </div>
        <button class="refresh-btn" id="globalRefresh">Refresh</button>
    </div>

    <!-- Toast container -->
    <div class="toast-container" id="toastContainer"></div>

    <!-- Confirm dialog (hidden) -->
    <div id="confirmOverlay" class="confirm-overlay" style="display:none;">
        <div class="confirm-box">
            <h3 id="confirmTitle">Confirm</h3>
            <div id="confirmWarning" class="confirm-warning" style="display:none;"></div>
            <p id="confirmText"></p>
            <div id="confirmTypedWrap" class="confirm-typed-wrap" style="display:none;">
                <label>Type <strong id="confirmTypedWord"></strong> to confirm</label>
                <input type="text" id="confirmTypedInput" class="confirm-typed-input" autocomplete="off" spellcheck="false" />
            </div>
            <div class="confirm-actions">
                <button class="btn" id="confirmCancel">Cancel</button>
                <button class="btn btn-danger" id="confirmOk">Confirm</button>
            </div>
        </div>
    </div>

    <!-- Flow run history dialog (hidden) -->
    <div id="historyOverlay" class="confirm-overlay" style="display:none;">
        <div class="confirm-box history-box">
            <h3 id="historyTitle">Run History</h3>
            <div class="history-body" id="historyBody"></div>
            <div class="confirm-actions">
                <button class="btn" id="historyClose">Close</button>
            </div>
        </div>
    </div>

    <!-- ===== Tab: Dashboard ===== -->
    <div class="tab-content active" id="tab-mission-control">
        <div class="summary-cards" id="mcCards">
            <div class="summary-card loading" id="mcCardFlows">
                <div class="summary-card-label">Cloud Flows</div>
                <div class="summary-card-value"><span class="spinner"></span></div>
                <div class="summary-card-detail"></div>
            </div>
            <div class="summary-card loading" id="mcCardBR">
                <div class="summary-card-label">Business Rules</div>
                <div class="summary-card-value"><span class="spinner"></span></div>
                <div class="summary-card-detail"></div>
            </div>
            <div class="summary-card loading" id="mcCardPlugins">
                <div class="summary-card-label">Plugin Steps</div>
                <div class="summary-card-value"><span class="spinner"></span></div>
                <div class="summary-card-detail"></div>
            </div>
            <div class="summary-card loading" id="mcCardEnvVars">
                <div class="summary-card-label">Environment Variables</div>
                <div class="summary-card-value"><span class="spinner"></span></div>
                <div class="summary-card-detail"></div>
            </div>
        </div>

        <div class="mc-grid">
            <div class="section">
                <div class="section-header"><h3>Recently Modified Components</h3></div>
                <div class="section-body" id="mcRecentList">
                    <div class="loading-state"><span class="spinner"></span> Loading...</div>
                </div>
            </div>
            <div class="section">
                <div class="section-header"><h3>Component Breakdown</h3></div>
                <div class="section-body" id="mcBreakdown">
                    <div class="loading-state"><span class="spinner"></span> Loading...</div>
                </div>
            </div>
        </div>
    </div>

    <!-- ===== Tab: Cloud Flows ===== -->
    <div class="tab-content" id="tab-cloud-flows">
        <div class="section">
            <div class="toolbar">
                <input type="text" class="toolbar-search" id="cfSearch" placeholder="Search flows by name..." />
                <span class="toolbar-count" id="cfCount"></span>
            </div>
            <div class="section-body">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>Entity</th>
                            <th>Status</th>
                            <th>Modified On</th>
                            <th style="width:170px;">Action</th>
                        </tr>
                    </thead>
                    <tbody id="cfBody">
                        <tr><td colspan="5"><div class="loading-state"><span class="spinner"></span> Loading cloud flows...</div></td></tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>

    <!-- ===== Tab: Plugin Steps ===== -->
    <div class="tab-content" id="tab-plugin-steps">
        <div class="section">
            <div class="toolbar">
                <input type="text" class="toolbar-search" id="psSearch" placeholder="Search plugin steps..." />
                <select class="toolbar-select" id="psScopeFilter" title="Custom steps are the ones your customizations (including managed ISV solutions like Cartographer) registered. Platform steps ship with Microsoft, identified by a Microsoft.* / System.* plugin assembly.">
                    <option value="custom" selected>Custom Steps Only</option>
                    <option value="all">All Steps (incl. Platform)</option>
                </select>
                <select class="toolbar-select" id="psStageFilter">
                    <option value="">All Stages</option>
                    <option value="10">Pre-Validation</option>
                    <option value="20">Pre-Operation</option>
                    <option value="30">Main Operation</option>
                    <option value="40">Post-Operation</option>
                </select>
                <span class="toolbar-count" id="psCount"></span>
            </div>
            <div class="section-body">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>Owner</th>
                            <th>Message</th>
                            <th>Stage</th>
                            <th>Mode</th>
                            <th>Status</th>
                            <th>Rank</th>
                            <th style="width:90px;">Action</th>
                        </tr>
                    </thead>
                    <tbody id="psBody">
                        <tr><td colspan="8"><div class="loading-state"><span class="spinner"></span> Loading plugin steps...</div></td></tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>

    <!-- ===== Tab: Environment Variables ===== -->
    <div class="tab-content" id="tab-env-variables">
        <div class="section">
            <div class="toolbar">
                <input type="text" class="toolbar-search" id="evSearch" placeholder="Search variables..." />
                <span class="toolbar-count" id="evCount"></span>
            </div>
            <div class="section-body">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th>Display Name</th>
                            <th>Schema Name</th>
                            <th>Type</th>
                            <th>Default Value</th>
                            <th>Current Value</th>
                            <th>Status</th>
                            <th style="width:140px;">Actions</th>
                        </tr>
                    </thead>
                    <tbody id="evBody">
                        <tr><td colspan="7"><div class="loading-state"><span class="spinner"></span> Loading environment variables...</div></td></tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>

    <!-- ===== Tab: Business Rules ===== -->
    <div class="tab-content" id="tab-business-rules">
        <div class="section">
            <div class="toolbar">
                <input type="text" class="toolbar-search" id="brSearch" placeholder="Search business rules..." />
                <span class="toolbar-count" id="brCount"></span>
            </div>
            <div class="section-body">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th>Name</th>
                            <th>Entity</th>
                            <th>Status</th>
                            <th>Modified On</th>
                            <th style="width:90px;">Action</th>
                        </tr>
                    </thead>
                    <tbody id="brBody">
                        <tr><td colspan="5"><div class="loading-state"><span class="spinner"></span> Loading business rules...</div></td></tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>

<script>
// Vendor analytics ping: fire-and-forget product-usage telemetry sent to Cartographer's own
// backend (not the customer's Dataverse). Cartographer is free now, but this call is kept
// deliberately for adoption metrics - it must never block the UI or surface an error, so both
// the outer request and the inner beacon are swallowed on failure.
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];
            return 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(function () { /* non-blocking, silent on failure by design */ });
    } catch(e) { /* silent */ }
}
vbAnalytics('page_view', {page: 'compass'});

(function () {
    'use strict';

    // =========================================================================
    // Configuration
    // =========================================================================
    var API_BASE = (function () {
        try {
            if (window.parent && window.parent.Xrm && window.parent.Xrm.Utility) {
                return window.parent.Xrm.Utility.getGlobalContext().getClientUrl() + '/api/data/v9.2/';
            }
        } catch (e) { /* ignore */ }
        return window.location.origin + '/api/data/v9.2/';
    })();

    var HEADERS_GET = {
        'Accept': 'application/json',
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0',
        'Prefer': 'odata.include-annotations="*"'
    };

    var HEADERS_POST = {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
        'OData-MaxVersion': '4.0',
        'OData-Version': '4.0'
    };

    // Component type labels (matching ComponentDashboard)
    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',
        10001034: 'Plugin Assembly'
    };

    // environmentvariabledefinition.type is a Microsoft-native choice column (values
    // 100000000-100000005), not one of Cartographer's own vb_ custom choices (which use the
    // 10001000+ publisher range seen in COMPONENT_TYPE_LABELS above) - using the wrong range here
    // made every row's type read "Unknown" even though the underlying data was fine. Keep this in
    // sync with MapEnvironmentVariableType in CrawlSecurityHandler.cs.
    var ENV_VAR_TYPE_LABELS = { 100000000: 'String', 100000001: 'Number', 100000002: 'Boolean',
                                100000003: 'JSON', 100000004: 'Data Source', 100000005: 'Secret' };

    // =========================================================================
    // Utility Functions
    // =========================================================================
    function htmlEncode(str) {
        if (str === null || str === undefined) return '';
        var div = document.createElement('div');
        div.textContent = String(str);
        return div.innerHTML;
    }

    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 '';
        return str.length > max ? str.substring(0, max) + '...' : str;
    }

    function friendlyPluginName(rawName) {
        if (!rawName) return '--';
        // Strip common namespace patterns (e.g., "MyCompany.Plugins.OnCreate" -> "On Create")
        var parts = rawName.split('.');
        var name = parts[parts.length - 1];
        // Also strip content after ": " pattern (step config descriptions)
        var colonIdx = name.indexOf(': ');
        if (colonIdx > 0) name = name.substring(0, colonIdx);
        // Add spaces before capitals (camelCase -> Camel Case)
        name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
        name = name.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
        return name;
    }

    function stageLabel(stage) {
        // Standard SdkMessageProcessingStep stages the platform documents. 50 is the deprecated
        // post-operation stage still seen on a handful of legacy steps. Anything else (e.g. 25)
        // is an internal/undocumented Microsoft stage - label it as such rather than falling
        // back to a bare "Stage N" debug string.
        if (stage === 10) return 'Pre-Val';
        if (stage === 20) return 'Pre-Op';
        if (stage === 30) return 'Main Op';
        if (stage === 40) return 'Post-Op';
        if (stage === 50) return 'Post-Op (Deprecated)';
        return stage != null ? 'Internal (Stage ' + stage + ')' : '--';
    }

    function modeLabel(mode) {
        if (mode === 0) return 'Sync';
        if (mode === 1) return 'Async';
        return mode != null ? 'Mode ' + mode : '--';
    }

    // Workflow rows (cloud flows and business rules both live on the workflow table) that
    // aren't bound to a table don't come back with an empty/null primaryentity - Dataverse
    // stores the literal string "none" there instead. That string is truthy, so
    // `flow.primaryentity || '--'` never falls through to the placeholder and the grid was
    // showing the word "none" verbatim. Route both tabs through here so neither can drift
    // from the other on what counts as "no entity".
    function entityLabel(primaryEntity) {
        if (!primaryEntity || primaryEntity.toLowerCase() === 'none') return '--';
        return primaryEntity;
    }

    // Shared "Set" predicate for environment variables: true only when there is a non-empty
    // CURRENT value (environmentvariablevalue.value). A default value alone
    // (environmentvariabledefinition.defaultvalue) lets the variable resolve, but it is not
    // something the customer has actually overridden - the grid shows that case as "Default",
    // not "Set". Both the Dashboard tile (loadMCCardEnvVars) and the Environment Variables grid
    // (renderEnvVariables) call this one function so their counts cannot drift apart again.
    function envVarHasCurrentValue(value) {
        return value !== null && value !== undefined && value !== '';
    }

    // Two different questions get asked about the same plugin step, and they need to err in
    // opposite directions, so one predicate can no longer answer both.
    //
    // isPlatformPluginStepForGuard, below, decides how scary the DISABLE confirmation should be.
    // It errs toward "platform": a false positive only costs an extra typed confirmation, while a
    // false negative lets a platform step be disabled with one click. It checks both the assembly
    // name and the type name - some Microsoft assemblies (MicrosoftPowerAppsCardsPlugins,
    // MicrosoftPowerAppsAppFrameworkPlatformFeaturePlugins,
    // MicrosoftPowerAppsModernShellPlatformApiPlugins) carry no dot at all, so a plain "microsoft."
    // prefix check misses them even though their typename (e.g.
    // Microsoft.PowerApps.Cards.Plugins.PreOperationCardUpdate) still starts with "Microsoft" -
    // and falls back to ismanaged, which deliberately also catches Cartographer's own managed
    // steps: nobody should be one click away from disabling those either.
    function isPlatformPluginStepForGuard(step) {
        var pt = step.eventhandler_plugintype;
        var assembly = (pt && pt.assemblyname) || '';
        var typeName = (pt && pt.typename) || '';
        if (/^(microsoft|system)/i.test(assembly) || /^(microsoft|system)/i.test(typeName)) return true;
        return step.ismanaged === true;
    }

    // isPlatformPluginStepForFilter, below, decides what "Custom Steps Only" hides and what the
    // platform/custom counts and Owner badge say - i.e. "is this somebody else's code?" It errs
    // the other way, toward "custom", and unlike the guard above it must NOT fall back to
    // ismanaged: Cartographer itself ships as a managed AppSource solution, so its own custom-API
    // plugin steps are also ismanaged=true, and folding that into "platform" here is exactly what
    // made this filter return 0 of Cartographer's own steps. Only a Microsoft.* / System.*
    // assembly or type name means "not part of your customizations".
    function isPlatformPluginStepForFilter(step) {
        var pt = step.eventhandler_plugintype;
        var assembly = (pt && pt.assemblyname) || '';
        var typeName = (pt && pt.typename) || '';
        return /^(microsoft|system)/i.test(assembly) || /^(microsoft|system)/i.test(typeName);
    }

    // =========================================================================
    // API Helpers
    // =========================================================================
    function apiGet(url) {
        return fetch(API_BASE + url, { method: 'GET', headers: HEADERS_GET, credentials: 'include' })
            .then(function (r) {
                if (!r.ok) return r.text().then(function (t) { throw new Error('API ' + r.status + ': ' + t); });
                return r.json();
            });
    }

    function apiPost(url, body) {
        return fetch(API_BASE + url, {
            method: 'POST', headers: HEADERS_POST, credentials: 'include',
            body: JSON.stringify(body || {})
        }).then(function (r) {
            if (!r.ok) return r.text().then(function (t) { throw new Error('API ' + r.status + ': ' + t); });
            var ct = r.headers.get('content-type') || '';
            if (ct.indexOf('application/json') >= 0) return r.json();
            return r.text();
        });
    }

    // =========================================================================
    // Toast Notifications
    // =========================================================================
    var toastContainer = document.getElementById('toastContainer');

    function showToast(message, type) {
        var toast = document.createElement('div');
        toast.className = 'toast ' + (type || 'success');
        var icon = type === 'error' ? '\u2716' : '\u2714';
        toast.innerHTML = '<span class="toast-icon">' + icon + '</span><span>' + htmlEncode(message) + '</span>';
        toastContainer.appendChild(toast);

        setTimeout(function () {
            toast.classList.add('fade-out');
            setTimeout(function () { toast.remove(); }, 300);
        }, 4000);
    }

    // =========================================================================
    // Confirmation Dialog
    // =========================================================================
    var confirmOverlay = document.getElementById('confirmOverlay');
    var confirmTitleEl = document.getElementById('confirmTitle');
    var confirmTextEl = document.getElementById('confirmText');
    var confirmWarningEl = document.getElementById('confirmWarning');
    var confirmTypedWrapEl = document.getElementById('confirmTypedWrap');
    var confirmTypedWordEl = document.getElementById('confirmTypedWord');
    var confirmTypedInputEl = document.getElementById('confirmTypedInput');
    var confirmOkBtn = document.getElementById('confirmOk');
    var confirmCancelBtn = document.getElementById('confirmCancel');
    var pendingConfirmCallback = null;

    // options (optional): { requireTypedWord: 'DISABLE', warning: 'extra banner text' }
    // Use requireTypedWord to make an especially dangerous action (e.g. disabling a platform
    // component) harder than a single click, without hiding the capability entirely.
    function showConfirm(title, message, onConfirm, options) {
        options = options || {};
        confirmTitleEl.textContent = title;
        confirmTextEl.textContent = message;
        confirmOverlay.style.display = 'flex';
        pendingConfirmCallback = onConfirm;

        if (options.warning) {
            confirmWarningEl.textContent = options.warning;
            confirmWarningEl.style.display = 'block';
        } else {
            confirmWarningEl.style.display = 'none';
        }

        if (options.requireTypedWord) {
            confirmTypedWordEl.textContent = options.requireTypedWord;
            confirmTypedInputEl.value = '';
            confirmTypedWrapEl.style.display = 'block';
            confirmOkBtn.disabled = true;
            confirmTypedInputEl.oninput = function () {
                confirmOkBtn.disabled = confirmTypedInputEl.value.trim() !== options.requireTypedWord;
            };
            setTimeout(function () { confirmTypedInputEl.focus(); }, 50);
        } else {
            confirmTypedWrapEl.style.display = 'none';
            confirmTypedInputEl.oninput = null;
            confirmOkBtn.disabled = false;
        }
    }

    function hideConfirm() {
        confirmOverlay.style.display = 'none';
        pendingConfirmCallback = null;
        confirmWarningEl.style.display = 'none';
        confirmTypedWrapEl.style.display = 'none';
        confirmTypedInputEl.oninput = null;
        confirmOkBtn.disabled = false;
    }

    confirmCancelBtn.addEventListener('click', hideConfirm);
    confirmOkBtn.addEventListener('click', function () {
        var cb = pendingConfirmCallback;
        hideConfirm();
        if (cb) cb();
    });

    // Close confirm on Escape
    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && confirmOverlay.style.display === 'flex') {
            hideConfirm();
        }
    });

    // =========================================================================
    // Tab Navigation
    // =========================================================================
    var tabButtons = document.querySelectorAll('.tab-btn');
    var tabContents = document.querySelectorAll('.tab-content');
    var activeTab = 'mission-control';
    var tabLoadedMap = {};

    tabButtons.forEach(function (btn) {
        btn.addEventListener('click', function () {
            var tabId = btn.getAttribute('data-tab');
            switchTab(tabId);
        });
    });

    function switchTab(tabId) {
        activeTab = tabId;

        tabButtons.forEach(function (b) {
            b.classList.toggle('active', b.getAttribute('data-tab') === tabId);
        });

        tabContents.forEach(function (tc) {
            tc.classList.toggle('active', tc.id === 'tab-' + tabId);
        });

        // Lazy-load tab data on first visit
        if (!tabLoadedMap[tabId]) {
            tabLoadedMap[tabId] = true;
            loadTabData(tabId);
        }
    }

    function loadTabData(tabId) {
        switch (tabId) {
            case 'mission-control': loadMissionControl(); break;
            case 'cloud-flows': loadCloudFlows(); break;
            case 'plugin-steps': loadPluginSteps(); break;
            case 'env-variables': loadEnvVariables(); break;
            case 'business-rules': loadBusinessRules(); break;
        }
    }

    // Global refresh
    document.getElementById('globalRefresh').addEventListener('click', function () {
        tabLoadedMap = {};
        tabLoadedMap[activeTab] = true;
        loadTabData(activeTab);
    });

    // =========================================================================
    // MISSION CONTROL
    // =========================================================================
    function loadMissionControl() {
        loadMCCardFlows();
        loadMCCardBR();
        loadMCCardPlugins();
        loadMCCardEnvVars();
        loadMCRecentComponents();
        loadMCBreakdown();
    }

    function renderSummaryCard(cardId, total, details) {
        var card = document.getElementById(cardId);
        card.classList.remove('loading');
        card.querySelector('.summary-card-value').textContent = total;
        card.querySelector('.summary-card-detail').innerHTML = details;
    }

    function renderSummaryCardError(cardId) {
        var card = document.getElementById(cardId);
        card.classList.remove('loading');
        card.querySelector('.summary-card-value').textContent = '--';
        card.querySelector('.summary-card-detail').innerHTML = '<span style="color:#dc2626;">Failed to load</span>';
    }

    // Card: Cloud Flows
    function loadMCCardFlows() {
        var card = document.getElementById('mcCardFlows');
        card.classList.add('loading');
        card.querySelector('.summary-card-value').innerHTML = '<span class="spinner"></span>';

        apiGet("workflows?$filter=category eq 5&$select=statecode")
            .then(function (data) {
                var records = data.value || [];
                var active = 0, inactive = 0;
                records.forEach(function (r) {
                    if (r.statecode === 1) active++;
                    else inactive++;
                });
                renderSummaryCard('mcCardFlows', records.length,
                    '<span class="active-count">' + active + ' Active</span>' +
                    '<span class="inactive-count">' + inactive + ' Inactive</span>');
            })
            .catch(function () { renderSummaryCardError('mcCardFlows'); });
    }

    // Card: Business Rules
    function loadMCCardBR() {
        var card = document.getElementById('mcCardBR');
        card.classList.add('loading');
        card.querySelector('.summary-card-value').innerHTML = '<span class="spinner"></span>';

        apiGet("workflows?$filter=category eq 2&$select=statecode")
            .then(function (data) {
                var records = data.value || [];
                var active = 0, inactive = 0;
                records.forEach(function (r) {
                    if (r.statecode === 1) active++;
                    else inactive++;
                });
                renderSummaryCard('mcCardBR', records.length,
                    '<span class="active-count">' + active + ' Active</span>' +
                    '<span class="inactive-count">' + inactive + ' Inactive</span>');
            })
            .catch(function () { renderSummaryCardError('mcCardBR'); });
    }

    // Card: Plugin Steps
    function loadMCCardPlugins() {
        var card = document.getElementById('mcCardPlugins');
        card.classList.add('loading');
        card.querySelector('.summary-card-value').innerHTML = '<span class="spinner"></span>';

        // Scope the card to CUSTOM steps, matching the Plugin Steps tab's default scope, using the
        // exact same isPlatformPluginStepForFilter predicate as that tab so the two counts cannot
        // drift apart. Counting every step in the environment made the tile read 594 while the tab
        // it summarises showed 0, because almost all of those are Microsoft platform steps the tab
        // deliberately hides; counting by ismanaged alone would instead have hidden Cartographer's
        // own managed-solution steps from this tile too - which is why this uses the Filter
        // predicate, not the disable-guard one.
        apiGet("sdkmessageprocessingsteps?$filter=ishidden/Value eq false&$select=statecode,ismanaged&$expand=eventhandler_plugintype($select=typename,assemblyname)")
            .then(function (data) {
                var records = (data.value || []).filter(function (r) { return !isPlatformPluginStepForFilter(r); });
                var platform = (data.value || []).length - records.length;
                var enabled = 0, disabled = 0;
                records.forEach(function (r) {
                    if (r.statecode === 0) enabled++;
                    else disabled++;
                });
                renderSummaryCard('mcCardPlugins', records.length,
                    '<span class="active-count">' + enabled + ' Enabled</span>' +
                    '<span class="inactive-count">' + disabled + ' Disabled</span>' +
                    (platform ? '<span class="inactive-count">' + platform + ' platform hidden</span>' : ''));
            })
            .catch(function () { renderSummaryCardError('mcCardPlugins'); });
    }

    // Card: Environment Variables
    function loadMCCardEnvVars() {
        var card = document.getElementById('mcCardEnvVars');
        card.classList.add('loading');
        card.querySelector('.summary-card-value').innerHTML = '<span class="spinner"></span>';

        Promise.all([
            apiGet("environmentvariabledefinitions?$select=environmentvariabledefinitionid,defaultvalue"),
            apiGet("environmentvariablevalues?$select=_environmentvariabledefinitionid_value,value")
        ]).then(function (results) {
            var defs = results[0].value || [];
            var vals = results[1].value || [];

            // Build lookup: defId -> has a non-empty current value, using the same
            // envVarHasCurrentValue predicate the grid uses for its "Set" badge. A default value
            // alone does NOT count here - that was the source of the "5 Set, 0 Not Set" tile vs.
            // grid mismatch, since the grid only marks a row "Set" when it has a current value
            // override (falling back to a "Default" badge, not "Set", when only defaultvalue is
            // present).
            var valMap = {};
            vals.forEach(function (v) {
                if (envVarHasCurrentValue(v.value)) {
                    valMap[v._environmentvariabledefinitionid_value] = true;
                }
            });

            var total = defs.length;
            var set = 0, notSet = 0;
            defs.forEach(function (d) {
                if (valMap[d.environmentvariabledefinitionid]) {
                    set++;
                } else {
                    notSet++;
                }
            });

            renderSummaryCard('mcCardEnvVars', total,
                '<span class="set-count">' + set + ' Set</span>' +
                '<span class="notset-count">' + notSet + ' Not Set</span>');
        }).catch(function () { renderSummaryCardError('mcCardEnvVars'); });
    }

    // Recently Modified Components (from crawled data)
    function loadMCRecentComponents() {
        var container = document.getElementById('mcRecentList');
        container.innerHTML = '<div class="loading-state"><span class="spinner"></span> Loading...</div>';

        apiGet("vb_components?$orderby=vb_source_modified_on desc&$top=10&$select=vb_name,vb_component_type,vb_source_modified_on")
            .then(function (data) {
                var records = data.value || [];
                if (records.length === 0) {
                    container.innerHTML = '<div class="empty-state"><h4>No crawled data yet</h4><p>Run a crawl from the Cartographer app to populate component data.</p></div>';
                    return;
                }

                var html = '<ul class="recent-list">';
                records.forEach(function (r) {
                    var typeLabel = COMPONENT_TYPE_LABELS[r.vb_component_type] || 'Unknown';
                    html += '<li class="recent-item">' +
                        '<span class="name" title="' + htmlEncode(r.vb_name) + '">' + htmlEncode(r.vb_name) + '</span>' +
                        '<span class="meta">' +
                        '<span class="badge badge-type">' + htmlEncode(typeLabel) + '</span>' +
                        '<span>' + formatDate(r.vb_source_modified_on) + '</span>' +
                        '</span></li>';
                });
                html += '</ul>';
                container.innerHTML = html;
            })
            .catch(function (err) {
                container.innerHTML = '<div class="empty-state"><h4>Unable to load components</h4><p>' + htmlEncode(err.message) + '</p></div>';
            });
    }

    // Component Breakdown (from crawled data)
    function loadMCBreakdown() {
        var container = document.getElementById('mcBreakdown');
        container.innerHTML = '<div class="loading-state"><span class="spinner"></span> Loading...</div>';

        apiGet("vb_components?$select=vb_component_type")
            .then(function (data) {
                var records = data.value || [];
                if (records.length === 0) {
                    container.innerHTML = '<div class="empty-state"><h4>No crawled data yet</h4><p>Run a crawl to see the component breakdown.</p></div>';
                    return;
                }

                // Count by type
                var counts = {};
                records.forEach(function (r) {
                    var t = r.vb_component_type;
                    counts[t] = (counts[t] || 0) + 1;
                });

                // Sort by count descending
                var sorted = Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a]; });

                var html = '<div class="breakdown-grid">';
                sorted.forEach(function (key) {
                    var label = COMPONENT_TYPE_LABELS[key] || 'Unknown (' + key + ')';
                    html += '<div class="breakdown-item">' +
                        '<span class="type-name" title="' + htmlEncode(label) + '">' + htmlEncode(label) + '</span>' +
                        '<span class="type-count">' + counts[key] + '</span></div>';
                });
                html += '</div>';
                container.innerHTML = html;
            })
            .catch(function (err) {
                container.innerHTML = '<div class="empty-state"><h4>Unable to load breakdown</h4><p>' + htmlEncode(err.message) + '</p></div>';
            });
    }

    // =========================================================================
    // CLOUD FLOW MONITOR
    // =========================================================================
    var allCloudFlows = [];
    var cfSearchInput = document.getElementById('cfSearch');
    var cfSearchTimer = null;

    cfSearchInput.addEventListener('input', function () {
        clearTimeout(cfSearchTimer);
        cfSearchTimer = setTimeout(function () { renderCloudFlows(); }, 300);
    });

    // Cloud-flow analogue of isPlatformPluginStepForGuard, for the same reason: this tab was
    // offering one-click Enable/Disable on Microsoft's own platform flows (observed in this
    // environment: SLAInstanceMonitoringWarningAndExpiryFlow, "Integrated Search API trigger
    // flow", "Search Dynamics 365 knowledge article flow", all ismanaged=true) with no
    // platform/custom distinction anywhere. There is no plugin-type assembly to inspect for a
    // flow, only its own name, so a Microsoft/System name prefix is checked as a hint and
    // ismanaged is the definitive signal - erring toward "platform" the same way the plugin step
    // guard does, so a managed flow (including a future Cartographer one) is treated as platform
    // on purpose.
    function isPlatformFlowForGuard(flow) {
        var name = flow.name || '';
        if (/^(microsoft|system)/i.test(name)) return true;
        return flow.ismanaged === true;
    }

    function loadCloudFlows() {
        var body = document.getElementById('cfBody');
        body.innerHTML = '<tr><td colspan="5"><div class="loading-state"><span class="spinner"></span> Loading cloud flows...</div></td></tr>';

        apiGet("workflows?$filter=category eq 5&$select=name,statecode,statuscode,primaryentity,modifiedon,createdon,workflowid,ismanaged&$orderby=name")
            .then(function (data) {
                allCloudFlows = data.value || [];
                renderCloudFlows();
            })
            .catch(function (err) {
                body.innerHTML = '<tr><td colspan="5"><div class="empty-state"><h4>Failed to load cloud flows</h4><p>' + htmlEncode(err.message) + '</p></div></td></tr>';
            });
    }

    function renderCloudFlows() {
        var body = document.getElementById('cfBody');
        var countEl = document.getElementById('cfCount');
        var search = (cfSearchInput.value || '').trim().toLowerCase();

        var filtered = allCloudFlows;
        if (search) {
            filtered = allCloudFlows.filter(function (f) {
                return (f.name || '').toLowerCase().indexOf(search) >= 0;
            });
        }

        countEl.textContent = filtered.length + ' flow' + (filtered.length !== 1 ? 's' : '');

        if (filtered.length === 0) {
            body.innerHTML = '<tr><td colspan="5"><div class="empty-state"><h4>No flows found</h4><p>' +
                (search ? 'Try a different search term.' : 'No cloud flows in this environment.') + '</p></div></td></tr>';
            return;
        }

        var html = '';
        filtered.forEach(function (flow) {
            var isOn = flow.statecode === 1;
            var isPlatform = isPlatformFlowForGuard(flow);
            var statusBadge = isOn
                ? '<span class="badge badge-on">On</span>'
                : '<span class="badge badge-off">Off</span>';
            var toggleClass = isOn ? 'btn-toggle is-on' : 'btn-toggle is-off';
            var toggleLabel = isOn ? 'Disable' : 'Enable';

            html += '<tr>' +
                '<td class="col-name" title="' + htmlEncode(flow.name) + '">' + htmlEncode(flow.name) + '</td>' +
                '<td class="col-mono">' + htmlEncode(entityLabel(flow.primaryentity)) + '</td>' +
                '<td>' + statusBadge + '</td>' +
                '<td class="col-date">' + formatDate(flow.modifiedon) + '</td>' +
                '<td><div style="display:flex;gap:6px;">' +
                '<button class="btn ' + toggleClass + '" data-flowid="' + flow.workflowid + '" data-flowname="' + htmlEncode(flow.name) + '" data-ison="' + isOn + '" data-isplatform="' + isPlatform + '">' + toggleLabel + '</button>' +
                '<button class="btn" data-action="history" data-flowid="' + flow.workflowid + '" data-flowname="' + htmlEncode(flow.name) + '">History</button>' +
                '</div></td>' +
                '</tr>';
        });

        body.innerHTML = html;

        // Attach toggle handlers
        body.querySelectorAll('.btn-toggle').forEach(function (btn) {
            btn.addEventListener('click', function () {
                var flowId = btn.getAttribute('data-flowid');
                var flowName = btn.getAttribute('data-flowname');
                var isOn = btn.getAttribute('data-ison') === 'true';
                var isPlatform = btn.getAttribute('data-isplatform') === 'true';
                var action = isOn ? 'disable' : 'enable';

                // Enabling or disabling a platform/system flow can break core platform
                // functionality for every user in the environment - the same class of problem as a
                // platform plugin step - so require the admin to type ENABLE/DISABLE rather than a
                // single click. Everything else keeps the normal one-click-confirm dialog: the goal
                // is to make the dangerous action harder, not to hide the capability.
                if (isPlatform) {
                    showConfirm(
                        (isOn ? 'Disable' : 'Enable') + ' Platform Cloud Flow',
                        '“' + flowName + '” is a Microsoft/managed platform component, not part of your customizations. ' + (isOn ? 'Disabling' : 'Enabling') + ' it can break core platform functionality for every user in this environment.',
                        function () { toggleCloudFlow(btn, flowId, flowName, !isOn); },
                        { requireTypedWord: isOn ? 'DISABLE' : 'ENABLE', warning: 'This flow is platform-owned, not something your solution created.' }
                    );
                } else {
                    showConfirm(
                        (isOn ? 'Disable' : 'Enable') + ' Cloud Flow',
                        'Are you sure you want to ' + action + ' \'' + flowName + '\'?',
                        function () { toggleCloudFlow(btn, flowId, flowName, !isOn); }
                    );
                }
            });
        });

        // Attach run-history handlers
        body.querySelectorAll('[data-action="history"]').forEach(function (btn) {
            btn.addEventListener('click', function () {
                var flowId = btn.getAttribute('data-flowid');
                var flowName = btn.getAttribute('data-flowname');
                showFlowRunHistory(flowId, flowName);
            });
        });
    }

    function toggleCloudFlow(btn, flowId, flowName, enable) {
        btn.disabled = true;
        btn.innerHTML = '<span class="spinner spinner-sm"></span>';

        apiPost('vb_ToggleComponent', {
            ComponentType: 'CloudFlow',
            ComponentId: flowId,
            Enable: enable
        }).then(function () {
            showToast(flowName + ' ' + (enable ? 'enabled' : 'disabled') + ' successfully.', 'success');
            // Update local data
            allCloudFlows.forEach(function (f) {
                if (f.workflowid === flowId) {
                    f.statecode = enable ? 1 : 0;
                }
            });
            renderCloudFlows();
        }).catch(function (err) {
            showToast('Failed to ' + (enable ? 'enable' : 'disable') + ' ' + flowName + ': ' + err.message, 'error');
            btn.disabled = false;
            btn.textContent = enable ? 'Enable' : 'Disable';
        });
    }

    // =========================================================================
    // CLOUD FLOW RUN HISTORY
    // =========================================================================
    var historyOverlay = document.getElementById('historyOverlay');
    var historyTitleEl = document.getElementById('historyTitle');
    var historyBodyEl = document.getElementById('historyBody');
    var historyCloseBtn = document.getElementById('historyClose');

    historyCloseBtn.addEventListener('click', hideFlowRunHistory);

    // Close run history on Escape, same as the confirm dialog.
    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && historyOverlay.style.display === 'flex') {
            hideFlowRunHistory();
        }
    });

    function hideFlowRunHistory() {
        historyOverlay.style.display = 'none';
    }

    function showFlowRunHistory(flowId, flowName) {
        historyTitleEl.textContent = 'Run History — ' + flowName;
        historyBodyEl.innerHTML = '<div class="loading-state"><span class="spinner"></span> Loading run history...</div>';
        historyOverlay.style.display = 'flex';

        apiPost('vb_GetFlowRunHistory', { WorkflowId: flowId, Count: '25' })
            .then(function (result) { renderFlowRunHistory(result); })
            .catch(function (err) {
                historyBodyEl.innerHTML = '<div class="empty-state"><h4>Failed to load run history</h4><p>' + htmlEncode(err.message) + '</p></div>';
            });
    }

    // Badge class for a flowsession status string, as mapped by FlowRunHistoryPlugin.
    // MapFlowSessionStatus (NotSpecified/Paused/Running/Waiting/Succeeded/Failed/Cancelled/
    // Skipped/Suspended/Unknown).
    function flowRunStatusBadgeClass(status) {
        if (status === 'Succeeded') return 'badge-run-ok';
        if (status === 'Failed') return 'badge-run-fail';
        if (status === 'Running' || status === 'Waiting') return 'badge-run-active';
        return 'badge-run-other';
    }

    // Renders the vb_GetFlowRunHistory custom API response: { Success, ResultJson } where
    // ResultJson is a JSON array of { runId, status, startedOn, completedOn, duration,
    // errorMessage } (see FlowRunHistoryPlugin.cs). Success=false (bad WorkflowId, no license, or
    // an internal error) always still carries a parseable ResultJson ("[]" per SetResult), but we
    // treat it as a failure state rather than silently rendering an empty table. Tolerant of
    // malformed JSON so a bad response never throws past this function.
    function renderFlowRunHistory(result) {
        if (!result || result.Success !== true) {
            historyBodyEl.innerHTML = '<div class="empty-state"><h4>Unable to load run history</h4>' +
                '<p>The flow run history could not be retrieved. This can happen if the Cartographer license is inactive or the flow has no run history.</p></div>';
            return;
        }

        var runs;
        try {
            runs = JSON.parse(result.ResultJson || '[]');
        } catch (e) {
            historyBodyEl.innerHTML = '<div class="empty-state"><h4>Unable to read run history</h4><p>The server returned a response that could not be parsed.</p></div>';
            return;
        }

        if (!runs || !runs.length) {
            historyBodyEl.innerHTML = '<div class="empty-state"><h4>No runs found</h4><p>This flow has not run yet, or its run history has been purged.</p></div>';
            return;
        }

        var html = '<table class="data-table"><thead><tr>' +
            '<th>Status</th><th>Started</th><th>Completed</th><th>Duration</th><th>Error</th>' +
            '</tr></thead><tbody>';

        runs.forEach(function (run) {
            var status = (run && run.status) || 'Unknown';
            var errorMessage = run && run.errorMessage;
            html += '<tr>' +
                '<td><span class="badge ' + flowRunStatusBadgeClass(status) + '">' + htmlEncode(status) + '</span></td>' +
                '<td class="col-date">' + formatDate(run && run.startedOn) + '</td>' +
                '<td class="col-date">' + formatDate(run && run.completedOn) + '</td>' +
                '<td>' + htmlEncode((run && run.duration) || '--') + '</td>' +
                '<td title="' + htmlEncode(errorMessage || '') + '">' + (errorMessage ? htmlEncode(truncate(errorMessage, 80)) : '--') + '</td>' +
                '</tr>';
        });

        html += '</tbody></table>';
        historyBodyEl.innerHTML = html;
    }

    // =========================================================================
    // PLUGIN STEP MANAGER
    // =========================================================================
    var allPluginSteps = [];
    var psSearchInput = document.getElementById('psSearch');
    var psScopeFilter = document.getElementById('psScopeFilter');
    var psStageFilter = document.getElementById('psStageFilter');
    var psSearchTimer = null;

    psSearchInput.addEventListener('input', function () {
        clearTimeout(psSearchTimer);
        psSearchTimer = setTimeout(function () { renderPluginSteps(); }, 300);
    });

    psScopeFilter.addEventListener('change', function () { renderPluginSteps(); });
    psStageFilter.addEventListener('change', function () { renderPluginSteps(); });

    function loadPluginSteps() {
        var body = document.getElementById('psBody');
        body.innerHTML = '<tr><td colspan="8"><div class="loading-state"><span class="spinner"></span> Loading plugin steps...</div></td></tr>';

        // eventhandler is a polymorphic lookup (plugintype for a code plugin, or serviceendpoint
        // for an Azure Service Bus step); expanding it via its type-specific navigation property
        // (eventhandler_plugintype) surfaces the plugin's assembly and type name, which both
        // isPlatformPluginStepForFilter (the default "Custom Steps Only" scope and the Owner
        // badge, below) and isPlatformPluginStepForGuard (the disable confirmation) use to tell
        // Microsoft's own steps apart from a customer's or ISV's (e.g. Cartographer's) - ismanaged
        // alone can't do that for the filter, since a managed ISV solution's steps are
        // ismanaged=true too.
        apiGet("sdkmessageprocessingsteps?$filter=ishidden/Value eq false&$select=name,stage,mode,statecode,rank,sdkmessageprocessingstepid,ismanaged&$expand=sdkmessageid($select=name),eventhandler_plugintype($select=typename,assemblyname)&$orderby=name")
            .then(function (data) {
                allPluginSteps = data.value || [];
                renderPluginSteps();
            })
            .catch(function (err) {
                body.innerHTML = '<tr><td colspan="8"><div class="empty-state"><h4>Failed to load plugin steps</h4><p>' + htmlEncode(err.message) + '</p></div></td></tr>';
            });
    }

    function renderPluginSteps() {
        var body = document.getElementById('psBody');
        var countEl = document.getElementById('psCount');
        var search = (psSearchInput.value || '').trim().toLowerCase();
        var scopeVal = psScopeFilter.value; // 'custom' (default) or 'all'
        var stageVal = psStageFilter.value;

        var platformTotal = allPluginSteps.filter(function (s) { return isPlatformPluginStepForFilter(s); }).length;

        var filtered = allPluginSteps.filter(function (s) {
            if (scopeVal === 'custom' && isPlatformPluginStepForFilter(s)) return false;
            if (search && (s.name || '').toLowerCase().indexOf(search) < 0) return false;
            if (stageVal && String(s.stage) !== stageVal) return false;
            return true;
        });

        var countText = filtered.length + ' step' + (filtered.length !== 1 ? 's' : '');
        if (scopeVal === 'custom' && platformTotal > 0) {
            countText += ' (' + platformTotal + ' platform step' + (platformTotal !== 1 ? 's' : '') + ' hidden)';
        }
        countEl.textContent = countText;

        if (filtered.length === 0) {
            body.innerHTML = '<tr><td colspan="8"><div class="empty-state"><h4>No plugin steps found</h4><p>Try adjusting your filters.</p></div></td></tr>';
            return;
        }

        var html = '';
        filtered.forEach(function (step) {
            var isEnabled = step.statecode === 0;
            // Owner badge uses the Filter predicate ("is this somebody else's code?"); the button's
            // data-isplatform uses the Guard predicate ("how scary should the confirmation be?") -
            // they can legitimately disagree on a Cartographer step, which is intentional.
            var isPlatformBadge = isPlatformPluginStepForFilter(step);
            var isPlatformGuard = isPlatformPluginStepForGuard(step);
            var statusBadge = isEnabled
                ? '<span class="badge badge-enabled">Enabled</span>'
                : '<span class="badge badge-disabled">Disabled</span>';
            var ownerBadge = isPlatformBadge
                ? '<span class="badge badge-platform" title="Registered under a Microsoft.* / System.* plugin assembly - ships with Dataverse, not part of your customizations.">Platform</span>'
                : '<span class="badge badge-custom">Custom</span>';
            var modeBadge = step.mode === 0
                ? '<span class="badge badge-sync">Sync</span>'
                : '<span class="badge badge-async">Async</span>';
            var toggleClass = isEnabled ? 'btn-toggle is-on' : 'btn-toggle is-off';
            var toggleLabel = isEnabled ? 'Disable' : 'Enable';
            var messageName = (step.sdkmessageid && step.sdkmessageid.name) || '--';
            var friendly = friendlyPluginName(step.name);

            html += '<tr>' +
                '<td class="col-name" title="' + htmlEncode(step.name) + '">' + htmlEncode(friendly) + '</td>' +
                '<td>' + ownerBadge + '</td>' +
                '<td>' + htmlEncode(messageName) + '</td>' +
                '<td><span class="badge badge-stage">' + stageLabel(step.stage) + '</span></td>' +
                '<td>' + modeBadge + '</td>' +
                '<td>' + statusBadge + '</td>' +
                '<td style="text-align:center;">' + (step.rank != null ? step.rank : '--') + '</td>' +
                '<td><button class="btn ' + toggleClass + '" data-stepid="' + step.sdkmessageprocessingstepid + '" data-stepname="' + htmlEncode(friendly) + '" data-isenabled="' + isEnabled + '" data-isplatform="' + isPlatformGuard + '">' + toggleLabel + '</button></td>' +
                '</tr>';
        });

        body.innerHTML = html;

        // Attach toggle handlers
        body.querySelectorAll('.btn-toggle').forEach(function (btn) {
            btn.addEventListener('click', function () {
                var stepId = btn.getAttribute('data-stepid');
                var stepName = btn.getAttribute('data-stepname');
                var isEnabled = btn.getAttribute('data-isenabled') === 'true';
                var isPlatform = btn.getAttribute('data-isplatform') === 'true';
                var action = isEnabled ? 'disable' : 'enable';

                // Disabling a platform/system step can break core platform functionality for
                // every user in the environment - require the admin to type DISABLE rather than
                // a single click. Everything else keeps the normal one-click-confirm dialog: the
                // goal is to make the dangerous action harder, not to hide the capability.
                if (action === 'disable' && isPlatform) {
                    showConfirm(
                        'Disable Platform Plugin Step',
                        '“' + stepName + '” is a Microsoft/managed platform component, not part of your customizations. Disabling it can break core platform functionality for every user in this environment.',
                        function () { togglePluginStep(btn, stepId, stepName, !isEnabled); },
                        { requireTypedWord: 'DISABLE', warning: 'This step is platform-owned, not something your solution created.' }
                    );
                } else {
                    showConfirm(
                        (isEnabled ? 'Disable' : 'Enable') + ' Plugin Step',
                        'Are you sure you want to ' + action + ' \'' + stepName + '\'?',
                        function () { togglePluginStep(btn, stepId, stepName, !isEnabled); }
                    );
                }
            });
        });
    }

    function togglePluginStep(btn, stepId, stepName, enable) {
        btn.disabled = true;
        btn.innerHTML = '<span class="spinner spinner-sm"></span>';

        apiPost('vb_ToggleComponent', {
            ComponentType: 'PluginStep',
            ComponentId: stepId,
            Enable: enable
        }).then(function () {
            showToast(stepName + ' ' + (enable ? 'enabled' : 'disabled') + ' successfully.', 'success');
            allPluginSteps.forEach(function (s) {
                if (s.sdkmessageprocessingstepid === stepId) {
                    s.statecode = enable ? 0 : 1;
                }
            });
            renderPluginSteps();
        }).catch(function (err) {
            showToast('Failed to ' + (enable ? 'enable' : 'disable') + ' ' + stepName + ': ' + err.message, 'error');
            btn.disabled = false;
            btn.textContent = enable ? 'Enable' : 'Disable';
        });
    }

    // =========================================================================
    // ENVIRONMENT VARIABLES
    // =========================================================================
    var allEnvVarDefs = [];
    var envVarValueMap = {};   // defId -> { value, valueId }
    var evSearchInput = document.getElementById('evSearch');
    var evSearchTimer = null;
    var editingEnvVarId = null;

    evSearchInput.addEventListener('input', function () {
        clearTimeout(evSearchTimer);
        evSearchTimer = setTimeout(function () { renderEnvVariables(); }, 300);
    });

    function loadEnvVariables() {
        var body = document.getElementById('evBody');
        body.innerHTML = '<tr><td colspan="7"><div class="loading-state"><span class="spinner"></span> Loading environment variables...</div></td></tr>';

        Promise.all([
            apiGet("environmentvariabledefinitions?$select=schemaname,displayname,type,defaultvalue,environmentvariabledefinitionid&$orderby=displayname"),
            apiGet("environmentvariablevalues?$select=_environmentvariabledefinitionid_value,value,environmentvariablevalueid")
        ]).then(function (results) {
            allEnvVarDefs = results[0].value || [];
            var vals = results[1].value || [];

            envVarValueMap = {};
            vals.forEach(function (v) {
                envVarValueMap[v._environmentvariabledefinitionid_value] = {
                    value: v.value,
                    valueId: v.environmentvariablevalueid
                };
            });

            renderEnvVariables();
        }).catch(function (err) {
            body.innerHTML = '<tr><td colspan="7"><div class="empty-state"><h4>Failed to load environment variables</h4><p>' + htmlEncode(err.message) + '</p></div></td></tr>';
        });
    }

    function renderEnvVariables() {
        var body = document.getElementById('evBody');
        var countEl = document.getElementById('evCount');
        var search = (evSearchInput.value || '').trim().toLowerCase();

        var filtered = allEnvVarDefs.filter(function (d) {
            if (!search) return true;
            return (d.displayname || '').toLowerCase().indexOf(search) >= 0 ||
                   (d.schemaname || '').toLowerCase().indexOf(search) >= 0;
        });

        countEl.textContent = filtered.length + ' variable' + (filtered.length !== 1 ? 's' : '');

        if (filtered.length === 0) {
            body.innerHTML = '<tr><td colspan="7"><div class="empty-state"><h4>No variables found</h4><p>' +
                (search ? 'Try a different search term.' : 'No environment variables in this environment.') + '</p></div></td></tr>';
            return;
        }

        var html = '';
        filtered.forEach(function (def) {
            var id = def.environmentvariabledefinitionid;
            var valEntry = envVarValueMap[id];
            var currentValue = valEntry ? valEntry.value : null;
            var defaultValue = def.defaultvalue;
            var typeLabel = getEnvVarTypeLabel(def.type);
            var isSecret = def.type === 10001005;

            // Determine status via the same envVarHasCurrentValue predicate the Dashboard tile
            // uses, so the two can never drift apart again.
            var statusBadge, statusClass;
            if (envVarHasCurrentValue(currentValue)) {
                statusBadge = '<span class="badge badge-set">Set</span>';
                statusClass = 'set';
            } else if (defaultValue !== null && defaultValue !== undefined && defaultValue !== '') {
                statusBadge = '<span class="badge badge-default">Default</span>';
                statusClass = 'default';
            } else {
                statusBadge = '<span class="badge badge-notset">NOT SET</span>';
                statusClass = 'notset';
            }

            // Display values (mask secrets)
            var displayDefault = defaultValue != null ? truncate(String(defaultValue), 50) : '--';
            var displayCurrent;
            if (isSecret && currentValue != null) {
                displayCurrent = '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022';
            } else {
                displayCurrent = currentValue != null ? truncate(String(currentValue), 50) : '--';
            }

            // Editing state
            var isEditing = editingEnvVarId === id;

            var currentValueCell;
            if (isEditing) {
                var editVal = isSecret ? '' : (currentValue || defaultValue || '');
                currentValueCell = '<td>' +
                    '<div class="inline-edit-group">' +
                    '<input type="' + (isSecret ? 'password' : 'text') + '" class="inline-edit-input" id="ev-edit-input-' + id + '" value="' + htmlEncode(editVal) + '" placeholder="Enter value..." />' +
                    '<button class="btn btn-primary" data-action="save" data-defid="' + id + '" style="padding:5px 10px;">Save</button>' +
                    '<button class="btn" data-action="cancel-edit" style="padding:5px 10px;">Cancel</button>' +
                    '</div></td>';
            } else {
                currentValueCell = '<td title="' + htmlEncode(currentValue || '') + '">' + htmlEncode(displayCurrent) + '</td>';
            }

            // Action buttons
            var actionsHtml = '';
            if (!isEditing) {
                actionsHtml = '<button class="btn" data-action="edit" data-defid="' + id + '">Edit</button>';
                if (valEntry) {
                    actionsHtml += ' <button class="btn btn-danger" data-action="reset" data-defid="' + id + '" data-defname="' + htmlEncode(def.displayname || def.schemaname) + '">Reset</button>';
                }
            }

            html += '<tr>' +
                '<td class="col-name" title="' + htmlEncode(def.displayname) + '">' + htmlEncode(def.displayname || '--') + '</td>' +
                '<td class="col-mono">' + htmlEncode(def.schemaname) + '</td>' +
                '<td><span class="badge badge-type">' + htmlEncode(typeLabel) + '</span></td>' +
                '<td title="' + htmlEncode(defaultValue || '') + '">' + htmlEncode(displayDefault) + '</td>' +
                currentValueCell +
                '<td>' + statusBadge + '</td>' +
                '<td>' + actionsHtml + '</td>' +
                '</tr>';
        });

        body.innerHTML = html;

        // Attach event handlers
        body.querySelectorAll('[data-action="edit"]').forEach(function (btn) {
            btn.addEventListener('click', function () {
                editingEnvVarId = btn.getAttribute('data-defid');
                renderEnvVariables();
                // Focus the input
                var input = document.getElementById('ev-edit-input-' + editingEnvVarId);
                if (input) input.focus();
            });
        });

        body.querySelectorAll('[data-action="cancel-edit"]').forEach(function (btn) {
            btn.addEventListener('click', function () {
                editingEnvVarId = null;
                renderEnvVariables();
            });
        });

        body.querySelectorAll('[data-action="save"]').forEach(function (btn) {
            btn.addEventListener('click', function () {
                var defId = btn.getAttribute('data-defid');
                var input = document.getElementById('ev-edit-input-' + defId);
                if (input) saveEnvVariable(btn, defId, input.value);
            });
        });

        body.querySelectorAll('[data-action="reset"]').forEach(function (btn) {
            btn.addEventListener('click', function () {
                var defId = btn.getAttribute('data-defid');
                var defName = btn.getAttribute('data-defname');
                showConfirm(
                    'Reset to Default',
                    'Are you sure you want to reset \'' + defName + '\' to its default value? The current override will be removed.',
                    function () { resetEnvVariable(btn, defId, defName); }
                );
            });
        });

        // Handle Enter key in edit inputs
        body.querySelectorAll('.inline-edit-input').forEach(function (input) {
            input.addEventListener('keydown', function (e) {
                if (e.key === 'Enter') {
                    var defId = input.id.replace('ev-edit-input-', '');
                    var saveBtn = body.querySelector('[data-action="save"][data-defid="' + defId + '"]');
                    if (saveBtn) saveBtn.click();
                } else if (e.key === 'Escape') {
                    editingEnvVarId = null;
                    renderEnvVariables();
                }
            });
        });
    }

    function getEnvVarTypeLabel(type) {
        return ENV_VAR_TYPE_LABELS[type] || 'Unknown';
    }

    function saveEnvVariable(btn, defId, newValue) {
        btn.disabled = true;
        btn.innerHTML = '<span class="spinner spinner-sm"></span>';

        apiPost('vb_UpdateEnvVariable', {
            DefinitionId: defId,
            NewValue: newValue
        }).then(function () {
            showToast('Environment variable updated successfully.', 'success');
            editingEnvVarId = null;
            // Update local data
            if (!envVarValueMap[defId]) {
                envVarValueMap[defId] = { value: newValue, valueId: null };
            } else {
                envVarValueMap[defId].value = newValue;
            }
            renderEnvVariables();
        }).catch(function (err) {
            showToast('Failed to update variable: ' + err.message, 'error');
            btn.disabled = false;
            btn.textContent = 'Save';
        });
    }

    function resetEnvVariable(btn, defId, defName) {
        btn.disabled = true;
        btn.innerHTML = '<span class="spinner spinner-sm"></span>';

        apiPost('vb_ResetEnvVariable', {
            DefinitionId: defId
        }).then(function () {
            showToast('\'' + defName + '\' reset to default.', 'success');
            delete envVarValueMap[defId];
            editingEnvVarId = null;
            renderEnvVariables();
        }).catch(function (err) {
            showToast('Failed to reset variable: ' + err.message, 'error');
            btn.disabled = false;
            btn.textContent = 'Reset';
        });
    }

    // =========================================================================
    // BUSINESS RULES
    // =========================================================================
    var allBusinessRules = [];
    var brSearchInput = document.getElementById('brSearch');
    var brSearchTimer = null;

    brSearchInput.addEventListener('input', function () {
        clearTimeout(brSearchTimer);
        brSearchTimer = setTimeout(function () { renderBusinessRules(); }, 300);
    });

    function loadBusinessRules() {
        var body = document.getElementById('brBody');
        body.innerHTML = '<tr><td colspan="5"><div class="loading-state"><span class="spinner"></span> Loading business rules...</div></td></tr>';

        apiGet("workflows?$filter=category eq 2&$select=name,primaryentity,statecode,statuscode,modifiedon,workflowid&$orderby=name")
            .then(function (data) {
                allBusinessRules = data.value || [];
                renderBusinessRules();
            })
            .catch(function (err) {
                body.innerHTML = '<tr><td colspan="5"><div class="empty-state"><h4>Failed to load business rules</h4><p>' + htmlEncode(err.message) + '</p></div></td></tr>';
            });
    }

    function renderBusinessRules() {
        var body = document.getElementById('brBody');
        var countEl = document.getElementById('brCount');
        var search = (brSearchInput.value || '').trim().toLowerCase();

        var filtered = allBusinessRules.filter(function (r) {
            if (!search) return true;
            return (r.name || '').toLowerCase().indexOf(search) >= 0 ||
                   (r.primaryentity || '').toLowerCase().indexOf(search) >= 0;
        });

        countEl.textContent = filtered.length + ' rule' + (filtered.length !== 1 ? 's' : '');

        if (filtered.length === 0) {
            body.innerHTML = '<tr><td colspan="5"><div class="empty-state"><h4>No business rules found</h4><p>' +
                (search ? 'Try a different search term.' : 'No business rules in this environment.') + '</p></div></td></tr>';
            return;
        }

        var html = '';
        filtered.forEach(function (rule) {
            var isActive = rule.statecode === 1;
            var statusBadge = isActive
                ? '<span class="badge badge-active">Active</span>'
                : '<span class="badge badge-inactive">Inactive</span>';
            var toggleClass = isActive ? 'btn-toggle is-on' : 'btn-toggle is-off';
            var toggleLabel = isActive ? 'Deactivate' : 'Activate';

            html += '<tr>' +
                '<td class="col-name" title="' + htmlEncode(rule.name) + '">' + htmlEncode(rule.name) + '</td>' +
                '<td class="col-mono">' + htmlEncode(entityLabel(rule.primaryentity)) + '</td>' +
                '<td>' + statusBadge + '</td>' +
                '<td class="col-date">' + formatDate(rule.modifiedon) + '</td>' +
                '<td><button class="btn ' + toggleClass + '" data-ruleid="' + rule.workflowid + '" data-rulename="' + htmlEncode(rule.name) + '" data-isactive="' + isActive + '">' + toggleLabel + '</button></td>' +
                '</tr>';
        });

        body.innerHTML = html;

        // Attach toggle handlers
        body.querySelectorAll('.btn-toggle').forEach(function (btn) {
            btn.addEventListener('click', function () {
                var ruleId = btn.getAttribute('data-ruleid');
                var ruleName = btn.getAttribute('data-rulename');
                var isActive = btn.getAttribute('data-isactive') === 'true';
                var action = isActive ? 'deactivate' : 'activate';

                showConfirm(
                    (isActive ? 'Deactivate' : 'Activate') + ' Business Rule',
                    'Are you sure you want to ' + action + ' \'' + ruleName + '\'?',
                    function () { toggleBusinessRule(btn, ruleId, ruleName, !isActive); }
                );
            });
        });
    }

    function toggleBusinessRule(btn, ruleId, ruleName, activate) {
        btn.disabled = true;
        btn.innerHTML = '<span class="spinner spinner-sm"></span>';

        apiPost('vb_ToggleComponent', {
            ComponentType: 'BusinessRule',
            ComponentId: ruleId,
            Enable: activate
        }).then(function () {
            showToast(ruleName + ' ' + (activate ? 'activated' : 'deactivated') + ' successfully.', 'success');
            allBusinessRules.forEach(function (r) {
                if (r.workflowid === ruleId) {
                    r.statecode = activate ? 1 : 0;
                }
            });
            renderBusinessRules();
        }).catch(function (err) {
            showToast('Failed to ' + (activate ? 'activate' : 'deactivate') + ' ' + ruleName + ': ' + err.message, 'error');
            btn.disabled = false;
            btn.textContent = activate ? 'Activate' : 'Deactivate';
        });
    }

    // =========================================================================
    // Initialize — Compass is no longer gated by tier (Cartographer's free tier
    // is now the feature superset; the old Professional/Enterprise gate is gone).
    // =========================================================================
    tabLoadedMap['mission-control'] = true;
    loadMissionControl();

})();
</script>

</body>
</html>
