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.
Files changed (36) hide show
  1. package/README.md +160 -47
  2. package/collectors/http-collector.d.ts +12 -0
  3. package/collectors/http-collector.js +132 -0
  4. package/collectors/http-collector.js.map +1 -0
  5. package/collectors/log-collector.d.ts +3 -1
  6. package/collectors/log-collector.js +33 -12
  7. package/collectors/log-collector.js.map +1 -1
  8. package/common/profiler-options.interface.d.ts +1 -0
  9. package/common/profiler.model.d.ts +14 -0
  10. package/controllers/profiler.controller.d.ts +14 -1
  11. package/controllers/profiler.controller.js +148 -2
  12. package/controllers/profiler.controller.js.map +1 -1
  13. package/package.json +2 -2
  14. package/profiler.module.js +6 -0
  15. package/profiler.module.js.map +1 -1
  16. package/services/code-quality.service.d.ts +65 -0
  17. package/services/code-quality.service.js +216 -0
  18. package/services/code-quality.service.js.map +1 -0
  19. package/services/health.service.d.ts +59 -0
  20. package/services/health.service.js +175 -0
  21. package/services/health.service.js.map +1 -0
  22. package/services/profiler.service.d.ts +43 -1
  23. package/services/profiler.service.js +120 -0
  24. package/services/profiler.service.js.map +1 -1
  25. package/services/template-builder.service.d.ts +7 -0
  26. package/services/template-builder.service.js +278 -1
  27. package/services/template-builder.service.js.map +1 -1
  28. package/services/view.service.js +24 -12
  29. package/services/view.service.js.map +1 -1
  30. package/views/code-quality.html +573 -0
  31. package/views/dashboard.html +24 -0
  32. package/views/detail.html +2 -0
  33. package/views/health.html +468 -0
  34. package/views/layout.html +64 -1
  35. package/views/live-logs.html +324 -0
  36. 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);