<!-- Interactive Action Buttons — Jira Ticket Creation & Triage Status Updates
     Include this partial in report templates to enable interactive features.
     Requires: findings/components with internal_id, component_id, project_version_id -->

<style>
/* === Action Buttons === */
.action-btn {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 3px 8px; font-size: 0.75em; cursor: pointer;
    border: 1px solid #dee2e6; border-radius: 4px;
    background: #fff; color: #495057; transition: all 0.15s;
    white-space: nowrap; vertical-align: middle;
}
.action-btn:hover { background: #f8f9fa; border-color: #adb5bd; }
.action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.action-btn.btn-jira { border-color: #0052cc; color: #0052cc; }
.action-btn.btn-jira:hover { background: #e6f0ff; }
.action-btn.btn-triage { border-color: #fd7e14; color: #fd7e14; }
.action-btn.btn-triage:hover { background: #fff3e6; }
.action-btn.btn-success { border-color: #28a745; color: #28a745; background: #e6ffe6; opacity: 1; }
.action-btn.btn-intriage { border-color: #e6a817; color: #856404; background: #fff8e1; opacity: 1; }
.action-btn.btn-error { border-color: #dc3545; color: #dc3545; background: #ffe6e6; }
.action-btn .spinner {
    display: inline-block; width: 12px; height: 12px;
    border: 2px solid currentColor; border-top-color: transparent;
    border-radius: 50%; animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.action-cell { white-space: nowrap; }
.action-cell .action-btn + .action-btn { margin-left: 4px; }

/* === Connection Badge (top-right corner) === */
#fsConnectionBadge {
    position: fixed; top: 12px; right: 16px; z-index: 9000;
}
.fs-connected-badge {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 4px 12px; font-size: 0.8em; border-radius: 12px;
    background: #e6ffe6; color: #28a745; border: 1px solid #28a745;
    box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.fs-connect-btn {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 4px 12px; font-size: 0.8em; border-radius: 12px;
    background: #fff; color: #0052cc; border: 1px solid #0052cc;
    cursor: pointer; box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.fs-connect-btn:hover { background: #e6f0ff; }
@media print { #fsConnectionBadge { display: none !important; } }

/* === Modal Overlay === */
.fs-modal-overlay {
    display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
    background: rgba(0,0,0,0.5); z-index: 9999;
    justify-content: center; align-items: center;
}
.fs-modal-overlay.active { display: flex; }
.fs-modal {
    background: #fff; border-radius: 10px; box-shadow: 0 8px 32px rgba(0,0,0,0.2);
    max-width: 560px; width: 90%; max-height: 85vh; overflow-y: auto;
    padding: 28px; position: relative;
}
.fs-modal h3 { margin: 0 0 18px; color: #2c3e50; font-size: 1.3em; }
.fs-modal label { display: block; font-size: 0.85em; color: #495057; margin-bottom: 4px; font-weight: 600; }
.fs-modal input, .fs-modal select, .fs-modal textarea {
    width: 100%; padding: 8px 10px; border: 1px solid #ced4da; border-radius: 6px;
    font-size: 0.9em; margin-bottom: 14px; font-family: inherit;
}
.fs-modal textarea { min-height: 100px; resize: vertical; }
.fs-modal input:focus, .fs-modal select:focus, .fs-modal textarea:focus {
    border-color: #0052cc; outline: none; box-shadow: 0 0 0 2px rgba(0,82,204,0.15);
}
.fs-modal-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 10px; }
.fs-modal-btn {
    padding: 8px 20px; border-radius: 6px; font-size: 0.9em; cursor: pointer;
    border: 1px solid #dee2e6; background: #fff; color: #495057; font-weight: 500;
}
.fs-modal-btn:hover { background: #f8f9fa; }
.fs-modal-btn.primary { background: #0052cc; color: #fff; border-color: #0052cc; }
.fs-modal-btn.primary:hover { background: #0747a6; }
.fs-modal-btn.primary:disabled { background: #80b3ff; cursor: not-allowed; }
.fs-modal-btn.danger { background: #dc3545; color: #fff; border-color: #dc3545; }
.fs-modal-close {
    position: absolute; top: 12px; right: 16px; background: none; border: none;
    font-size: 1.4em; color: #adb5bd; cursor: pointer; line-height: 1;
}
.fs-modal-close:hover { color: #495057; }
.fs-modal-error {
    background: #fff3f3; border: 1px solid #dc3545; color: #dc3545;
    padding: 10px 14px; border-radius: 6px; font-size: 0.85em; margin-bottom: 14px;
    display: none; line-height: 1.5; word-break: break-word;
}
.fs-modal-error.visible { display: block; }
.fs-modal-success {
    background: #e6ffe6; border: 1px solid #28a745; color: #28a745;
    padding: 8px 12px; border-radius: 6px; font-size: 0.85em; margin-bottom: 14px;
    display: none;
}
.fs-modal-success.visible { display: block; }
.fs-modal-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
</style>

<!-- Credential Modal -->
<div class="fs-modal-overlay" id="fsCredModal">
    <div class="fs-modal">
        <button class="fs-modal-close" onclick="fsCloseModal('fsCredModal')">&times;</button>
        <h3>Connect to Finite State</h3>
        <p style="color:#6c757d;font-size:0.85em;margin-bottom:18px;">
            Enter your API credentials to enable triage status updates and Jira ticket creation (if configured).
            Credentials are stored only in this browser tab and cleared when you close it.
        </p>
        <div class="fs-modal-error" id="fsCredError"></div>
        <label for="fsDomain">Domain</label>
        <input type="text" id="fsDomain" placeholder="e.g. platform.finitestate.io">
        <label for="fsToken">API Token</label>
        <input type="password" id="fsToken" placeholder="Paste your FINITE_STATE_AUTH_TOKEN">
        <div class="fs-modal-actions">
            <button class="fs-modal-btn" onclick="fsCloseModal('fsCredModal')">Cancel</button>
            <button class="fs-modal-btn primary" id="fsCredConnectBtn" onclick="fsConnect()">Connect</button>
        </div>
    </div>
</div>

<!-- Jira Ticket Modal -->
<div class="fs-modal-overlay" id="fsJiraModal">
    <div class="fs-modal">
        <button class="fs-modal-close" onclick="fsCloseModal('fsJiraModal')">&times;</button>
        <h3>Create Jira Ticket</h3>
        <div class="fs-modal-error" id="fsJiraError"></div>
        <div class="fs-modal-success" id="fsJiraSuccess"></div>
        <div class="fs-modal-row">
            <div>
                <label for="fsJiraProject">Jira Project</label>
                <select id="fsJiraProject" onchange="fsUpdateTicketTypes()"></select>
            </div>
            <div>
                <label for="fsJiraType">Ticket Type</label>
                <select id="fsJiraType"></select>
            </div>
        </div>
        <div class="fs-modal-row">
            <div>
                <label for="fsJiraPriority">Priority</label>
                <select id="fsJiraPriority">
                    <option value="Highest">Highest</option>
                    <option value="High">High</option>
                    <option value="Medium" selected>Medium</option>
                    <option value="Low">Low</option>
                    <option value="Lowest">Lowest</option>
                </select>
            </div>
            <div>
                <label for="fsJiraMode">Mode</label>
                <select id="fsJiraMode"></select>
            </div>
        </div>
        <label for="fsJiraTitle">Title</label>
        <input type="text" id="fsJiraTitle">
        <label for="fsJiraSummary">Summary</label>
        <textarea id="fsJiraSummary"></textarea>
        <div class="fs-modal-actions">
            <button class="fs-modal-btn" onclick="fsCloseModal('fsJiraModal')">Cancel</button>
            <button class="fs-modal-btn primary" id="fsJiraCreateBtn" onclick="fsCreateTicket()">Create Ticket</button>
        </div>
    </div>
</div>

<!-- Triage Confirm Modal -->
<div class="fs-modal-overlay" id="fsTriageModal">
    <div class="fs-modal" style="max-width:420px;">
        <button class="fs-modal-close" onclick="fsCloseModal('fsTriageModal')">&times;</button>
        <h3 id="fsTriageTitle">Set Finding Status</h3>
        <div class="fs-modal-error" id="fsTriageError"></div>
        <p id="fsTriageDesc" style="margin-bottom:14px;font-size:0.9em;"></p>
        <p style="color:#6c757d;font-size:0.82em;margin-bottom:14px;">
            This will update the finding status in the Finite State platform.
        </p>
        <div id="fsTriageRecInfo" style="display:none; background:#f0fdf4; border:1px solid #86efac; border-radius:6px; padding:10px 14px; margin-bottom:14px; font-size:0.85em;">
            <strong>Recommendation:</strong> <span id="fsTriageRecStatus"></span>
            <div id="fsTriageRecReason" style="color:#6c757d; margin-top:4px;"></div>
        </div>
        <div class="fs-modal-actions">
            <button class="fs-modal-btn" onclick="fsCloseModal('fsTriageModal')">Cancel</button>
            <button class="fs-modal-btn primary" id="fsTriageClearBtn" style="display:none;" onclick="fsClearStatus()">Clear Status</button>
            <button class="fs-modal-btn primary" id="fsTriageConfirmBtn" onclick="fsSetTriage()">Set IN_TRIAGE</button>
            <button class="fs-modal-btn primary" id="fsTriageAcceptBtn" style="display:none;" onclick="fsAcceptRecommendation()">Accept</button>
        </div>
    </div>
</div>

<script>
(function() {
    'use strict';

    // =========================================================================
    // Credential & Session Management
    // =========================================================================

    function getSession() {
        try {
            const s = sessionStorage.getItem('fs_session');
            return s ? JSON.parse(s) : null;
        } catch { return null; }
    }

    function setSession(data) {
        sessionStorage.setItem('fs_session', JSON.stringify(data));
        updateConnectionBadge();
        updateJiraVisibility();
    }

    function isConnected() { return !!getSession(); }
    function hasJira() { var s = getSession(); return s && s.jiraAvailable; }

    // Detect if we're served via --serve (localhost) and can use the proxy
    function _useProxy() {
        return window.location.protocol === 'http:' &&
               (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
    }

    // Server-side session nonce (populated by auto-connect)
    var _serverNonce = null;
    // True when the server holds the token (no browser-side token needed)
    var _serverSideAuth = false;

    function apiBase() {
        const s = getSession();
        if (!s) return '';
        // When served via --serve, route through the local proxy to avoid CORS
        if (_useProxy()) return '/fsapi';
        return 'https://' + s.domain + '/api';
    }

    function apiHeaders() {
        const s = getSession();
        const headers = {
            'Content-Type': 'application/json',
            'Accept': 'application/json',
        };
        // When server holds the token, include the CSRF nonce instead
        if (_serverSideAuth && _serverNonce) {
            headers['X-FS-Session'] = _serverNonce;
        } else if (s && s.token) {
            headers['X-Authorization'] = s.token;
        }
        return headers;
    }

    // Auto-connect: call /fsapi/session on load to see if the server
    // already has a token.  If so, create a session without prompting.
    async function _tryAutoConnect() {
        if (!_useProxy()) return false;
        try {
            const resp = await fetch('/fsapi/session');
            if (!resp.ok) return false;
            const data = await resp.json();
            if (data.connected && data.domain) {
                _serverNonce = data.nonce || null;
                _serverSideAuth = true;
                setSession({
                    domain: data.domain,
                    token: '',  // server holds the token
                    jiraAvailable: data.jiraAvailable || false,
                    projects: data.jiraProjects || [],
                });
                return true;
            }
        } catch { /* server not running or unreachable */ }
        return false;
    }

    // =========================================================================
    // Modal Helpers
    // =========================================================================

    window.fsCloseModal = function(id) {
        document.getElementById(id).classList.remove('active');
    };

    function openModal(id) {
        document.getElementById(id).classList.add('active');
    }

    function showError(elemId, msg) {
        const el = document.getElementById(elemId);
        el.textContent = msg; el.classList.add('visible');
    }

    function hideError(elemId) {
        const el = document.getElementById(elemId);
        el.textContent = ''; el.classList.remove('visible');
    }

    function showSuccess(elemId, msg) {
        const el = document.getElementById(elemId);
        el.textContent = msg; el.classList.add('visible');
    }

    function setLoading(btnId, loading) {
        const btn = document.getElementById(btnId);
        if (loading) {
            btn._origText = btn.textContent;
            btn.innerHTML = '<span class="spinner"></span> Working...';
            btn.disabled = true;
        } else {
            btn.textContent = btn._origText || 'Submit';
            btn.disabled = false;
        }
    }

    // =========================================================================
    // Connection Badge
    // =========================================================================

    function updateConnectionBadge() {
        const badge = document.getElementById('fsConnectionBadge');
        if (!badge) return;
        const s = getSession();
        if (s) {
            const domainLabel = s.domain.replace(/^(https?:\/\/)?/, '').split('/')[0];
            const hasJiraBtns = document.querySelectorAll('.btn-jira').length > 0;
            const jiraNote = (hasJiraBtns && !s.jiraAvailable) ? ' <span style="opacity:0.7;font-size:0.85em;">(Jira not configured)</span>' : '';
            badge.innerHTML = '<span class="fs-connected-badge">&#10003; Connected to ' + domainLabel + jiraNote + '</span>';
        } else {
            badge.innerHTML = '<button class="fs-connect-btn" onclick="fsShowCredModal()">Connect to Finite State</button>';
        }
    }

    function updateJiraVisibility() {
        const jiraEnabled = hasJira();
        document.querySelectorAll('.btn-jira').forEach(function(btn) {
            btn.style.display = jiraEnabled ? '' : 'none';
        });
    }

    // =========================================================================
    // Connect Flow
    // =========================================================================

    window.fsShowCredModal = function() {
        hideError('fsCredError');
        // Pre-fill domain if available from report metadata
        const domainInput = document.getElementById('fsDomain');
        if (!domainInput.value) {
            const meta = document.querySelector('meta[name="fs-domain"]');
            if (meta) domainInput.value = meta.content;
        }
        openModal('fsCredModal');
        document.getElementById('fsToken').focus();
    };

    window.fsConnect = async function() {
        hideError('fsCredError');
        const domain = document.getElementById('fsDomain').value.trim().replace(/^https?:\/\//, '').replace(/\/+$/, '');
        const token = document.getElementById('fsToken').value.trim();
        if (!domain || !token) {
            showError('fsCredError', 'Both domain and token are required.');
            return;
        }
        setLoading('fsCredConnectBtn', true);
        try {
            // Step 1: Validate token with authUser (works on every domain)
            const authHeaders = { 'X-Authorization': token, 'Accept': 'application/json', 'Content-Type': 'application/json' };
            let authUrl;
            if (_useProxy()) {
                authUrl = '/fsapi/public/v0/authUser';
                authHeaders['X-FS-Domain'] = domain;
            } else {
                authUrl = 'https://' + domain + '/api/public/v0/authUser';
            }
            const authRes = await fetch(authUrl, {
                method: 'GET',
                headers: authHeaders,
            });
            if (!authRes.ok) {
                const text = await authRes.text();
                if (authRes.status === 401 || authRes.status === 403) {
                    throw new Error('Authentication failed — check that your API token is valid for ' + domain);
                }
                throw new Error('HTTP ' + authRes.status + ': ' + (text || 'Connection failed'));
            }

            // Step 2: Try Jira ping to fetch projects (optional — not all domains have Jira)
            var jiraProjects = [];
            var jiraAvailable = false;
            try {
                const pingHeaders = { 'X-Authorization': token, 'Accept': 'application/json', 'Content-Type': 'application/json' };
                let pingUrl;
                if (_useProxy()) {
                    pingUrl = '/fsapi/public/v0/tracker/tickets/ping';
                    pingHeaders['X-FS-Domain'] = domain;
                } else {
                    pingUrl = 'https://' + domain + '/api/public/v0/tracker/tickets/ping';
                }
                const pingRes = await fetch(pingUrl, {
                    method: 'POST',
                    headers: pingHeaders,
                    body: '{}',
                });
                if (pingRes.ok) {
                    const pingData = await pingRes.json();
                    jiraProjects = pingData.projects || [];
                    jiraAvailable = true;
                }
                // If ping fails (400, 404, etc.), silently skip — Jira just isn't configured
            } catch (_) { /* Jira integration not available — that's fine */ }

            setSession({ domain: domain, token: token, projects: jiraProjects, jiraAvailable: jiraAvailable });
            fsCloseModal('fsCredModal');
            // Refresh triage button states from platform (non-blocking)
            refreshTriageStatuses();
            // If there was a pending action, execute it
            if (window._fsPendingAction) {
                const action = window._fsPendingAction;
                window._fsPendingAction = null;
                action();
            }
        } catch (err) {
            if (err.message === 'Failed to fetch' || err.name === 'TypeError') {
                // CORS or network error — give actionable guidance
                const isFile = window.location.protocol === 'file:';
                let msg = 'Connection failed — the browser blocked the request.';
                if (isFile) {
                    msg += ' This report is opened as a local file (file://), which prevents API calls due to CORS. '
                        + 'Re-run with --serve to start a local HTTP server: '
                        + 'poetry run fs-report --recipe "Triage Prioritization" --serve';
                } else {
                    msg += ' Check that the domain is correct and reachable.';
                }
                showError('fsCredError', msg);
            } else {
                showError('fsCredError', 'Connection failed: ' + err.message);
            }
        } finally {
            setLoading('fsCredConnectBtn', false);
        }
    };

    // Ensure connected, then run callback
    function ensureConnected(callback) {
        if (isConnected()) { callback(); }
        else {
            window._fsPendingAction = callback;
            fsShowCredModal();
        }
    }

    // =========================================================================
    // Jira Ticket Creation
    // =========================================================================

    let _jiraContext = {};  // holds current ticket data

    window.fsOpenJiraModal = function(data) {
        // Don't re-open if this button already succeeded
        if (data.sourceBtn && data.sourceBtn.disabled) return;
        ensureConnected(function() {
            if (!hasJira()) {
                alert('Jira integration is not configured on this domain. Contact your Finite State administrator to enable it.');
                return;
            }
            _jiraContext = data;
            hideError('fsJiraError');
            const successEl = document.getElementById('fsJiraSuccess');
            successEl.classList.remove('visible');

            // Populate projects dropdown
            const s = getSession();
            const projSelect = document.getElementById('fsJiraProject');
            projSelect.innerHTML = '';
            (s.projects || []).forEach(function(p) {
                const opt = document.createElement('option');
                opt.value = p.key;
                opt.textContent = p.name + ' (' + p.key + ')';
                if (p['default']) opt.selected = true;
                projSelect.appendChild(opt);
            });
            fsUpdateTicketTypes();
            _populateModes(data.endpoint || 'findings', data.mode);

            // Build Finite State deep link for the first line of the description
            var deepLink = '';
            var s2 = getSession();
            if (s2 && s2.domain && data.projectId && data.projectVersionId) {
                var base = 'https://' + s2.domain + '/projects/' + data.projectId + '/versions/' + data.projectVersionId;
                if (data.endpoint === 'findings' && data.internalId) {
                    deepLink = base + '/findings?findingId=' + data.internalId;
                } else if (data.endpoint === 'components' && data.componentId) {
                    deepLink = base + '/bill-of-materials?view=list&componentId=' + data.componentId;
                }
            }

            // Populate fields — deep link as the first line of the summary
            document.getElementById('fsJiraTitle').value = data.title || '';
            var summaryText = data.summary || '';
            if (deepLink) {
                summaryText = 'Finite State: ' + deepLink + '\n\n' + summaryText;
            }
            document.getElementById('fsJiraSummary').value = summaryText;
            // Map priority band
            const bandMap = { 'CRITICAL': 'Highest', 'HIGH': 'High', 'MEDIUM': 'Medium', 'LOW': 'Low', 'INFO': 'Lowest' };
            document.getElementById('fsJiraPriority').value = bandMap[data.band] || 'Medium';

            // Save single-item context for restoring when switching modes
            _singleContext = {
                findings: (data.findings || []).slice(),
                components: (data.components || []).slice(),
                title: document.getElementById('fsJiraTitle').value,
                summary: document.getElementById('fsJiraSummary').value,
            };

            // Reset the Create button (may have been changed to "Close" after a previous success)
            var createBtn = document.getElementById('fsJiraCreateBtn');
            createBtn.textContent = 'Create Ticket';
            createBtn.disabled = false;
            createBtn.onclick = fsCreateTicket;
            openModal('fsJiraModal');
        });
    };

    window.fsUpdateTicketTypes = function() {
        const s = getSession();
        const projKey = document.getElementById('fsJiraProject').value;
        const proj = (s.projects || []).find(function(p) { return p.key === projKey; });
        const typeSelect = document.getElementById('fsJiraType');
        typeSelect.innerHTML = '';
        (proj && proj.ticketTypes || ['Task', 'Bug', 'Story']).forEach(function(t) {
            const opt = document.createElement('option');
            opt.value = t; opt.textContent = t;
            if (t === 'Bug' || t === 'Task') opt.selected = (t === 'Bug');
            typeSelect.appendChild(opt);
        });
    };

    var _modesByEndpoint = {
        findings: [
            { value: 'SINGLE_FINDING', label: 'Single finding' },
            { value: 'ONE_PER_FINDING', label: 'One ticket per finding' },
            { value: 'ONE_FOR_ALL_FINDINGS', label: 'One ticket for all findings' },
        ],
        components: [
            { value: 'SINGLE_COMPONENT', label: 'Single component' },
            { value: 'ONE_PER_COMPONENT', label: 'One ticket per component' },
            { value: 'ONE_FOR_ALL_COMPONENTS', label: 'One ticket for all components' },
        ],
    };

    function _populateModes(endpoint, defaultMode) {
        var modeSelect = document.getElementById('fsJiraMode');
        modeSelect.innerHTML = '';
        var modes = _modesByEndpoint[endpoint] || _modesByEndpoint.findings;
        modes.forEach(function(m) {
            var opt = document.createElement('option');
            opt.value = m.value;
            opt.textContent = m.label;
            if (m.value === defaultMode) opt.selected = true;
            modeSelect.appendChild(opt);
        });
        modeSelect.onchange = _onModeChange;
    }

    // Store original single-item context so we can restore on mode switch
    var _singleContext = {};

    function _onModeChange() {
        var mode = document.getElementById('fsJiraMode').value;
        var endpoint = _jiraContext.endpoint || 'findings';
        var isSingle = (mode === 'SINGLE_FINDING' || mode === 'SINGLE_COMPONENT');

        if (isSingle) {
            // Restore original single-item data
            _jiraContext.findings = _singleContext.findings || [];
            _jiraContext.components = _singleContext.components || [];
            document.getElementById('fsJiraTitle').value = _singleContext.title || '';
            document.getElementById('fsJiraSummary').value = _singleContext.summary || '';
            return;
        }

        // Multi-item mode: collect all items from matching buttons on the page
        var allFindings = [];
        var allComponents = [];
        var itemLabels = [];

        document.querySelectorAll('.btn-jira').forEach(function(btn) {
            if (btn.disabled) return;
            try {
                var data = JSON.parse(btn.dataset.action);
                if (data.endpoint !== endpoint) return;
                (data.findings || []).forEach(function(id) {
                    if (id && allFindings.indexOf(id) === -1) allFindings.push(id);
                });
                (data.components || []).forEach(function(id) {
                    if (id && allComponents.indexOf(id) === -1) allComponents.push(id);
                });
                if (data.title) itemLabels.push(data.title);
            } catch (_) {}
        });

        _jiraContext.findings = allFindings;
        _jiraContext.components = allComponents;

        // Update title and summary for aggregate modes
        var isAll = (mode === 'ONE_FOR_ALL_FINDINGS' || mode === 'ONE_FOR_ALL_COMPONENTS');
        if (endpoint === 'findings') {
            var title = allFindings.length + ' findings requiring remediation';
            document.getElementById('fsJiraTitle').value = title;
            if (isAll) {
                document.getElementById('fsJiraSummary').value =
                    allFindings.length + ' findings across ' + allComponents.length + ' components:\n\n' +
                    itemLabels.slice(0, 50).join('\n') +
                    (itemLabels.length > 50 ? '\n... and ' + (itemLabels.length - 50) + ' more' : '');
            }
        } else {
            var title = allComponents.length + ' components requiring remediation';
            document.getElementById('fsJiraTitle').value = title;
            if (isAll) {
                document.getElementById('fsJiraSummary').value =
                    allComponents.length + ' components with ' + allFindings.length + ' total findings:\n\n' +
                    itemLabels.slice(0, 50).join('\n') +
                    (itemLabels.length > 50 ? '\n... and ' + (itemLabels.length - 50) + ' more' : '');
            }
        }
    }

    window.fsCreateTicket = async function() {
        hideError('fsJiraError');
        setLoading('fsJiraCreateBtn', true);
        const projKey = document.getElementById('fsJiraProject').value;
        const s = getSession();
        const proj = (s.projects || []).find(function(p) { return p.key === projKey; });

        const endpoint = _jiraContext.endpoint || 'findings';
        const body = {
            components: _jiraContext.components || [],
            findings: _jiraContext.findings || [],
            ticket_name: document.getElementById('fsJiraTitle').value,
            ticket_summary: document.getElementById('fsJiraSummary').value,
            priority: document.getElementById('fsJiraPriority').value,
            project_name: proj ? proj.name : '',
            project_key: projKey,
            type: document.getElementById('fsJiraType').value,
            mode: document.getElementById('fsJiraMode').value,
        };

        try {
            const res = await fetch(apiBase() + '/public/v0/tracker/tickets/' + endpoint, {
                method: 'POST',
                headers: apiHeaders(),
                body: JSON.stringify(body),
            });
            if (!res.ok) {
                const text = await res.text();
                throw new Error('HTTP ' + res.status + ': ' + (text || 'Failed to create ticket'));
            }
            const result = await res.json();
            showSuccess('fsJiraSuccess', 'Ticket created successfully.' +
                (result.ticket_url ? ' ' + result.ticket_url : ''));

            // Update the source button in the table
            if (_jiraContext.sourceBtn) {
                _jiraContext.sourceBtn.classList.add('btn-success');
                _jiraContext.sourceBtn.innerHTML = '&#10003; Created';
                _jiraContext.sourceBtn.disabled = true;
            }
            // Change modal button to "Close" so user can dismiss
            const createBtn = document.getElementById('fsJiraCreateBtn');
            createBtn.textContent = 'Close';
            createBtn.disabled = false;
            createBtn.onclick = function() { fsCloseModal('fsJiraModal'); };
        } catch (err) {
            showError('fsJiraError', err.message);
            setLoading('fsJiraCreateBtn', false);
        }
    };

    // =========================================================================
    // Set Triage Status
    // =========================================================================

    let _triageContext = {};

    window.fsOpenTriageModal = function(data) {
        ensureConnected(function() {
            _triageContext = data;
            hideError('fsTriageError');

            var hasStatus = data.currentStatus;
            var clearBtn = document.getElementById('fsTriageClearBtn');
            var confirmBtn = document.getElementById('fsTriageConfirmBtn');
            var acceptBtn = document.getElementById('fsTriageAcceptBtn');
            var recInfo = document.getElementById('fsTriageRecInfo');

            // Recommendation handling
            var rec = data.recommendation;
            var hasRec = rec && rec.recommended_vex_status;
            if (hasRec) {
                document.getElementById('fsTriageRecStatus').textContent = rec.recommended_vex_status;
                document.getElementById('fsTriageRecReason').textContent = rec.reason || '';
                recInfo.style.display = '';
                acceptBtn.style.display = '';
                acceptBtn.textContent = 'Accept: ' + rec.recommended_vex_status;
                acceptBtn.disabled = false;
            } else {
                recInfo.style.display = 'none';
                acceptBtn.style.display = 'none';
            }

            if (hasStatus) {
                // Finding already has a status — offer to clear or set IN_TRIAGE
                document.getElementById('fsTriageTitle').textContent = 'Update Finding Status';
                document.getElementById('fsTriageDesc').innerHTML =
                    '<strong>' + data.findingId + '</strong> is currently <strong>' + hasStatus + '</strong>.';
                clearBtn.style.display = '';
                clearBtn.textContent = 'Clear Status';
                clearBtn.disabled = false;
                confirmBtn.textContent = 'Set IN_TRIAGE';
                confirmBtn.disabled = false;
            } else {
                // No status yet — simple set IN_TRIAGE
                document.getElementById('fsTriageTitle').textContent = 'Set Finding Status';
                document.getElementById('fsTriageDesc').innerHTML =
                    'Set <strong>' + data.findingId + '</strong> to <strong>IN_TRIAGE</strong>?';
                clearBtn.style.display = 'none';
                confirmBtn.textContent = 'Set IN_TRIAGE';
                confirmBtn.disabled = false;
            }
            openModal('fsTriageModal');
        });
    };

    // Helper: reset a triage button to default "no status" appearance
    function _resetTriageBtn(btn) {
        btn.className = 'action-btn btn-triage';
        btn.innerHTML = '&#x2691; Triage';
        btn.disabled = false;
    }

    // Helper: set a triage button to show a status
    function _setTriageBtnStatus(btn, status) {
        var style = _statusStyles[status] || { label: status, cls: 'btn-success' };
        // Remove any previous status classes, keep base classes
        btn.className = 'action-btn btn-triage ' + style.cls;
        btn.innerHTML = '&#10003; ' + style.label;
        btn.disabled = false; // Keep clickable so user can clear/change
        // Store current status on the button for the modal
        btn.dataset.currentStatus = status;
    }

    window.fsSetTriage = async function() {
        hideError('fsTriageError');
        setLoading('fsTriageConfirmBtn', true);

        const pvId = _triageContext.projectVersionId;
        const intId = _triageContext.internalId;
        if (!pvId || !intId) {
            showError('fsTriageError', 'Missing project version or finding ID.');
            setLoading('fsTriageConfirmBtn', false);
            return;
        }
        const url = apiBase() + '/public/v0/findings/' + pvId + '/' + intId + '/status';

        var defaults = _vexDefaults('IN_TRIAGE', _triageContext.reachabilityLabel || 'INCONCLUSIVE');
        try {
            const res = await fetch(url, {
                method: 'PUT',
                headers: apiHeaders(),
                body: JSON.stringify({
                    status: 'IN_TRIAGE',
                    response: defaults.response,
                    justification: defaults.justification,
                    reason: _triageContext.reason || '',
                }),
            });
            if (!res.ok) {
                const text = await res.text();
                throw new Error('HTTP ' + res.status + ': ' + (text || 'Failed to update status'));
            }
            fsCloseModal('fsTriageModal');
            if (_triageContext.sourceBtn) {
                _setTriageBtnStatus(_triageContext.sourceBtn, 'IN_TRIAGE');
            }
        } catch (err) {
            showError('fsTriageError', err.message);
        } finally {
            setLoading('fsTriageConfirmBtn', false);
        }
    };

    window.fsClearStatus = async function() {
        hideError('fsTriageError');
        setLoading('fsTriageClearBtn', true);

        const pvId = _triageContext.projectVersionId;
        const intId = _triageContext.internalId;
        const url = apiBase() + '/public/v0/findings/' + pvId + '/' + intId + '/status/clear';

        try {
            var hdrs = apiHeaders();
            delete hdrs['Content-Type']; // clear endpoint rejects a body
            const res = await fetch(url, {
                method: 'PUT',
                headers: hdrs,
            });
            if (!res.ok) {
                const text = await res.text();
                throw new Error('HTTP ' + res.status + ': ' + (text || 'Failed to clear status'));
            }
            fsCloseModal('fsTriageModal');
            if (_triageContext.sourceBtn) {
                _resetTriageBtn(_triageContext.sourceBtn);
                delete _triageContext.sourceBtn.dataset.currentStatus;
            }
        } catch (err) {
            showError('fsTriageError', err.message);
        } finally {
            setLoading('fsTriageClearBtn', false);
        }
    };

    // =========================================================================
    // Accept Recommendation
    // =========================================================================

    // Smart defaults matching Python vex_applier.get_smart_defaults()
    function _vexDefaults(status, reachLabel) {
        var justification = 'CODE_NOT_PRESENT';
        if (status === 'NOT_AFFECTED' && reachLabel === 'UNREACHABLE') {
            justification = 'CODE_NOT_REACHABLE';
        }
        return { response: 'WILL_NOT_FIX', justification: justification };
    }

    window.fsAcceptRecommendation = async function() {
        hideError('fsTriageError');
        setLoading('fsTriageAcceptBtn', true);

        var rec = _triageContext.recommendation;
        if (!rec || !rec.recommended_vex_status) {
            showError('fsTriageError', 'No recommendation data available.');
            setLoading('fsTriageAcceptBtn', false);
            return;
        }

        const pvId = _triageContext.projectVersionId;
        const intId = _triageContext.internalId;
        if (!pvId || !intId) {
            showError('fsTriageError', 'Missing project version or finding ID.');
            setLoading('fsTriageAcceptBtn', false);
            return;
        }
        const url = apiBase() + '/public/v0/findings/' + pvId + '/' + intId + '/status';

        // Build body with smart defaults (mirrors Python vex_applier logic)
        var defaults = _vexDefaults(
            rec.recommended_vex_status,
            _triageContext.reachabilityLabel || 'INCONCLUSIVE'
        );
        var body = {
            status: rec.recommended_vex_status,
            response: defaults.response,
            justification: rec.justification || defaults.justification,
            reason: rec.reason || '',
        };

        try {
            const res = await fetch(url, {
                method: 'PUT',
                headers: apiHeaders(),
                body: JSON.stringify(body),
            });
            if (!res.ok) {
                const text = await res.text();
                throw new Error('HTTP ' + res.status + ': ' + (text || 'Failed to accept recommendation'));
            }
            fsCloseModal('fsTriageModal');
            if (_triageContext.sourceBtn) {
                _setTriageBtnStatus(_triageContext.sourceBtn, rec.recommended_vex_status);
            }
        } catch (err) {
            showError('fsTriageError', err.message);
        } finally {
            setLoading('fsTriageAcceptBtn', false);
        }
    };

    // =========================================================================
    // Refresh Triage Statuses on Connect
    // =========================================================================

    // Status display mapping: VEX status → button label and style
    var _statusStyles = {
        'IN_TRIAGE':              { label: 'IN_TRIAGE',    cls: 'btn-intriage' },
        'NOT_AFFECTED':           { label: 'NOT_AFFECTED', cls: 'btn-success' },
        'FALSE_POSITIVE':         { label: 'FALSE_POSITIVE', cls: 'btn-success' },
        'RESOLVED':               { label: 'RESOLVED',     cls: 'btn-success' },
        'RESOLVED_WITH_PEDIGREE': { label: 'RESOLVED',     cls: 'btn-success' },
        'EXPLOITABLE':            { label: 'EXPLOITABLE',  cls: 'btn-error' },
    };

    async function refreshTriageStatuses() {
        // Collect all triage buttons that haven't already been updated
        var buttons = document.querySelectorAll('.btn-triage');
        if (!buttons.length) return;

        var byVersion = {};
        buttons.forEach(function(btn) {
            if (btn.disabled) return; // Already shows a status
            try {
                var data = JSON.parse(btn.dataset.action);
                var pvId = String(data.projectVersionId || '');
                if (!pvId) return;
                if (!byVersion[pvId]) byVersion[pvId] = { entries: [], cveIds: {} };
                byVersion[pvId].entries.push({ btn: btn, internalId: String(data.internalId) });
                // Collect unique CVE IDs so we can filter the API request
                var cveId = String(data.findingId || '');
                if (cveId) byVersion[pvId].cveIds[cveId] = true;
            } catch (_) {}
        });

        var pvIds = Object.keys(byVersion);
        if (!pvIds.length) return;

        // For each version, fetch only the specific findings shown in the report.
        // Uses findingId=in=(...) filter so we aren't limited by pagination.
        // Batch CVE IDs into groups of 40 to keep URLs reasonable.
        var allFetches = [];
        pvIds.forEach(function(pvId) {
            var group = byVersion[pvId];
            var cveList = Object.keys(group.cveIds);
            var batchSize = 40;
            for (var i = 0; i < cveList.length; i += batchSize) {
                var batch = cveList.slice(i, i + batchSize);
                var cveFilter = 'findingId%3Din%3D(' + batch.map(encodeURIComponent).join(',') + ')';
                var filter = 'projectVersion%3D%3D' + pvId + '%3B' + cveFilter;
                var url = apiBase() + '/public/v0/findings?filter=' + filter + '&limit=100';
                allFetches.push(
                    fetch(url, { method: 'GET', headers: apiHeaders() })
                        .then(function(res) { return res.ok ? res.json() : []; })
                        .then(function(data) {
                            var items = Array.isArray(data) ? data : (data.items || data.data || []);
                            return { pvId: pvId, findings: items };
                        })
                        .catch(function() { return { pvId: pvId, findings: [] }; })
                );
            }
        });

        var results = await Promise.all(allFetches);

        // Build id→status map per version (merge results from batched fetches)
        var statusMaps = {};
        results.forEach(function(result) {
            if (!statusMaps[result.pvId]) statusMaps[result.pvId] = {};
            result.findings.forEach(function(f) {
                if (f && f.id && f.status) {
                    statusMaps[result.pvId][String(f.id)] = String(f.status);
                }
            });
        });

        // Update matching buttons
        pvIds.forEach(function(pvId) {
            var map = statusMaps[pvId] || {};
            (byVersion[pvId].entries || []).forEach(function(entry) {
                var status = map[entry.internalId];
                if (status) {
                    _setTriageBtnStatus(entry.btn, status);
                }
            });
        });
    }

    // =========================================================================
    // Init
    // =========================================================================

    document.addEventListener('DOMContentLoaded', async function() {
        // Interactive features require --serve (local proxy for API calls).
        // Hide all action buttons and connection UI when not served via localhost.
        if (!_useProxy()) {
            document.querySelectorAll('.action-btn, .action-cell, #fsConnectionBadge').forEach(function(el) {
                el.style.display = 'none';
            });
            // Also hide the Actions column headers
            document.querySelectorAll('th').forEach(function(th) {
                if (th.textContent.trim() === 'Actions') th.style.display = 'none';
            });
            return; // skip remaining interactive setup
        }

        // Try server-side auto-connect (no credential prompt needed)
        await _tryAutoConnect();

        updateConnectionBadge();
        updateJiraVisibility();

        // If connected (auto or session persisted in tab), refresh triage statuses
        if (isConnected()) {
            refreshTriageStatuses();
        }

        // Close modals on overlay click
        document.querySelectorAll('.fs-modal-overlay').forEach(function(overlay) {
            overlay.addEventListener('click', function(e) {
                if (e.target === overlay) overlay.classList.remove('active');
            });
        });

        // Close modals on Escape
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                document.querySelectorAll('.fs-modal-overlay.active').forEach(function(m) {
                    m.classList.remove('active');
                });
            }
        });
    });

})();
</script>
