nestjs-profiler 1.0.22 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +160 -47
- package/collectors/http-collector.d.ts +12 -0
- package/collectors/http-collector.js +132 -0
- package/collectors/http-collector.js.map +1 -0
- package/collectors/log-collector.d.ts +3 -1
- package/collectors/log-collector.js +33 -12
- package/collectors/log-collector.js.map +1 -1
- package/common/profiler-options.interface.d.ts +1 -0
- package/common/profiler.model.d.ts +14 -0
- package/controllers/profiler.controller.d.ts +14 -1
- package/controllers/profiler.controller.js +148 -2
- package/controllers/profiler.controller.js.map +1 -1
- package/package.json +2 -2
- package/profiler.module.js +6 -0
- package/profiler.module.js.map +1 -1
- package/services/code-quality.service.d.ts +65 -0
- package/services/code-quality.service.js +216 -0
- package/services/code-quality.service.js.map +1 -0
- package/services/health.service.d.ts +59 -0
- package/services/health.service.js +175 -0
- package/services/health.service.js.map +1 -0
- package/services/profiler.service.d.ts +43 -1
- package/services/profiler.service.js +120 -0
- package/services/profiler.service.js.map +1 -1
- package/services/template-builder.service.d.ts +7 -0
- package/services/template-builder.service.js +278 -1
- package/services/template-builder.service.js.map +1 -1
- package/services/view.service.js +24 -12
- package/services/view.service.js.map +1 -1
- package/views/code-quality.html +573 -0
- package/views/dashboard.html +24 -0
- package/views/detail.html +2 -0
- package/views/health.html +468 -0
- package/views/layout.html +64 -1
- package/views/live-logs.html +324 -0
- package/views/summary.html +205 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
<div id="healthRoot">
|
|
2
|
+
|
|
3
|
+
<!-- ── Idle / intro state — shown by default; JS replaces it immediately ── -->
|
|
4
|
+
<div id="idleState" class="bg-white rounded-xl border border-gray-200 shadow-sm p-10">
|
|
5
|
+
<div class="max-w-lg mx-auto text-center">
|
|
6
|
+
<div class="flex justify-center mb-5">
|
|
7
|
+
<div class="w-14 h-14 rounded-full bg-indigo-50 flex items-center justify-center">
|
|
8
|
+
<svg class="w-7 h-7 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
9
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
10
|
+
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
|
11
|
+
</svg>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<h2 class="text-lg font-semibold text-gray-800 mb-1">Package Health</h2>
|
|
15
|
+
<p class="text-sm text-gray-500 mb-6">Runs <code class="bg-gray-100 px-1 rounded">npm audit</code> and <code class="bg-gray-100 px-1 rounded">npm outdated</code> to surface vulnerabilities and stale dependencies.</p>
|
|
16
|
+
<button onclick="window.__profilerRunHealthAudit && window.__profilerRunHealthAudit()"
|
|
17
|
+
class="px-4 py-2 text-sm font-medium rounded-lg bg-indigo-600 hover:bg-indigo-700 text-white">
|
|
18
|
+
Run Health Audit
|
|
19
|
+
</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<!-- ── Loading state ────────────────────────────────────────────────────── -->
|
|
24
|
+
<div id="loadingState" class="hidden bg-white rounded-xl border border-gray-200 shadow-sm p-10">
|
|
25
|
+
<div class="max-w-lg mx-auto text-center">
|
|
26
|
+
<!-- Icon -->
|
|
27
|
+
<div class="flex justify-center mb-5">
|
|
28
|
+
<div class="w-14 h-14 rounded-full bg-indigo-50 flex items-center justify-center">
|
|
29
|
+
<svg class="w-7 h-7 text-indigo-500 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
30
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
31
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
|
32
|
+
</svg>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<h2 class="text-lg font-semibold text-gray-800 mb-1">Running security audit…</h2>
|
|
37
|
+
<p id="loadingLabel" class="text-sm text-gray-500 mb-6">Connecting to npm registry</p>
|
|
38
|
+
|
|
39
|
+
<!-- Progress bar track -->
|
|
40
|
+
<div class="w-full bg-gray-100 rounded-full h-2.5 overflow-hidden">
|
|
41
|
+
<div id="progressBar"
|
|
42
|
+
class="h-2.5 rounded-full bg-indigo-500 transition-all duration-700 ease-out"
|
|
43
|
+
style="width: 0%">
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Step labels -->
|
|
48
|
+
<div class="mt-5 flex justify-between text-xs text-gray-400">
|
|
49
|
+
<span id="step1" class="font-medium">npm audit</span>
|
|
50
|
+
<span id="step2">npm outdated</span>
|
|
51
|
+
<span id="step3">Analysing results</span>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- ── Error state ────────────────────────────────────────────────────── -->
|
|
57
|
+
<div id="errorState" class="hidden bg-red-50 border border-red-200 rounded-xl p-8 text-center">
|
|
58
|
+
<svg class="w-10 h-10 text-red-400 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
60
|
+
d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
61
|
+
</svg>
|
|
62
|
+
<p class="text-red-700 font-semibold mb-1">Audit failed</p>
|
|
63
|
+
<p id="errorMsg" class="text-red-500 text-sm mb-4"></p>
|
|
64
|
+
<button onclick="window.__profilerRunHealthAudit()" class="px-4 py-2 text-sm font-medium rounded-lg bg-red-100 hover:bg-red-200 text-red-700">
|
|
65
|
+
Try again
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- ── Results state ──────────────────────────────────────────────────── -->
|
|
70
|
+
<div id="resultsState" class="hidden space-y-6">
|
|
71
|
+
|
|
72
|
+
<!-- Header bar -->
|
|
73
|
+
<div class="flex items-center justify-between">
|
|
74
|
+
<div>
|
|
75
|
+
<h2 class="text-lg font-bold text-gray-800">Package Health</h2>
|
|
76
|
+
<p id="scanMeta" class="text-xs text-gray-400 mt-0.5"></p>
|
|
77
|
+
</div>
|
|
78
|
+
<button onclick="window.__profilerRunHealthAudit()"
|
|
79
|
+
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border border-gray-200 bg-white hover:bg-gray-50 text-gray-600">
|
|
80
|
+
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
81
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
82
|
+
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
|
83
|
+
</svg>
|
|
84
|
+
Re-run audit
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<!-- ── Environment cards ── -->
|
|
89
|
+
<div id="envCards" class="grid grid-cols-2 sm:grid-cols-4 gap-3"></div>
|
|
90
|
+
|
|
91
|
+
<!-- ── Severity summary cards ── -->
|
|
92
|
+
<div id="severityCards" class="grid grid-cols-2 sm:grid-cols-4 gap-3"></div>
|
|
93
|
+
|
|
94
|
+
<!-- ── Vulnerabilities table ── -->
|
|
95
|
+
<div id="vulnSection" class="hidden bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
96
|
+
<div class="px-5 py-3 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
|
|
97
|
+
<h3 class="text-sm font-bold text-gray-600 uppercase tracking-widest">Vulnerabilities</h3>
|
|
98
|
+
<span id="vulnCount" class="text-xs text-gray-400"></span>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="overflow-x-auto">
|
|
101
|
+
<table class="w-full text-sm">
|
|
102
|
+
<thead class="bg-gray-50 text-xs text-gray-500 uppercase">
|
|
103
|
+
<tr>
|
|
104
|
+
<th class="px-4 py-2 text-left w-24">Severity</th>
|
|
105
|
+
<th class="px-4 py-2 text-left">Package</th>
|
|
106
|
+
<th class="px-4 py-2 text-left">Vulnerable range</th>
|
|
107
|
+
<th class="px-4 py-2 text-left w-20">Direct</th>
|
|
108
|
+
<th class="px-4 py-2 text-left">Fix</th>
|
|
109
|
+
</tr>
|
|
110
|
+
</thead>
|
|
111
|
+
<tbody id="vulnBody" class="divide-y divide-gray-100"></tbody>
|
|
112
|
+
</table>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<!-- ── Outdated packages table ── -->
|
|
117
|
+
<div id="outdatedSection" class="hidden bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
118
|
+
<div class="px-5 py-3 border-b border-gray-100 bg-gray-50 flex justify-between items-center">
|
|
119
|
+
<h3 class="text-sm font-bold text-gray-600 uppercase tracking-widest">Outdated Packages</h3>
|
|
120
|
+
<span id="outdatedCount" class="text-xs text-gray-400"></span>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="overflow-x-auto">
|
|
123
|
+
<table class="w-full text-sm">
|
|
124
|
+
<thead class="bg-gray-50 text-xs text-gray-500 uppercase">
|
|
125
|
+
<tr>
|
|
126
|
+
<th class="px-4 py-2 text-left">Package</th>
|
|
127
|
+
<th class="px-4 py-2 text-center">Current</th>
|
|
128
|
+
<th class="px-4 py-2 text-center">Wanted</th>
|
|
129
|
+
<th class="px-4 py-2 text-center">Latest</th>
|
|
130
|
+
<th class="px-4 py-2 text-left">Status</th>
|
|
131
|
+
</tr>
|
|
132
|
+
</thead>
|
|
133
|
+
<tbody id="outdatedBody" class="divide-y divide-gray-100"></tbody>
|
|
134
|
+
</table>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<!-- ── All-clear state ── -->
|
|
139
|
+
<div id="allClear" class="hidden bg-green-50 border border-green-200 rounded-xl p-8 text-center">
|
|
140
|
+
<svg class="w-12 h-12 text-green-400 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
141
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
142
|
+
</svg>
|
|
143
|
+
<p class="text-green-700 font-semibold text-lg">No vulnerabilities found</p>
|
|
144
|
+
<p class="text-green-500 text-sm mt-1">All packages are up to date and secure.</p>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<script>
|
|
151
|
+
// ── Severity config ───────────────────────────────────────────────────────
|
|
152
|
+
const SEV = {
|
|
153
|
+
critical: { label: 'Critical', bg: 'bg-red-50', text: 'text-red-700', badge: 'bg-red-100 text-red-700', icon: '🔴' },
|
|
154
|
+
high: { label: 'High', bg: 'bg-orange-50', text: 'text-orange-700', badge: 'bg-orange-100 text-orange-700', icon: '🟠' },
|
|
155
|
+
moderate: { label: 'Moderate', bg: 'bg-yellow-50', text: 'text-yellow-700', badge: 'bg-yellow-100 text-yellow-700', icon: '🟡' },
|
|
156
|
+
low: { label: 'Low', bg: 'bg-blue-50', text: 'text-blue-700', badge: 'bg-blue-100 text-blue-700', icon: '🔵' },
|
|
157
|
+
info: { label: 'Info', bg: 'bg-gray-50', text: 'text-gray-600', badge: 'bg-gray-100 text-gray-600', icon: '⚪' },
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// ── Progress simulation ───────────────────────────────────────────────────
|
|
161
|
+
const STEPS = [
|
|
162
|
+
{ pct: 15, label: 'Connecting to npm registry…', step: 'step1' },
|
|
163
|
+
{ pct: 40, label: 'Running npm audit…', step: 'step1' },
|
|
164
|
+
{ pct: 70, label: 'Checking outdated packages…', step: 'step2' },
|
|
165
|
+
{ pct: 88, label: 'Analysing results…', step: 'step3' },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
let progressTimer = null;
|
|
169
|
+
let stepIndex = 0;
|
|
170
|
+
|
|
171
|
+
function startProgress() {
|
|
172
|
+
stepIndex = 0;
|
|
173
|
+
setProgress(0, 'Connecting to npm registry…');
|
|
174
|
+
|
|
175
|
+
progressTimer = setInterval(() => {
|
|
176
|
+
if (stepIndex >= STEPS.length) { clearInterval(progressTimer); return; }
|
|
177
|
+
const s = STEPS[stepIndex++];
|
|
178
|
+
setProgress(s.pct, s.label);
|
|
179
|
+
document.querySelectorAll('#loadingState .flex.justify-between span').forEach(el => {
|
|
180
|
+
el.classList.remove('text-indigo-600', 'font-semibold');
|
|
181
|
+
});
|
|
182
|
+
const active = document.getElementById(s.step);
|
|
183
|
+
if (active) active.classList.add('text-indigo-600', 'font-semibold');
|
|
184
|
+
}, 1200);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function setProgress(pct, label) {
|
|
188
|
+
const bar = document.getElementById('progressBar');
|
|
189
|
+
const lbl = document.getElementById('loadingLabel');
|
|
190
|
+
if (bar) bar.style.width = pct + '%';
|
|
191
|
+
if (lbl) lbl.textContent = label;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function finishProgress(cb) {
|
|
195
|
+
clearInterval(progressTimer);
|
|
196
|
+
setProgress(100, 'Done!');
|
|
197
|
+
setTimeout(cb, 500);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── State switches ────────────────────────────────────────────────────────
|
|
201
|
+
function hideAll() {
|
|
202
|
+
['idleState','loadingState','errorState','resultsState'].forEach(id => {
|
|
203
|
+
const el = document.getElementById(id);
|
|
204
|
+
if (el) el.classList.add('hidden');
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function showLoading() {
|
|
209
|
+
hideAll();
|
|
210
|
+
const el = document.getElementById('loadingState');
|
|
211
|
+
if (el) el.classList.remove('hidden');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function showError(msg) {
|
|
215
|
+
clearInterval(progressTimer);
|
|
216
|
+
hideAll();
|
|
217
|
+
const el = document.getElementById('errorState');
|
|
218
|
+
if (el) el.classList.remove('hidden');
|
|
219
|
+
const msgEl = document.getElementById('errorMsg');
|
|
220
|
+
if (msgEl) msgEl.textContent = msg;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function showResults(data) {
|
|
224
|
+
hideAll();
|
|
225
|
+
const el = document.getElementById('resultsState');
|
|
226
|
+
if (el) el.classList.remove('hidden');
|
|
227
|
+
renderResults(data);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Rendering ─────────────────────────────────────────────────────────────
|
|
231
|
+
function renderResults(data) {
|
|
232
|
+
renderEnvCards(data);
|
|
233
|
+
renderSeverityCards(data);
|
|
234
|
+
renderVulnerabilities(data);
|
|
235
|
+
renderOutdated(data);
|
|
236
|
+
|
|
237
|
+
// Scan metadata + cached badge
|
|
238
|
+
const meta = document.getElementById('scanMeta');
|
|
239
|
+
if (meta) {
|
|
240
|
+
const time = new Date(data.scannedAt).toLocaleTimeString();
|
|
241
|
+
const fromCache = data.fromCache;
|
|
242
|
+
const age = Math.round((Date.now() - data.scannedAt) / 1000);
|
|
243
|
+
const ageLabel = age < 60 ? `${age}s ago` : `${Math.round(age / 60)}m ago`;
|
|
244
|
+
meta.innerHTML = `
|
|
245
|
+
Scanned with <strong>${data.packageManager}</strong> · ${time}
|
|
246
|
+
${fromCache
|
|
247
|
+
? `<span class="ml-2 inline-flex items-center gap-1 px-1.5 py-0.5 text-xs rounded-full bg-gray-100 text-gray-500">
|
|
248
|
+
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
249
|
+
Cached · ${ageLabel}
|
|
250
|
+
</span>`
|
|
251
|
+
: `<span class="ml-2 inline-flex items-center gap-1 px-1.5 py-0.5 text-xs rounded-full bg-green-100 text-green-700">
|
|
252
|
+
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2a10 10 0 100 20A10 10 0 0012 2zm-1 14.5v-5l-2 2-1.41-1.41L11 8.67V7h2v1.67l3.41 3.42L15 13.5l-2-2v5h-2z"/></svg>
|
|
253
|
+
Fresh scan
|
|
254
|
+
</span>`
|
|
255
|
+
}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// All-clear
|
|
259
|
+
const hasVulns = data.audit?.metadata?.vulnerabilities?.total > 0;
|
|
260
|
+
const hasOutdated = Object.keys(data.outdated || {}).length > 0;
|
|
261
|
+
document.getElementById('allClear').classList.toggle('hidden', hasVulns || hasOutdated);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function renderEnvCards(data) {
|
|
265
|
+
const nodeOk = checkNodeEngine(data.nodeVersion, data.engines?.node);
|
|
266
|
+
document.getElementById('envCards').innerHTML = [
|
|
267
|
+
envCard('Node.js', data.nodeVersion, nodeOk ? 'green' : 'yellow', nodeOk ? 'Meets engines requirement' : 'Check engines field'),
|
|
268
|
+
envCard('npm', `v${data.npmVersion}`, 'gray', 'Package manager'),
|
|
269
|
+
envCard('Package manager', data.packageManager, 'gray', 'Detected from lock file'),
|
|
270
|
+
data.engines ? envCard('engines.node', data.engines.node || 'not set', 'gray', 'From package.json') : '',
|
|
271
|
+
].join('');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function envCard(label, value, color, sub) {
|
|
275
|
+
const colors = {
|
|
276
|
+
green: 'bg-green-50 border-green-200 text-green-700',
|
|
277
|
+
yellow: 'bg-yellow-50 border-yellow-200 text-yellow-700',
|
|
278
|
+
gray: 'bg-gray-50 border-gray-200 text-gray-700',
|
|
279
|
+
};
|
|
280
|
+
return `
|
|
281
|
+
<div class="rounded-lg border ${colors[color]} p-3">
|
|
282
|
+
<div class="text-xs text-gray-400 mb-1">${label}</div>
|
|
283
|
+
<div class="font-semibold text-sm">${value}</div>
|
|
284
|
+
<div class="text-xs mt-0.5 opacity-70">${sub}</div>
|
|
285
|
+
</div>`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function renderSeverityCards(data) {
|
|
289
|
+
// Hide severity cards entirely when audit didn't run
|
|
290
|
+
const el = document.getElementById('severityCards');
|
|
291
|
+
if (!data.audit) { if (el) el.classList.add('hidden'); return; }
|
|
292
|
+
if (el) el.classList.remove('hidden');
|
|
293
|
+
const counts = data.audit?.metadata?.vulnerabilities || {};
|
|
294
|
+
const order = ['critical', 'high', 'moderate', 'low'];
|
|
295
|
+
document.getElementById('severityCards').innerHTML = order.map(sev => {
|
|
296
|
+
const n = counts[sev] || 0;
|
|
297
|
+
const cfg = SEV[sev];
|
|
298
|
+
const ring = n > 0 ? 'ring-2 ring-offset-1 ' + (sev === 'critical' ? 'ring-red-300' : sev === 'high' ? 'ring-orange-300' : sev === 'moderate' ? 'ring-yellow-300' : 'ring-blue-300') : '';
|
|
299
|
+
return `
|
|
300
|
+
<div class="rounded-xl border ${cfg.bg} p-4 text-center ${ring}">
|
|
301
|
+
<div class="text-3xl font-bold ${cfg.text}">${n}</div>
|
|
302
|
+
<div class="text-xs font-semibold ${cfg.text} uppercase tracking-widest mt-1">${cfg.label}</div>
|
|
303
|
+
</div>`;
|
|
304
|
+
}).join('');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderVulnerabilities(data) {
|
|
308
|
+
const section = document.getElementById('vulnSection');
|
|
309
|
+
|
|
310
|
+
// Audit failed (network/registry unreachable) — show inline warning, still render outdated
|
|
311
|
+
if (!data.audit && data.error) {
|
|
312
|
+
section.classList.remove('hidden');
|
|
313
|
+
document.getElementById('vulnCount').textContent = '';
|
|
314
|
+
document.getElementById('vulnBody').innerHTML = `
|
|
315
|
+
<tr><td colspan="5" class="px-4 py-6">
|
|
316
|
+
<div class="flex items-start gap-3 bg-amber-50 border border-amber-200 rounded-lg p-4">
|
|
317
|
+
<svg class="w-5 h-5 text-amber-500 shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
318
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
319
|
+
</svg>
|
|
320
|
+
<div>
|
|
321
|
+
<p class="text-sm font-semibold text-amber-800">Vulnerability scan unavailable</p>
|
|
322
|
+
<p class="text-xs text-amber-600 mt-0.5">${data.error}</p>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</td></tr>`;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const vulns = Object.values(data.audit?.vulnerabilities || {});
|
|
330
|
+
const body = document.getElementById('vulnBody');
|
|
331
|
+
const count = document.getElementById('vulnCount');
|
|
332
|
+
|
|
333
|
+
if (!vulns.length) { section.classList.add('hidden'); return; }
|
|
334
|
+
section.classList.remove('hidden');
|
|
335
|
+
count.textContent = `${vulns.length} package${vulns.length !== 1 ? 's' : ''} affected`;
|
|
336
|
+
|
|
337
|
+
// Sort: critical → high → moderate → low → info
|
|
338
|
+
const order = ['critical', 'high', 'moderate', 'low', 'info'];
|
|
339
|
+
vulns.sort((a, b) => order.indexOf(a.severity) - order.indexOf(b.severity));
|
|
340
|
+
|
|
341
|
+
body.innerHTML = vulns.map(v => {
|
|
342
|
+
const cfg = SEV[v.severity] || SEV.info;
|
|
343
|
+
const fix = v.fixAvailable === true
|
|
344
|
+
? `<span class="text-green-600 text-xs font-medium">✓ Available</span>`
|
|
345
|
+
: typeof v.fixAvailable === 'object' && v.fixAvailable
|
|
346
|
+
? `<span class="text-yellow-600 text-xs font-medium">⚠ Breaking (${v.fixAvailable.name}@${v.fixAvailable.version})</span>`
|
|
347
|
+
: `<span class="text-gray-400 text-xs">None</span>`;
|
|
348
|
+
const via = Array.isArray(v.via) ? v.via.filter(x => typeof x === 'string').join(', ') : '';
|
|
349
|
+
|
|
350
|
+
return `
|
|
351
|
+
<tr class="hover:bg-gray-50">
|
|
352
|
+
<td class="px-4 py-3">
|
|
353
|
+
<span class="inline-block text-xs font-semibold px-2 py-0.5 rounded ${cfg.badge}">${cfg.label}</span>
|
|
354
|
+
</td>
|
|
355
|
+
<td class="px-4 py-3">
|
|
356
|
+
<div class="font-mono text-sm font-semibold text-gray-800">${v.name}</div>
|
|
357
|
+
${via ? `<div class="text-xs text-gray-400 mt-0.5">via ${via}</div>` : ''}
|
|
358
|
+
</td>
|
|
359
|
+
<td class="px-4 py-3 font-mono text-xs text-gray-500">${v.range || '-'}</td>
|
|
360
|
+
<td class="px-4 py-3 text-center text-xs">${v.isDirect ? '<span class="text-indigo-600 font-medium">Direct</span>' : '<span class="text-gray-400">Transitive</span>'}</td>
|
|
361
|
+
<td class="px-4 py-3">${fix}</td>
|
|
362
|
+
</tr>`;
|
|
363
|
+
}).join('');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderOutdated(data) {
|
|
367
|
+
const entries = Object.entries(data.outdated || {});
|
|
368
|
+
const section = document.getElementById('outdatedSection');
|
|
369
|
+
const body = document.getElementById('outdatedBody');
|
|
370
|
+
const count = document.getElementById('outdatedCount');
|
|
371
|
+
|
|
372
|
+
if (!entries.length) { section.classList.add('hidden'); return; }
|
|
373
|
+
section.classList.remove('hidden');
|
|
374
|
+
count.textContent = `${entries.length} package${entries.length !== 1 ? 's' : ''}`;
|
|
375
|
+
|
|
376
|
+
body.innerHTML = entries.map(([name, pkg]) => {
|
|
377
|
+
const isMajor = pkg.latest && pkg.current && pkg.latest.split('.')[0] !== pkg.current.split('.')[0];
|
|
378
|
+
const isWanted = pkg.current !== pkg.wanted;
|
|
379
|
+
const badge = isMajor
|
|
380
|
+
? '<span class="text-xs font-semibold px-1.5 py-0.5 rounded bg-red-100 text-red-700">Major update</span>'
|
|
381
|
+
: isWanted
|
|
382
|
+
? '<span class="text-xs font-semibold px-1.5 py-0.5 rounded bg-yellow-100 text-yellow-700">Update available</span>'
|
|
383
|
+
: '<span class="text-xs font-semibold px-1.5 py-0.5 rounded bg-blue-100 text-blue-700">Latest available</span>';
|
|
384
|
+
return `
|
|
385
|
+
<tr class="hover:bg-gray-50">
|
|
386
|
+
<td class="px-4 py-3 font-mono text-sm font-semibold text-gray-800">${name}</td>
|
|
387
|
+
<td class="px-4 py-3 text-center font-mono text-xs text-gray-500">${pkg.current || '-'}</td>
|
|
388
|
+
<td class="px-4 py-3 text-center font-mono text-xs ${isWanted ? 'text-yellow-600 font-semibold' : 'text-gray-500'}">${pkg.wanted || '-'}</td>
|
|
389
|
+
<td class="px-4 py-3 text-center font-mono text-xs ${isMajor ? 'text-red-600 font-semibold' : 'text-green-600'}">${pkg.latest || '-'}</td>
|
|
390
|
+
<td class="px-4 py-3">${badge}</td>
|
|
391
|
+
</tr>`;
|
|
392
|
+
}).join('');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function checkNodeEngine(current, required) {
|
|
396
|
+
if (!required || !current) return true;
|
|
397
|
+
// Basic semver check: just compare major versions
|
|
398
|
+
try {
|
|
399
|
+
const curMajor = parseInt(current.replace('v', '').split('.')[0]);
|
|
400
|
+
const reqMatch = required.match(/(\d+)/);
|
|
401
|
+
if (reqMatch) return curMajor >= parseInt(reqMatch[1]);
|
|
402
|
+
} catch { /* ignore */ }
|
|
403
|
+
return true;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Client-side cache (survives SPA navigation) ───────────────────────────
|
|
407
|
+
// Stored on window so it persists when the layout re-injects this page's HTML
|
|
408
|
+
if (!window.__profilerHealthCache) window.__profilerHealthCache = null;
|
|
409
|
+
|
|
410
|
+
// Cancel any in-flight fetch from a previous navigation
|
|
411
|
+
if (window.__profilerHealthAbort) {
|
|
412
|
+
window.__profilerHealthAbort.abort();
|
|
413
|
+
window.__profilerHealthAbort = null;
|
|
414
|
+
}
|
|
415
|
+
if (window.__profilerHealthProgressTimer) {
|
|
416
|
+
clearInterval(window.__profilerHealthProgressTimer);
|
|
417
|
+
window.__profilerHealthProgressTimer = null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Override startProgress / finishProgress to track timers globally
|
|
421
|
+
const _startProgress = startProgress;
|
|
422
|
+
startProgress = function() {
|
|
423
|
+
_startProgress();
|
|
424
|
+
window.__profilerHealthProgressTimer = progressTimer;
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// ── Main fetch ────────────────────────────────────────────────────────────
|
|
428
|
+
async function runAudit(force = false) {
|
|
429
|
+
// If not forced and we have a client-side cache, show it instantly
|
|
430
|
+
if (!force && window.__profilerHealthCache) {
|
|
431
|
+
showResults(window.__profilerHealthCache);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
showLoading();
|
|
436
|
+
startProgress();
|
|
437
|
+
|
|
438
|
+
const controller = new AbortController();
|
|
439
|
+
window.__profilerHealthAbort = controller;
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const url = force ? '/__profiler/api/health?force=true' : '/__profiler/api/health';
|
|
443
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
444
|
+
const data = await res.json();
|
|
445
|
+
|
|
446
|
+
window.__profilerHealthCache = data;
|
|
447
|
+
window.__profilerHealthAbort = null;
|
|
448
|
+
|
|
449
|
+
finishProgress(() => {
|
|
450
|
+
// Always show results — audit errors are rendered inline
|
|
451
|
+
showResults(data);
|
|
452
|
+
});
|
|
453
|
+
} catch (e) {
|
|
454
|
+
if (e.name === 'AbortError') return; // navigated away — silently ignore
|
|
455
|
+
showError(e.message || 'Could not reach /__profiler/api/health');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Re-run button calls this
|
|
460
|
+
window.__profilerRunHealthAudit = () => runAudit(true);
|
|
461
|
+
|
|
462
|
+
// On load: show cache instantly if available, else auto-start fresh audit
|
|
463
|
+
try {
|
|
464
|
+
runAudit(false);
|
|
465
|
+
} catch(e) {
|
|
466
|
+
showError('Failed to start audit: ' + e.message);
|
|
467
|
+
}
|
|
468
|
+
</script>
|
package/views/layout.html
CHANGED
|
@@ -91,7 +91,16 @@
|
|
|
91
91
|
</a>
|
|
92
92
|
</div>
|
|
93
93
|
<nav class="flex-1 px-4 py-6 space-y-1">
|
|
94
|
-
<a href="/__profiler"
|
|
94
|
+
<a href="/__profiler/view/summary"
|
|
95
|
+
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ summaryActive }}}">
|
|
96
|
+
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ summaryIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
97
|
+
stroke="currentColor">
|
|
98
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
99
|
+
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
100
|
+
</svg>
|
|
101
|
+
Summary
|
|
102
|
+
</a>
|
|
103
|
+
<a href="/__profiler/view/requests"
|
|
95
104
|
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ requestsActive }}}">
|
|
96
105
|
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ requestsIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
97
106
|
stroke="currentColor">
|
|
@@ -145,6 +154,43 @@
|
|
|
145
154
|
</svg>
|
|
146
155
|
Cache
|
|
147
156
|
</a>
|
|
157
|
+
<a href="/__profiler/view/http-calls"
|
|
158
|
+
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ httpCallsActive }}}">
|
|
159
|
+
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ httpCallsIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
160
|
+
stroke="currentColor">
|
|
161
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
162
|
+
d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
163
|
+
</svg>
|
|
164
|
+
HTTP Calls
|
|
165
|
+
</a>
|
|
166
|
+
<a href="/__profiler/view/health"
|
|
167
|
+
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ healthActive }}}">
|
|
168
|
+
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ healthIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
169
|
+
stroke="currentColor">
|
|
170
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
171
|
+
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
172
|
+
</svg>
|
|
173
|
+
Health
|
|
174
|
+
</a>
|
|
175
|
+
<a href="/__profiler/view/code-quality"
|
|
176
|
+
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ codeQualityActive }}}">
|
|
177
|
+
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ codeQualityIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
178
|
+
stroke="currentColor">
|
|
179
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
180
|
+
d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
|
181
|
+
</svg>
|
|
182
|
+
Code Quality
|
|
183
|
+
</a>
|
|
184
|
+
<a href="/__profiler/view/logs/live"
|
|
185
|
+
class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ liveLogsActive }}}">
|
|
186
|
+
<svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ liveLogsIconClass }}}" fill="none" viewBox="0 0 24 24"
|
|
187
|
+
stroke="currentColor">
|
|
188
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
189
|
+
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
190
|
+
</svg>
|
|
191
|
+
Live Logs
|
|
192
|
+
<span class="ml-auto w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
|
193
|
+
</a>
|
|
148
194
|
</nav>
|
|
149
195
|
<div class="flex-shrink-0 flex bg-slate-800 p-4">
|
|
150
196
|
<a href="#" class="flex-shrink-0 w-full group block">
|
|
@@ -212,6 +258,18 @@
|
|
|
212
258
|
});
|
|
213
259
|
};
|
|
214
260
|
|
|
261
|
+
// Re-execute <script> tags injected via innerHTML (browsers skip them by default)
|
|
262
|
+
const reExecScripts = (container) => {
|
|
263
|
+
container.querySelectorAll('script').forEach(oldScript => {
|
|
264
|
+
const newScript = document.createElement('script');
|
|
265
|
+
Array.from(oldScript.attributes).forEach(a =>
|
|
266
|
+
newScript.setAttribute(a.name, a.value)
|
|
267
|
+
);
|
|
268
|
+
newScript.textContent = oldScript.textContent;
|
|
269
|
+
oldScript.parentNode.replaceChild(newScript, oldScript);
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
215
273
|
const navigate = async (url) => {
|
|
216
274
|
try {
|
|
217
275
|
// Show loading state if needed
|
|
@@ -232,6 +290,9 @@
|
|
|
232
290
|
// Update Title
|
|
233
291
|
document.title = doc.title;
|
|
234
292
|
|
|
293
|
+
// Re-execute inline scripts (needed for live logs SSE connect(), etc.)
|
|
294
|
+
reExecScripts(mainContent);
|
|
295
|
+
|
|
235
296
|
// Re-attach listeners (like search)
|
|
236
297
|
const newSearchInput = document.getElementById('search');
|
|
237
298
|
if (newSearchInput) {
|
|
@@ -259,6 +320,8 @@
|
|
|
259
320
|
if (link && link.href && link.href.includes('/__profiler') && !link.getAttribute('target')) {
|
|
260
321
|
const url = new URL(link.href);
|
|
261
322
|
if (url.origin === window.location.origin) {
|
|
323
|
+
// Live logs uses SSE — always do a full page load so the script runs fresh
|
|
324
|
+
if (url.pathname.includes('/logs/live')) return;
|
|
262
325
|
e.preventDefault();
|
|
263
326
|
const path = url.pathname + url.search;
|
|
264
327
|
history.pushState({}, '', path);
|