APP.ui.cards = {}; APP.ui.cards.renderFrameTrackList = function () { const { state } = APP.core; const { $ } = APP.core.utils; const frameTrackList = $("#frameTrackList"); const trackList = $("#trackList"); // Tab 2 (Engage) uses the same card logic const trackCount = $("#trackCount"); if (!frameTrackList && !trackList) return; if (frameTrackList) frameTrackList.innerHTML = ""; if (trackList) trackList.innerHTML = ""; // Filter: only show mission-relevant detections (or all in LEGACY mode) const dets = (state.detections || []).filter(d => { if (d.mission_relevant === null || d.mission_relevant === undefined) return true; return d.mission_relevant === true; }); if (trackCount) trackCount.textContent = dets.length; if (dets.length === 0) { const emptyMsg = '
No objects tracked.
'; if (frameTrackList) frameTrackList.innerHTML = emptyMsg; if (trackList) trackList.innerHTML = emptyMsg; return; } // Sort: ASSESSED first (by threat score), then UNASSESSED, then STALE const S = APP.core.gptMapping.STATUS; const statusOrder = { [S.ASSESSED]: 0, [S.UNASSESSED]: 1, [S.STALE]: 2 }; const sorted = [...dets].sort((a, b) => { const statusA = statusOrder[a.assessment_status] ?? 1; const statusB = statusOrder[b.assessment_status] ?? 1; if (statusA !== statusB) return statusA - statusB; const scoreA = a.threat_level_score || 0; const scoreB = b.threat_level_score || 0; if (scoreB !== scoreA) return scoreB - scoreA; return (b.score || 0) - (a.score || 0); }); sorted.forEach((det, i) => { const id = det.id || `T${String(i + 1).padStart(2, '0')}`; const isActive = state.selectedId === id; let rangeStr = "---"; let bearingStr = "---"; if (det.depth_valid && det.depth_est_m != null) { rangeStr = `${Math.round(det.depth_est_m)}m`; } else if (det.gpt_distance_m) { rangeStr = `~${det.gpt_distance_m}m`; } else if (det.baseRange_m) { rangeStr = `${Math.round(det.baseRange_m)}m`; } if (det.gpt_direction) { bearingStr = det.gpt_direction; } const card = document.createElement("div"); card.className = "track-card" + (isActive ? " active" : ""); card.id = `card-${id}`; card.onclick = () => { const ev = new CustomEvent("track-selected", { detail: { id } }); document.dispatchEvent(ev); }; // Assessment status badge let statusBadge = ""; const assessStatus = det.assessment_status || S.UNASSESSED; if (assessStatus === S.UNASSESSED) { statusBadge = 'UNASSESSED'; } else if (assessStatus === S.STALE) { statusBadge = 'STALE'; } else if (det.threat_level_score > 0) { statusBadge = `T-${det.threat_level_score}`; } else if (assessStatus === S.ASSESSED) { statusBadge = 'ASSESSED'; } // GPT description (collapsed summary) const desc = det.gpt_description ? `
${det.gpt_description}
` : ""; // Inline features (only shown when active/expanded) let featuresHtml = ""; if (isActive && det.features && Object.keys(det.features).length > 0) { const entries = Object.entries(det.features).slice(0, 12); const rows = entries.map(([k, v]) => `
${k}${String(v)}
` ).join(""); featuresHtml = `
${rows}
`; } card.innerHTML = `
${id} · ${det.label}
${statusBadge} ${det.mission_relevant === true ? 'RELEVANT' : det.mission_relevant === false ? 'N/R' : `${(det.score * 100).toFixed(0)}%` }
RNG ${rangeStr} · BRG ${bearingStr}
${desc} ${featuresHtml} `; if (frameTrackList) frameTrackList.appendChild(card); if (trackList) trackList.appendChild(card.cloneNode(true)); }); // Wire up click handlers on cloned Tab 2 cards if (trackList) { trackList.querySelectorAll(".track-card").forEach(card => { const id = card.id.replace("card-", ""); card.onclick = () => { document.dispatchEvent(new CustomEvent("track-selected", { detail: { id } })); }; }); } };