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 = `
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 } }));
};
});
}
};