|
1 | 1 | // scan-detail-manifest.js — Execution pipeline / manifest card rendering. |
2 | | -// Extracted from scan-detail.js for maintainability. |
3 | 2 | // Exposes: window.ScanDetailManifest |
4 | 3 | (() => { |
5 | 4 | const esc = (...args) => (typeof window.esc === 'function' ? window.esc(...args) : String(args[0] ?? '')); |
|
49 | 48 | return d.toLocaleTimeString(); |
50 | 49 | } |
51 | 50 |
|
52 | | - function _moduleRow(m) { |
| 51 | + function _moduleRow(m, scanId) { |
| 52 | + const mod = esc(m.module || m.name || 'unknown'); |
53 | 53 | return ` |
54 | | - <tr> |
55 | | - <td style="font-weight:600;font-size:13px">${esc(m.module || m.name || 'unknown')}</td> |
| 54 | + <tr class="manifest-row" data-module="${mod}" data-scan-id="${esc(scanId)}" style="cursor:pointer"> |
| 55 | + <td style="font-weight:600;font-size:13px">${mod}</td> |
56 | 56 | <td><span class="badge ${manifestStatusBadge(m.status)}">${esc(m.status)}</span></td> |
57 | 57 | <td style="font-family:monospace;font-size:12px;color:var(--text-muted)">${manifestArtifactLabel(m)}</td> |
58 | 58 | <td style="font-size:11px;color:var(--text-muted)">${manifestStartedLabel(m)}</td> |
59 | 59 | <td style="font-family:monospace;font-size:12px">${formatManifestDuration(m.duration_ms)}</td> |
| 60 | + </tr> |
| 61 | + <tr class="manifest-log-row" id="manifest-log-row-${mod}" style="display:none;background:rgba(0,0,0,.25)"> |
| 62 | + <td colspan="5" style="padding:0;border:none"> |
| 63 | + <div class="manifest-log-panel" id="manifest-log-panel-${mod}" style="padding:12px 16px;max-height:400px;overflow:auto;font-family:var(--font-mono,monospace);font-size:12px"> |
| 64 | + <div style="color:var(--text-muted)">Click to load logs…</div> |
| 65 | + </div> |
| 66 | + </td> |
60 | 67 | </tr>`; |
61 | 68 | } |
62 | 69 |
|
| 70 | + async function loadModuleLogs(scanId, module, container) { |
| 71 | + container.innerHTML = '<div style="color:var(--text-muted);padding:8px 0">Loading logs…</div>'; |
| 72 | + try { |
| 73 | + const resp = await apiFetch(`/api/scans/${encodeURIComponent(scanId)}/logs?module=${encodeURIComponent(module)}`); |
| 74 | + const lines = Array.isArray(resp?.lines) ? resp.lines : []; |
| 75 | + if (!lines.length) { |
| 76 | + container.innerHTML = '<div style="color:var(--text-muted);padding:8px 0">No logs captured for this phase yet.</div>'; |
| 77 | + return; |
| 78 | + } |
| 79 | + const html = lines.map((ln) => { |
| 80 | + const ts = ln.timestamp ? new Date(ln.timestamp).toLocaleTimeString() : ''; |
| 81 | + const level = String(ln.level || 'info').toLowerCase(); |
| 82 | + let color = 'var(--text-secondary)'; |
| 83 | + if (level === 'error' || level === 'fatal' || level === 'panic') color = '#ef4444'; |
| 84 | + else if (level === 'warn' || level === 'warning') color = '#f59e0b'; |
| 85 | + else if (level === 'debug') color = '#94a3b8'; |
| 86 | + else if (level === 'info') color = '#22c55e'; |
| 87 | + const msg = esc(ln.message || ''); |
| 88 | + const fields = ln.fields && Object.keys(ln.fields).length |
| 89 | + ? ' <span style="color:var(--text-muted);font-size:11px">' + esc(JSON.stringify(ln.fields)) + '</span>' |
| 90 | + : ''; |
| 91 | + return `<div style="padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04)"><span style="color:var(--text-muted);font-size:11px;margin-right:8px">${esc(ts)}</span><span style="color:${color};font-weight:600;margin-right:8px">${esc(level.toUpperCase())}</span><span style="color:var(--text-primary)">${msg}</span>${fields}</div>`; |
| 92 | + }).join(''); |
| 93 | + container.innerHTML = html; |
| 94 | + } catch (e) { |
| 95 | + container.innerHTML = `<div style="color:var(--accent-red);padding:8px 0">Failed to load logs: ${esc(e.message || String(e))}</div>`; |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + function wireManifestRowClicks(root) { |
| 100 | + if (!root) return; |
| 101 | + root.querySelectorAll('.manifest-row').forEach((row) => { |
| 102 | + row.addEventListener('click', async () => { |
| 103 | + const mod = row.getAttribute('data-module'); |
| 104 | + const scanId = row.getAttribute('data-scan-id'); |
| 105 | + const logRow = document.getElementById(`manifest-log-row-${mod}`); |
| 106 | + const panel = document.getElementById(`manifest-log-panel-${mod}`); |
| 107 | + if (!logRow || !panel) return; |
| 108 | + |
| 109 | + const isOpen = logRow.style.display !== 'none'; |
| 110 | + // Close any open log rows first |
| 111 | + root.querySelectorAll('.manifest-log-row').forEach((r) => { r.style.display = 'none'; }); |
| 112 | + |
| 113 | + if (!isOpen) { |
| 114 | + logRow.style.display = 'table-row'; |
| 115 | + // Load logs only on first open |
| 116 | + if (panel.dataset.loaded !== '1') { |
| 117 | + panel.dataset.loaded = '1'; |
| 118 | + await loadModuleLogs(scanId, mod, panel); |
| 119 | + } |
| 120 | + } |
| 121 | + }); |
| 122 | + }); |
| 123 | + } |
| 124 | + |
63 | 125 | function renderScanManifestCard(manifest, scan) { |
64 | 126 | const modules = Array.isArray(manifest?.modules) ? manifest.modules : []; |
65 | 127 | const scanStatus = scan?.status || scan?.Status || ''; |
66 | 128 | const isActive = /running|starting|paused|cancelling/i.test(scanStatus); |
| 129 | + const scanId = scan?.scan_id || scan?.ScanID || ''; |
67 | 130 |
|
68 | 131 | if (!modules.length && !isActive) return ''; |
69 | 132 |
|
70 | 133 | return ` |
71 | 134 | <div class="modern-card" style="margin-bottom:20px"> |
72 | 135 | <div class="card-header" style="cursor:pointer" onclick="const b=this.nextElementSibling; b.style.display=b.style.display==='none'?'block':'none'"> |
73 | 136 | <div class="card-title"><span class="card-title-icon">⚙</span>Execution Pipeline</div> |
74 | | - <div style="font-size:11px;color:var(--text-muted)">${modules.length} phases documented</div> |
| 137 | + <div style="font-size:11px;color:var(--text-muted)">${modules.length} phases documented · click any row for logs</div> |
75 | 138 | </div> |
76 | | - <div class="card-body" style="padding:0"> |
| 139 | + <div class="card-body" style="padding:0;display:block"> |
77 | 140 | <table class="dashboard-table" style="width:100%"> |
78 | 141 | <thead><tr><th>Phase</th><th>Status</th><th>Artifacts</th><th>Started</th><th>Duration</th></tr></thead> |
79 | 142 | <tbody id="scan-manifest-tbody"> |
80 | | - ${modules.map(_moduleRow).join('')} |
| 143 | + ${modules.map((m) => _moduleRow(m, scanId)).join('')} |
81 | 144 | </tbody> |
82 | 145 | </table> |
83 | 146 | </div> |
|
96 | 159 | const resp = await fetchScanManifest(scanId); |
97 | 160 | if (!resp || !resp.manifest) return; |
98 | 161 | const modules = Array.isArray(resp.manifest.modules) ? resp.manifest.modules : []; |
| 162 | + const scanIdVal = scan?.scan_id || scan?.ScanID || ''; |
99 | 163 | const tbody = document.getElementById('scan-manifest-tbody'); |
100 | 164 | if (tbody) { |
101 | | - tbody.innerHTML = modules.map(_moduleRow).join(''); |
| 165 | + tbody.innerHTML = modules.map((m) => _moduleRow(m, scanIdVal)).join(''); |
| 166 | + wireManifestRowClicks(tbody.closest('.modern-card')); |
102 | 167 | } |
103 | 168 | } |
104 | 169 |
|
|
110 | 175 | renderScanManifestCard, |
111 | 176 | fetchScanManifest, |
112 | 177 | refreshScanManifestCard, |
| 178 | + wireManifestRowClicks, |
113 | 179 | }; |
114 | 180 | })(); |
0 commit comments