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,573 @@
1
+ <div id="cqRoot">
2
+
3
+ <!-- ── Idle state (default, visible until JS runs) ──────────────────────── -->
4
+ <div id="cqIdle" class="bg-white rounded-xl border border-gray-200 shadow-sm p-12">
5
+ <div class="max-w-lg mx-auto text-center">
6
+ <div class="flex justify-center mb-5">
7
+ <div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg">
8
+ <svg class="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
10
+ </svg>
11
+ </div>
12
+ </div>
13
+ <h2 class="text-xl font-bold text-gray-900 mb-2">Code Quality Analysis</h2>
14
+ <p class="text-sm text-gray-500 mb-2">Runs <code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">eslint --format json</code> and <code class="bg-gray-100 px-1.5 py-0.5 rounded text-xs font-mono">tsc --noEmit</code> on your project and surfaces every issue with rule explanations and fix hints.</p>
15
+ <p class="text-xs text-gray-400 mb-6">Requires ESLint and/or TypeScript to be configured in your project.</p>
16
+ <button onclick="window.__cqRun && window.__cqRun(true)"
17
+ class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold rounded-lg bg-gradient-to-r from-violet-600 to-indigo-600 hover:from-violet-700 hover:to-indigo-700 text-white shadow-md transition-all">
18
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
20
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
21
+ </svg>
22
+ Run Analysis
23
+ </button>
24
+ </div>
25
+ </div>
26
+
27
+ <!-- ── Loading state ─────────────────────────────────────────────────────── -->
28
+ <div id="cqLoading" class="hidden bg-white rounded-xl border border-gray-200 shadow-sm p-12">
29
+ <div class="max-w-lg mx-auto text-center">
30
+ <div class="flex justify-center mb-5">
31
+ <div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center shadow-lg">
32
+ <svg class="w-8 h-8 text-white animate-spin" fill="none" viewBox="0 0 24 24">
33
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
34
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
35
+ </svg>
36
+ </div>
37
+ </div>
38
+ <h2 class="text-lg font-bold text-gray-800 mb-1">Analysing your codebase…</h2>
39
+ <p id="cqLoadLabel" class="text-sm text-gray-500 mb-6">Starting ESLint</p>
40
+ <div class="w-full bg-gray-100 rounded-full h-2 overflow-hidden">
41
+ <div id="cqProgressBar" class="h-2 rounded-full bg-gradient-to-r from-violet-500 to-indigo-500 transition-all duration-700 ease-out" style="width:0%"></div>
42
+ </div>
43
+ <div class="mt-4 flex justify-between text-xs text-gray-400">
44
+ <span id="cqStep1">ESLint</span>
45
+ <span id="cqStep2">TypeScript</span>
46
+ <span id="cqStep3">Processing results</span>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- ── Error state ────────────────────────────────────────────────────────── -->
52
+ <div id="cqError" class="hidden bg-red-50 border border-red-200 rounded-xl p-8 text-center">
53
+ <svg class="w-10 h-10 text-red-400 mx-auto mb-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
54
+ <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"/>
55
+ </svg>
56
+ <p class="text-red-700 font-semibold mb-1">Analysis failed</p>
57
+ <p id="cqErrorMsg" class="text-red-500 text-sm mb-4"></p>
58
+ <button onclick="window.__cqRun && window.__cqRun(true)"
59
+ class="px-4 py-2 text-sm font-medium rounded-lg bg-red-100 hover:bg-red-200 text-red-700">
60
+ Try again
61
+ </button>
62
+ </div>
63
+
64
+ <!-- ── Results state ──────────────────────────────────────────────────────── -->
65
+ <div id="cqResults" class="hidden space-y-6">
66
+
67
+ <!-- Header -->
68
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
69
+ <div>
70
+ <h2 class="text-xl font-bold text-gray-900">Code Quality Report</h2>
71
+ <p id="cqScanMeta" class="text-xs text-gray-400 mt-0.5"></p>
72
+ </div>
73
+ <div class="flex items-center gap-2">
74
+ <!-- View toggle -->
75
+ <div class="inline-flex rounded-lg border border-gray-200 bg-gray-50 p-0.5 text-xs font-medium">
76
+ <button id="btnByFile" onclick="setView('file')"
77
+ class="px-3 py-1.5 rounded-md transition-all bg-white shadow text-gray-800">
78
+ By File
79
+ </button>
80
+ <button id="btnByRule" onclick="setView('rule')"
81
+ class="px-3 py-1.5 rounded-md transition-all text-gray-500 hover:text-gray-700">
82
+ By Rule
83
+ </button>
84
+ </div>
85
+ <button onclick="window.__cqRun && window.__cqRun(true)"
86
+ 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">
87
+ <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
88
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
89
+ 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"/>
90
+ </svg>
91
+ Re-run
92
+ </button>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- ── Summary cards ── -->
97
+ <div id="cqSummaryCards" class="grid grid-cols-2 sm:grid-cols-5 gap-3"></div>
98
+
99
+ <!-- ── Sections: ESLint + TSC ── -->
100
+ <div id="cqSections" class="space-y-6"></div>
101
+
102
+ <!-- ── All-clear ── -->
103
+ <div id="cqAllClear" class="hidden bg-gradient-to-br from-green-50 to-emerald-50 border border-green-200 rounded-xl p-10 text-center">
104
+ <div class="flex justify-center mb-4">
105
+ <div class="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
106
+ <svg class="w-9 h-9 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
107
+ <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"/>
108
+ </svg>
109
+ </div>
110
+ </div>
111
+ <p class="text-green-800 font-bold text-lg">No issues found</p>
112
+ <p class="text-green-600 text-sm mt-1">Your codebase passed all checks. Great job!</p>
113
+ </div>
114
+
115
+ </div>
116
+ </div>
117
+
118
+ <style>
119
+ .cq-file-header { cursor: pointer; user-select: none; }
120
+ .cq-file-header:hover { background-color: #f8fafc; }
121
+ .cq-messages { display: none; }
122
+ .cq-messages.open { display: table-row-group; }
123
+ .cq-chevron { transition: transform 0.2s; }
124
+ .cq-chevron.open { transform: rotate(90deg); }
125
+ .rule-badge { font-family: ui-monospace, monospace; }
126
+ .sev-error { background:#fef2f2; color:#dc2626; }
127
+ .sev-warning { background:#fffbeb; color:#d97706; }
128
+ .sev-info { background:#eff6ff; color:#2563eb; }
129
+ </style>
130
+
131
+ <script>
132
+ // ── State machine ─────────────────────────────────────────────────────────
133
+ function cqHideAll() {
134
+ ['cqIdle','cqLoading','cqError','cqResults'].forEach(id => {
135
+ const el = document.getElementById(id);
136
+ if (el) el.classList.add('hidden');
137
+ });
138
+ }
139
+ function showIdle() { cqHideAll(); document.getElementById('cqIdle')?.classList.remove('hidden'); }
140
+ function showLoading() { cqHideAll(); document.getElementById('cqLoading')?.classList.remove('hidden'); }
141
+ function showError(msg){ cqHideAll(); document.getElementById('cqError')?.classList.remove('hidden'); const el = document.getElementById('cqErrorMsg'); if(el) el.textContent = msg; }
142
+ function showResults(d){ cqHideAll(); document.getElementById('cqResults')?.classList.remove('hidden'); renderAll(d); }
143
+
144
+ // ── Progress ──────────────────────────────────────────────────────────────
145
+ let _pt = null, _pi = 0;
146
+ const _steps = [
147
+ { pct:20, label:'Running ESLint…', step:'cqStep1' },
148
+ { pct:55, label:'Running TypeScript check…', step:'cqStep2' },
149
+ { pct:85, label:'Processing results…', step:'cqStep3' },
150
+ ];
151
+ function startProgress() {
152
+ _pi = 0; setPct(0, 'Starting analysis…');
153
+ _pt = setInterval(() => {
154
+ if (_pi >= _steps.length) { clearInterval(_pt); return; }
155
+ const s = _steps[_pi++];
156
+ setPct(s.pct, s.label);
157
+ document.querySelectorAll('#cqLoading .flex.justify-between span').forEach(el => el.classList.remove('text-violet-600','font-semibold'));
158
+ const active = document.getElementById(s.step);
159
+ if (active) active.classList.add('text-violet-600','font-semibold');
160
+ }, 1800);
161
+ window.__cqProgressTimer = _pt;
162
+ }
163
+ function setPct(pct, label) {
164
+ const bar = document.getElementById('cqProgressBar');
165
+ const lbl = document.getElementById('cqLoadLabel');
166
+ if (bar) bar.style.width = pct + '%';
167
+ if (lbl) lbl.textContent = label;
168
+ }
169
+ function finishProgress(cb) {
170
+ clearInterval(_pt);
171
+ setPct(100, 'Done!');
172
+ setTimeout(cb, 400);
173
+ }
174
+
175
+ // ── View toggle ───────────────────────────────────────────────────────────
176
+ let _currentView = 'file';
177
+ let _lastData = null;
178
+ function setView(v) {
179
+ _currentView = v;
180
+ document.getElementById('btnByFile').className = v === 'file'
181
+ ? 'px-3 py-1.5 rounded-md transition-all bg-white shadow text-gray-800'
182
+ : 'px-3 py-1.5 rounded-md transition-all text-gray-500 hover:text-gray-700';
183
+ document.getElementById('btnByRule').className = v === 'rule'
184
+ ? 'px-3 py-1.5 rounded-md transition-all bg-white shadow text-gray-800'
185
+ : 'px-3 py-1.5 rounded-md transition-all text-gray-500 hover:text-gray-700';
186
+ if (_lastData) renderSections(_lastData);
187
+ }
188
+
189
+ // ── Main render ───────────────────────────────────────────────────────────
190
+ function renderAll(data) {
191
+ _lastData = data;
192
+ renderSummaryCards(data);
193
+ renderSections(data);
194
+ renderMeta(data);
195
+
196
+ const eslintIssues = (data.eslint?.totalErrors || 0) + (data.eslint?.totalWarnings || 0);
197
+ const tscIssues = (data.tsc?.totalErrors || 0) + (data.tsc?.totalWarnings || 0);
198
+ document.getElementById('cqAllClear').classList.toggle('hidden', eslintIssues + tscIssues > 0);
199
+ }
200
+
201
+ function renderMeta(data) {
202
+ const meta = document.getElementById('cqScanMeta');
203
+ if (!meta) return;
204
+ const time = new Date(data.scannedAt).toLocaleTimeString();
205
+ const age = Math.round((Date.now() - data.scannedAt) / 1000);
206
+ const ageLabel = age < 60 ? `${age}s ago` : `${Math.round(age/60)}m ago`;
207
+ meta.innerHTML = `Scanned at ${time}` + (data.fromCache
208
+ ? ` <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">
209
+ <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>
210
+ Cached · ${ageLabel}</span>`
211
+ : ` <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">Fresh scan</span>`);
212
+ }
213
+
214
+ function renderSummaryCards(data) {
215
+ const eslint = data.eslint || {};
216
+ const tsc = data.tsc || {};
217
+ const totalErrors = (eslint.totalErrors || 0) + (tsc.totalErrors || 0);
218
+ const totalWarnings = (eslint.totalWarnings || 0) + (tsc.totalWarnings || 0);
219
+ const totalIssues = totalErrors + totalWarnings;
220
+ const totalFixable = eslint.totalFixable || 0;
221
+ const filesAffected = eslint.filesAffected || 0;
222
+
223
+ const card = (label, value, sub, cls) => `
224
+ <div class="rounded-xl border p-4 ${cls}">
225
+ <div class="text-3xl font-extrabold mb-0.5">${value}</div>
226
+ <div class="text-xs font-semibold uppercase tracking-widest">${label}</div>
227
+ ${sub ? `<div class="text-xs opacity-60 mt-0.5">${sub}</div>` : ''}
228
+ </div>`;
229
+
230
+ document.getElementById('cqSummaryCards').innerHTML = [
231
+ card('Total Issues', totalIssues, '', totalIssues > 0 ? 'bg-red-50 border-red-200 text-red-700' : 'bg-gray-50 border-gray-200 text-gray-700'),
232
+ card('Errors', totalErrors, 'must fix', totalErrors > 0 ? 'bg-rose-50 border-rose-200 text-rose-700' : 'bg-gray-50 border-gray-200 text-gray-700'),
233
+ card('Warnings', totalWarnings, 'should fix', totalWarnings > 0 ? 'bg-amber-50 border-amber-200 text-amber-700': 'bg-gray-50 border-gray-200 text-gray-700'),
234
+ card('Auto-fixable', totalFixable, 'eslint --fix', totalFixable > 0 ? 'bg-violet-50 border-violet-200 text-violet-700':'bg-gray-50 border-gray-200 text-gray-700'),
235
+ card('Files Affected', filesAffected, 'with lint issues', filesAffected > 0 ? 'bg-indigo-50 border-indigo-200 text-indigo-700':'bg-gray-50 border-gray-200 text-gray-700'),
236
+ ].join('');
237
+ }
238
+
239
+ function renderSections(data) {
240
+ const container = document.getElementById('cqSections');
241
+ if (!container) return;
242
+ container.innerHTML = '';
243
+ if (data.eslint?.available) {
244
+ container.innerHTML += renderEslintSection(data.eslint);
245
+ } else if (data.eslint) {
246
+ container.innerHTML += renderUnavailableSection('ESLint', data.eslint.error, '10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4');
247
+ }
248
+ if (data.tsc?.available) {
249
+ container.innerHTML += renderTscSection(data.tsc);
250
+ } else if (data.tsc) {
251
+ container.innerHTML += renderUnavailableSection('TypeScript', data.tsc.error, 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4');
252
+ }
253
+ attachToggleListeners();
254
+ }
255
+
256
+ // ── ESLint section ────────────────────────────────────────────────────────
257
+ function renderEslintSection(eslint) {
258
+ const hasIssues = (eslint.totalErrors + eslint.totalWarnings) > 0;
259
+ const body = _currentView === 'rule'
260
+ ? renderByRule(eslint)
261
+ : renderByFile(eslint.results || []);
262
+
263
+ return `
264
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
265
+ <div class="px-5 py-4 border-b border-gray-100 bg-gradient-to-r from-violet-50 to-indigo-50 flex items-center justify-between">
266
+ <div class="flex items-center gap-3">
267
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-violet-500 to-indigo-600 flex items-center justify-center">
268
+ <svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
269
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
270
+ </svg>
271
+ </div>
272
+ <div>
273
+ <h3 class="text-sm font-bold text-gray-800">ESLint</h3>
274
+ <p class="text-xs text-gray-500">${eslint.totalErrors} errors · ${eslint.totalWarnings} warnings · ${eslint.totalFixable} fixable</p>
275
+ </div>
276
+ </div>
277
+ ${eslint.totalFixable > 0 ? `
278
+ <div class="flex items-center gap-1.5 text-xs bg-violet-100 text-violet-700 px-2.5 py-1 rounded-full font-medium">
279
+ <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="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
280
+ ${eslint.totalFixable} auto-fixable — run <code class="font-mono">eslint --fix</code>
281
+ </div>` : ''}
282
+ </div>
283
+ ${hasIssues ? body : '<div class="p-8 text-center text-sm text-green-600 font-medium">✓ No ESLint issues found</div>'}
284
+ </div>`;
285
+ }
286
+
287
+ function renderByFile(results) {
288
+ if (!results.length) return '';
289
+ return `
290
+ <div class="overflow-x-auto">
291
+ <table class="w-full text-sm">
292
+ <thead class="bg-gray-50 text-xs text-gray-500 uppercase tracking-wider">
293
+ <tr>
294
+ <th class="px-4 py-2.5 text-left w-8"></th>
295
+ <th class="px-4 py-2.5 text-left">File</th>
296
+ <th class="px-4 py-2.5 text-center w-20">Errors</th>
297
+ <th class="px-4 py-2.5 text-center w-24">Warnings</th>
298
+ <th class="px-4 py-2.5 text-center w-20">Fixable</th>
299
+ </tr>
300
+ </thead>
301
+ ${results.map((r, i) => renderFileRow(r, i)).join('')}
302
+ </table>
303
+ </div>`;
304
+ }
305
+
306
+ function renderFileRow(r, i) {
307
+ const rowId = `cq-file-${i}`;
308
+ const fixable = r.fixableErrorCount + r.fixableWarningCount;
309
+ return `
310
+ <tbody>
311
+ <tr class="cq-file-header border-t border-gray-100" onclick="toggleFile('${rowId}')">
312
+ <td class="px-4 py-3 text-gray-400">
313
+ <svg id="chev-${rowId}" class="cq-chevron w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
314
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
315
+ </svg>
316
+ </td>
317
+ <td class="px-4 py-3">
318
+ <div class="flex items-center gap-1.5 group/fp">
319
+ <span class="font-mono text-xs text-gray-700 font-semibold">${escHtml(r.filePath)}</span>
320
+ <button onclick="event.stopPropagation(); cqCopy('${escHtml(r.filePath)}', this)"
321
+ class="opacity-0 group-hover/fp:opacity-100 transition-opacity p-0.5 rounded hover:bg-gray-200 text-gray-400 hover:text-gray-600"
322
+ title="Copy file path">
323
+ <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
324
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
325
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
326
+ </svg>
327
+ </button>
328
+ </div>
329
+ </td>
330
+ <td class="px-4 py-3 text-center">
331
+ ${r.errorCount > 0 ? `<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-red-100 text-red-700">${r.errorCount}</span>` : '<span class="text-gray-300 text-xs">—</span>'}
332
+ </td>
333
+ <td class="px-4 py-3 text-center">
334
+ ${r.warningCount > 0 ? `<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-amber-100 text-amber-700">${r.warningCount}</span>` : '<span class="text-gray-300 text-xs">—</span>'}
335
+ </td>
336
+ <td class="px-4 py-3 text-center">
337
+ ${fixable > 0 ? `<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-semibold bg-violet-100 text-violet-700">⚡ ${fixable}</span>` : '<span class="text-gray-300 text-xs">—</span>'}
338
+ </td>
339
+ </tr>
340
+ </tbody>
341
+ <tbody id="${rowId}" class="cq-messages">
342
+ ${r.messages.map(m => renderMessage(m)).join('')}
343
+ <tr class="border-t border-gray-50"><td colspan="5" class="py-1"></td></tr>
344
+ </tbody>`;
345
+ }
346
+
347
+ function renderMessage(m) {
348
+ const sevClass = m.severity === 2 ? 'sev-error' : 'sev-warning';
349
+ const sevLabel = m.severity === 2 ? 'error' : 'warn';
350
+ const ruleLink = m.ruleId
351
+ ? (m.ruleId.includes('/') || m.ruleId.includes('@')
352
+ ? `https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/${m.ruleId.split('/').pop()}.md`
353
+ : `https://eslint.org/docs/rules/${m.ruleId}`)
354
+ : null;
355
+
356
+ return `
357
+ <tr class="bg-gray-50 hover:bg-gray-100 border-t border-gray-100">
358
+ <td class="pl-8 py-2.5" colspan="2">
359
+ <div class="flex items-start gap-2">
360
+ <span class="inline-block text-xs font-bold px-1.5 py-0.5 rounded ${sevClass} shrink-0 mt-0.5">${sevLabel}</span>
361
+ <div>
362
+ <span class="text-sm text-gray-700">${escHtml(m.message)}</span>
363
+ ${m.suggestions?.length ? `<div class="text-xs text-blue-600 mt-0.5">💡 ${escHtml(m.suggestions[0].desc)}</div>` : ''}
364
+ </div>
365
+ </div>
366
+ </td>
367
+ <td class="px-4 py-2.5 text-center font-mono text-xs text-gray-400 whitespace-nowrap">
368
+ ${m.line}:${m.column}
369
+ </td>
370
+ <td class="px-4 py-2.5 text-center">
371
+ ${m.fixable ? '<span class="text-xs text-violet-600 font-medium">⚡ fixable</span>' : ''}
372
+ </td>
373
+ <td class="px-4 py-2.5">
374
+ ${m.ruleId ? `<a href="${ruleLink}" target="_blank" class="rule-badge text-xs px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 hover:bg-indigo-100 hover:underline">${escHtml(m.ruleId)}</a>` : ''}
375
+ </td>
376
+ </tr>`;
377
+ }
378
+
379
+ function renderByRule(eslint) {
380
+ const rules = eslint.topRules || [];
381
+ if (!rules.length) return '';
382
+ return `
383
+ <div class="overflow-x-auto">
384
+ <table class="w-full text-sm">
385
+ <thead class="bg-gray-50 text-xs text-gray-500 uppercase tracking-wider">
386
+ <tr>
387
+ <th class="px-4 py-2.5 text-left">Rule</th>
388
+ <th class="px-4 py-2.5 text-center w-20">Count</th>
389
+ <th class="px-4 py-2.5 text-center w-24">Severity</th>
390
+ <th class="px-4 py-2.5 text-left">Files</th>
391
+ </tr>
392
+ </thead>
393
+ <tbody class="divide-y divide-gray-100">
394
+ ${rules.map(r => {
395
+ const sevClass = r.severity === 2 ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700';
396
+ const sevLabel = r.severity === 2 ? 'error' : 'warning';
397
+ const ruleLink = r.ruleId.includes('/') || r.ruleId.includes('@')
398
+ ? `https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/docs/rules/${r.ruleId.split('/').pop()}.md`
399
+ : `https://eslint.org/docs/rules/${r.ruleId}`;
400
+ return `
401
+ <tr class="hover:bg-gray-50">
402
+ <td class="px-4 py-3">
403
+ <a href="${ruleLink}" target="_blank"
404
+ class="rule-badge text-xs px-2 py-1 rounded bg-indigo-50 text-indigo-700 hover:bg-indigo-100 hover:underline font-semibold">
405
+ ${escHtml(r.ruleId)}
406
+ </a>
407
+ </td>
408
+ <td class="px-4 py-3 text-center">
409
+ <span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-700 font-bold text-sm">${r.count}</span>
410
+ </td>
411
+ <td class="px-4 py-3 text-center">
412
+ <span class="inline-block text-xs font-semibold px-2 py-0.5 rounded-full ${sevClass}">${sevLabel}</span>
413
+ </td>
414
+ <td class="px-4 py-3">
415
+ <div class="flex flex-wrap gap-1">
416
+ ${(r.files || []).slice(0, 4).map(f => `<span class="text-xs bg-gray-100 text-gray-600 px-1.5 py-0.5 rounded font-mono">${escHtml(f.split('/').pop())}</span>`).join('')}
417
+ ${r.files?.length > 4 ? `<span class="text-xs text-gray-400">+${r.files.length - 4} more</span>` : ''}
418
+ </div>
419
+ </td>
420
+ </tr>`;
421
+ }).join('')}
422
+ </tbody>
423
+ </table>
424
+ </div>`;
425
+ }
426
+
427
+ // ── TypeScript section ────────────────────────────────────────────────────
428
+ function renderTscSection(tsc) {
429
+ const errors = (tsc.errors || []).filter(e => e.severity === 'error');
430
+ const warnings = (tsc.errors || []).filter(e => e.severity === 'warning');
431
+ const hasIssues = errors.length + warnings.length > 0;
432
+
433
+ // Group by file
434
+ const byFile = {};
435
+ (tsc.errors || []).forEach(e => {
436
+ if (!byFile[e.filePath]) byFile[e.filePath] = [];
437
+ byFile[e.filePath].push(e);
438
+ });
439
+
440
+ return `
441
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
442
+ <div class="px-5 py-4 border-b border-gray-100 bg-gradient-to-r from-blue-50 to-cyan-50 flex items-center gap-3">
443
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-600 flex items-center justify-center">
444
+ <svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
445
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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"/>
446
+ </svg>
447
+ </div>
448
+ <div>
449
+ <h3 class="text-sm font-bold text-gray-800">TypeScript</h3>
450
+ <p class="text-xs text-gray-500">${errors.length} errors · ${warnings.length} warnings</p>
451
+ </div>
452
+ </div>
453
+ ${hasIssues ? `
454
+ <div class="overflow-x-auto">
455
+ <table class="w-full text-sm">
456
+ <thead class="bg-gray-50 text-xs text-gray-500 uppercase tracking-wider">
457
+ <tr>
458
+ <th class="px-4 py-2.5 text-left">File</th>
459
+ <th class="px-4 py-2.5 text-center w-20">Line</th>
460
+ <th class="px-4 py-2.5 text-center w-24">Severity</th>
461
+ <th class="px-4 py-2.5 text-center w-24">Code</th>
462
+ <th class="px-4 py-2.5 text-left">Message</th>
463
+ </tr>
464
+ </thead>
465
+ <tbody class="divide-y divide-gray-100">
466
+ ${(tsc.errors || []).map(e => `
467
+ <tr class="hover:bg-gray-50">
468
+ <td class="px-4 py-2.5 max-w-xs">
469
+ <div class="flex items-center gap-1.5 group/fp">
470
+ <span class="font-mono text-xs text-gray-600 truncate" title="${escHtml(e.filePath)}">${escHtml(e.filePath)}</span>
471
+ <button onclick="cqCopy('${escHtml(e.filePath)}', this)"
472
+ class="opacity-0 group-hover/fp:opacity-100 transition-opacity shrink-0 p-0.5 rounded hover:bg-gray-200 text-gray-400 hover:text-gray-600"
473
+ title="Copy file path">
474
+ <svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
475
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
476
+ d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
477
+ </svg>
478
+ </button>
479
+ </div>
480
+ </td>
481
+ <td class="px-4 py-2.5 text-center font-mono text-xs text-gray-500">${e.line}:${e.column}</td>
482
+ <td class="px-4 py-2.5 text-center">
483
+ <span class="inline-block text-xs font-semibold px-2 py-0.5 rounded-full ${e.severity === 'error' ? 'bg-red-100 text-red-700' : 'bg-amber-100 text-amber-700'}">
484
+ ${e.severity}
485
+ </span>
486
+ </td>
487
+ <td class="px-4 py-2.5 text-center">
488
+ <a href="https://typescript.tv/errors/#${e.code.toLowerCase()}" target="_blank"
489
+ class="rule-badge text-xs px-2 py-0.5 rounded bg-blue-50 text-blue-600 hover:bg-blue-100 hover:underline font-semibold">
490
+ ${escHtml(e.code)}
491
+ </a>
492
+ </td>
493
+ <td class="px-4 py-2.5 text-gray-700 text-sm">${escHtml(e.message)}</td>
494
+ </tr>`).join('')}
495
+ </tbody>
496
+ </table>
497
+ </div>` : '<div class="p-8 text-center text-sm text-green-600 font-medium">✓ No TypeScript errors found</div>'}
498
+ </div>`;
499
+ }
500
+
501
+ function renderUnavailableSection(name, error, iconPath) {
502
+ return `
503
+ <div class="bg-gray-50 rounded-xl border border-dashed border-gray-300 p-6 flex items-center gap-4">
504
+ <div class="w-10 h-10 rounded-lg bg-gray-200 flex items-center justify-center shrink-0">
505
+ <svg class="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
506
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M${iconPath}"/>
507
+ </svg>
508
+ </div>
509
+ <div>
510
+ <p class="text-sm font-semibold text-gray-500">${name} not available</p>
511
+ <p class="text-xs text-gray-400 mt-0.5">${escHtml(error || 'Tool not found or not configured')}</p>
512
+ </div>
513
+ </div>`;
514
+ }
515
+
516
+ // ── Helpers ───────────────────────────────────────────────────────────────
517
+ function toggleFile(id) {
518
+ const body = document.getElementById(id);
519
+ const chevron = document.getElementById('chev-' + id);
520
+ if (!body) return;
521
+ const open = body.classList.toggle('open');
522
+ if (chevron) chevron.classList.toggle('open', open);
523
+ }
524
+
525
+ function attachToggleListeners() { /* handled by onclick in HTML */ }
526
+
527
+ function escHtml(s) {
528
+ if (s == null) return '';
529
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
530
+ }
531
+
532
+ function cqCopy(text, btn) {
533
+ navigator.clipboard.writeText(text).then(() => {
534
+ const orig = btn.innerHTML;
535
+ btn.innerHTML = `<svg class="w-3.5 h-3.5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
536
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
537
+ </svg>`;
538
+ setTimeout(() => { btn.innerHTML = orig; }, 1500);
539
+ });
540
+ }
541
+
542
+ // ── Fetch & cache ─────────────────────────────────────────────────────────
543
+ if (!window.__cqCache) window.__cqCache = null;
544
+
545
+ if (window.__cqAbort) { window.__cqAbort.abort(); window.__cqAbort = null; }
546
+ if (window.__cqProgressTimer) { clearInterval(window.__cqProgressTimer); window.__cqProgressTimer = null; }
547
+
548
+ async function runCheck(force = false) {
549
+ if (!force && window.__cqCache) { showResults(window.__cqCache); return; }
550
+
551
+ showLoading();
552
+ startProgress();
553
+
554
+ const ctrl = new AbortController();
555
+ window.__cqAbort = ctrl;
556
+
557
+ try {
558
+ const url = force ? '/__profiler/api/code-quality?force=true' : '/__profiler/api/code-quality';
559
+ const res = await fetch(url, { signal: ctrl.signal });
560
+ const data = await res.json();
561
+ window.__cqCache = data;
562
+ window.__cqAbort = null;
563
+ finishProgress(() => showResults(data));
564
+ } catch(e) {
565
+ if (e.name === 'AbortError') return;
566
+ showError(e.message || 'Could not reach /__profiler/api/code-quality');
567
+ }
568
+ }
569
+
570
+ window.__cqRun = (force = false) => runCheck(force);
571
+
572
+ try { runCheck(false); } catch(e) { showError('Failed to start: ' + e.message); }
573
+ </script>
@@ -51,6 +51,17 @@
51
51
  {{{ emptyState }}}
52
52
 
53
53
  <script>
54
+ function filterRows(term) {
55
+ const body = document.getElementById('tableBody');
56
+ if (!body) return;
57
+ const lower = (term || '').toLowerCase();
58
+ Array.from(body.rows).forEach(row => {
59
+ const urlCell = row.cells[1];
60
+ const text = urlCell ? urlCell.textContent.toLowerCase() : '';
61
+ row.style.display = (!lower || text.includes(lower)) ? '' : 'none';
62
+ });
63
+ }
64
+
54
65
  function toggleSort() {
55
66
  const table = document.getElementById('tableBody');
56
67
  const rows = Array.from(table.rows);
@@ -67,6 +78,19 @@
67
78
  desc.classList.remove('text-indigo-600', 'opacity-100'); desc.classList.add('opacity-50');
68
79
  }
69
80
  }
81
+
82
+ // Wire search input
83
+ const searchInput = document.getElementById('search');
84
+ if (searchInput) {
85
+ searchInput.addEventListener('input', () => filterRows(searchInput.value));
86
+
87
+ // Pre-fill from ?search= query param (used by links from summary page)
88
+ const preSearch = new URLSearchParams(window.location.search).get('search');
89
+ if (preSearch) {
90
+ searchInput.value = preSearch;
91
+ filterRows(preSearch);
92
+ }
93
+ }
70
94
  </script>
71
95
 
72
96
 
package/views/detail.html CHANGED
@@ -57,6 +57,8 @@
57
57
  </div>
58
58
 
59
59
  {{{ cacheSection }}}
60
+
61
+ {{{ httpCallsSection }}}
60
62
  </div>
61
63
 
62
64
  <!-- Sidebar: Meta Info -->