nestjs-profiler 1.0.24 → 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.
@@ -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
@@ -100,7 +100,7 @@
100
100
  </svg>
101
101
  Summary
102
102
  </a>
103
- <a href="/__profiler"
103
+ <a href="/__profiler/view/requests"
104
104
  class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ requestsActive }}}">
105
105
  <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ requestsIconClass }}}" fill="none" viewBox="0 0 24 24"
106
106
  stroke="currentColor">
@@ -163,6 +163,24 @@
163
163
  </svg>
164
164
  HTTP Calls
165
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>
166
184
  <a href="/__profiler/view/logs/live"
167
185
  class="flex items-center px-2 py-2 text-sm font-medium rounded-md group {{{ liveLogsActive }}}">
168
186
  <svg class="mr-3 flex-shrink-0 h-6 w-6 {{{ liveLogsIconClass }}}" fill="none" viewBox="0 0 24 24"