performance-optimizer 2.5.0__py3-none-any.whl

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,1502 @@
1
+ import json
2
+ from datetime import datetime
3
+ from typing import List, Dict, Any
4
+ from core.issue import Issue, Severity, Layer
5
+
6
+ def score_color(s: int) -> str:
7
+ if s < 45: return '#f43f5e'
8
+ if s < 65: return '#f59e0b'
9
+ if s < 80: return '#eab308'
10
+ return '#10b981'
11
+
12
+ def esc(s: Any) -> str:
13
+ return str(s).replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
14
+
15
+ class HtmlReporter:
16
+ def __init__(self, issues: List[Issue], scores: Dict[str, Any], meta: Dict[str, Any]):
17
+ self.issues = sorted(issues, key=lambda i: (i.severity_order, -i.priority_score))
18
+ self.scores = scores
19
+ self.meta = meta
20
+
21
+ def render(self, out_path: str):
22
+ html = self._build()
23
+ with open(out_path, 'w', encoding='utf-8') as fh:
24
+ fh.write(html)
25
+
26
+ def _build(self) -> str:
27
+ s = self.scores
28
+ total = s.get('total_issues', 0)
29
+ by_sev = s.get('by_severity', {})
30
+ by_layer = s.get('by_layer', {})
31
+ overall = s.get('overall', 100)
32
+ top3 = s.get('top3', [])
33
+ worst_files = s.get('worst_files', [])
34
+ metrics = s.get('metrics', {})
35
+
36
+ scan_time = self.meta.get('scan_time', datetime.now().strftime('%d %b %Y, %I:%M %p'))
37
+ files_scanned = self.meta.get('files_scanned', 0)
38
+ duration = self.meta.get('duration', '0.01')
39
+ project = self.meta.get('project', 'Project')
40
+ tech = self.meta.get('tech', 'Angular • React • Node.js • SQL • AWS')
41
+
42
+ # SVG Circle circumference: 2 * pi * 70 = ~440
43
+ ring_offset = round(440 * (1 - overall / 100))
44
+ s_color = score_color(overall)
45
+
46
+ status_text = "Critical Latency Risks" if overall < 50 else "Optimization Required" if overall < 70 else "Fair Performance" if overall < 85 else "Production Ready"
47
+ status_badge_class = "sev-critical" if overall < 50 else "sev-high" if overall < 70 else "sev-medium" if overall < 85 else "sev-ready"
48
+
49
+ table_rows_html = []
50
+ sim_checklist_html = []
51
+
52
+ for idx, issue in enumerate(self.issues, 1):
53
+ row_id = f"diag_row_{idx}"
54
+ sev = issue.severity.value
55
+ layer = issue.layer.value
56
+ sc = score_color(round(issue.priority_score * 10))
57
+ line_display = f":{issue.line_number}" if issue.line_number else ""
58
+ clean_file = issue.file.replace('\\', '/')
59
+
60
+ pt_weight = 18 if issue.severity == Severity.CRITICAL else 10 if issue.severity == Severity.HIGH else 4
61
+ search_str = f"{issue.id} {issue.title} {clean_file} {issue.category} {issue.description}".lower()
62
+
63
+ main_row = f'''
64
+ <tr class="main-row" onclick="toggleRow('{row_id}')" data-sev="{esc(sev)}" data-layer="{esc(layer)}" data-search="{esc(search_str)}">
65
+ <td><span class="rule-token">{esc(issue.id)}</span></td>
66
+ <td>
67
+ <div class="row-issue-title">{esc(issue.title)}</div>
68
+ <div class="row-issue-file">{esc(clean_file)}{line_display}</div>
69
+ </td>
70
+ <td><span class="badge-tag sev-{esc(sev)}">{esc(sev.upper())}</span></td>
71
+ <td><span class="badge-tag layer-{esc(layer)}">{esc(layer.upper())}</span></td>
72
+ <td><span style="font-weight:700;color:{score_color(issue.impact * 10)};">{issue.impact}/10</span> <span style="font-size:11px;color:var(--text-dim);">(Effort: {issue.effort})</span></td>
73
+ <td><span class="p-rank" style="color:{sc};">{'P1' if issue.priority_score >= 2.5 else 'P2' if issue.priority_score >= 1.5 else 'P3'}</span></td>
74
+ <td><button class="btn btn-ghost" style="padding:3px 9px;font-size:11px;">Diff</button></td>
75
+ </tr>
76
+ <tr class="expanded-row" id="{row_id}">
77
+ <td colspan="7">
78
+ <div class="diff-wrapper">
79
+ <div class="diff-desc">
80
+ <strong>Architectural Risk:</strong> {esc(issue.description)}
81
+ </div>
82
+ <div class="diff-split">
83
+ <div class="diff-pane">
84
+ <div class="diff-pane-head bad">
85
+ <span class="head-tag">Offending Code</span>
86
+ <span class="head-file">{esc(clean_file)}{line_display}</span>
87
+ </div>
88
+ <pre class="diff-code">{esc(issue.code_before)}</pre>
89
+ </div>
90
+ <div class="diff-pane">
91
+ <div class="diff-pane-head good">
92
+ <span class="head-tag">Remediated Code</span>
93
+ <button class="copy-chip" onclick="copySnippet(this)">Copy Patch</button>
94
+ </div>
95
+ <pre class="diff-code">{esc(issue.code_after)}</pre>
96
+ </div>
97
+ </div>
98
+ <div class="diff-footer">
99
+ <span class="diff-gain">
100
+ <span class="icon" style="width:13px;height:13px;"><svg viewBox="0 0 24 24" width="13" height="13"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg></span>
101
+ <span>{esc(issue.perf_gain)}</span>
102
+ </span>
103
+ {f'<a href="{esc(issue.doc_url)}" target="_blank" class="diff-link">Documentation →</a>' if issue.doc_url else ''}
104
+ </div>
105
+ </div>
106
+ </td>
107
+ </tr>'''
108
+ table_rows_html.append(main_row)
109
+
110
+ if idx <= 10:
111
+ sim_item = f'''
112
+ <label class="sim-item">
113
+ <input type="checkbox" onchange="recalculateSimScore()" data-points="{pt_weight}">
114
+ <div class="sim-body">
115
+ <div class="sim-title">
116
+ <span>[{esc(issue.id)}] {esc(issue.title)}</span>
117
+ <span class="badge-tag sev-{esc(sev)}">+{pt_weight} pts</span>
118
+ </div>
119
+ <div class="sim-desc">{esc(issue.perf_gain)} • {esc(clean_file)}</div>
120
+ </div>
121
+ </label>'''
122
+ sim_checklist_html.append(sim_item)
123
+
124
+ table_rows = '\n'.join(table_rows_html) if table_rows_html else '<tr><td colspan="7" style="text-align:center;padding:32px;color:var(--text-dim);">No performance defects detected. Codebase is clean.</td></tr>'
125
+ sim_checklist = '\n'.join(sim_checklist_html) if sim_checklist_html else '<p style="color:var(--text-dim);">No defects available for simulation.</p>'
126
+
127
+ exec_cards = ''
128
+ for t in top3:
129
+ exec_cards += f'''
130
+ <div class="exec-card">
131
+ <span class="badge-tag sev-{esc(t["severity"])}" style="margin-bottom:8px;">{esc(t["severity"].upper())} • {esc(t["layer"].upper())}</span>
132
+ <h5>{esc(t["title"])}</h5>
133
+ <p>{esc(t["description"][:130])}...</p>
134
+ <div class="gain-text">
135
+ <span class="icon" style="width:13px;height:13px;"><svg viewBox="0 0 24 24" width="13" height="13"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/></svg></span>
136
+ <span>{esc(t["perf_gain"])}</span>
137
+ </div>
138
+ </div>'''
139
+ if not exec_cards:
140
+ exec_cards = '<div class="exec-card"><h5>No Critical Defects</h5><p>No critical performance defects detected in the analyzed scope.</p></div>'
141
+
142
+ file_cards = ''
143
+ for fi in worst_files:
144
+ fc = score_color(fi['score'])
145
+ clean_name = fi['file'].replace('\\', '/').split('/')[-1]
146
+ clean_dir = '/'.join(fi['file'].replace('\\', '/').split('/')[:-1])
147
+ file_cards += f'''
148
+ <div class="hotspot-item">
149
+ <div class="hotspot-info">
150
+ <div class="hotspot-name">{esc(clean_name)}</div>
151
+ <div class="hotspot-dir">{esc(clean_dir or '.')}</div>
152
+ <div class="hotspot-tags">
153
+ {f'<span class="badge-tag sev-critical">{fi["critical"]} Critical</span>' if fi["critical"] else ''}
154
+ {f'<span class="badge-tag sev-high">{fi["high"]} High</span>' if fi["high"] else ''}
155
+ <span class="badge-tag sev-neutral">{fi["issues"]} Total</span>
156
+ </div>
157
+ </div>
158
+ <div style="text-align:right;">
159
+ <div class="hotspot-score" style="color:{fc};">{fi["score"]}<span style="font-size:12px;color:var(--text-dim);">/100</span></div>
160
+ <span style="font-size:11px;color:var(--text-dim);font-weight:600;">File Rating</span>
161
+ </div>
162
+ </div>'''
163
+ if not file_cards:
164
+ file_cards = '<p style="color:var(--text-dim);padding:20px;">No hotspot files found.</p>'
165
+
166
+ sev_chart_data = json.dumps([by_sev.get('critical', 0), by_sev.get('high', 0), by_sev.get('medium', 0), by_sev.get('low', 0)])
167
+ layer_chart_data = json.dumps([by_layer.get('frontend', 0), by_layer.get('backend', 0), by_layer.get('database', 0), by_layer.get('infra', 0)])
168
+
169
+ return f'''<!DOCTYPE html>
170
+ <html lang="en">
171
+ <head>
172
+ <meta charset="UTF-8">
173
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
174
+ <title>Performance Intelligence Report — {esc(project)}</title>
175
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
176
+ <link rel="preconnect" href="https://fonts.googleapis.com">
177
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
178
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
179
+ <style>
180
+ :root {{
181
+ --bg: #07090e;
182
+ --bg-surface: #0c0f17;
183
+ --card: #111622;
184
+ --card-hover: #161c2b;
185
+ --card-active: #1c2438;
186
+ --border: rgba(255, 255, 255, 0.08);
187
+ --border-highlight: rgba(99, 102, 241, 0.4);
188
+ --primary: #6366f1;
189
+ --primary-light: #818cf8;
190
+ --primary-glow: rgba(99, 102, 241, 0.22);
191
+ --cyan: #06b6d4;
192
+ --emerald: #10b981;
193
+ --emerald-bg: rgba(16, 185, 129, 0.08);
194
+ --crimson: #f43f5e;
195
+ --crimson-bg: rgba(244, 63, 94, 0.08);
196
+ --amber: #f59e0b;
197
+ --amber-bg: rgba(245, 158, 11, 0.08);
198
+ --text: #f8fafc;
199
+ --text-muted: #94a3b8;
200
+ --text-dim: #64748b;
201
+ --code-bg: #07090e;
202
+ --font: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
203
+ --font-mono: 'JetBrains Mono', monospace;
204
+ --radius: 12px;
205
+ --radius-lg: 16px;
206
+ --ease: cubic-bezier(0.16, 1, 0.3, 1);
207
+ }}
208
+
209
+ [data-theme="light"] {{
210
+ --bg: #f8fafc;
211
+ --bg-surface: #ffffff;
212
+ --card: #ffffff;
213
+ --card-hover: #f1f5f9;
214
+ --card-active: #e2e8f0;
215
+ --border: rgba(0, 0, 0, 0.08);
216
+ --border-highlight: rgba(79, 70, 229, 0.4);
217
+ --primary: #4f46e5;
218
+ --primary-light: #6366f1;
219
+ --primary-glow: rgba(79, 70, 229, 0.12);
220
+ --cyan: #0891b2;
221
+ --emerald: #059669;
222
+ --emerald-bg: rgba(5, 150, 105, 0.08);
223
+ --crimson: #e11d48;
224
+ --crimson-bg: rgba(225, 29, 72, 0.08);
225
+ --amber: #d97706;
226
+ --amber-bg: rgba(217, 119, 6, 0.08);
227
+ --text: #0f172a;
228
+ --text-muted: #475569;
229
+ --text-dim: #64748b;
230
+ --code-bg: #0f172a;
231
+ }}
232
+
233
+ * {{ margin:0; padding:0; box-sizing:border-box; }}
234
+ body {{
235
+ background: var(--bg);
236
+ color: var(--text);
237
+ font-family: var(--font);
238
+ line-height: 1.6;
239
+ min-height: 100vh;
240
+ -webkit-font-smoothing: antialiased;
241
+ transition: background 0.35s var(--ease), color 0.35s var(--ease);
242
+ }}
243
+
244
+ /* GLOBAL SVG SAFETY RESET */
245
+ svg {{
246
+ max-width: 100%;
247
+ max-height: 100%;
248
+ box-sizing: border-box;
249
+ display: block;
250
+ }}
251
+
252
+ .icon {{
253
+ width: 16px;
254
+ height: 16px;
255
+ min-width: 16px;
256
+ min-height: 16px;
257
+ display: inline-flex;
258
+ align-items: center;
259
+ justify-content: center;
260
+ flex-shrink: 0;
261
+ line-height: 1;
262
+ overflow: hidden;
263
+ }}
264
+ .icon svg {{
265
+ width: 16px;
266
+ height: 16px;
267
+ max-width: 16px;
268
+ max-height: 16px;
269
+ stroke: currentColor;
270
+ fill: none;
271
+ stroke-width: 1.8;
272
+ stroke-linecap: round;
273
+ stroke-linejoin: round;
274
+ flex-shrink: 0;
275
+ }}
276
+
277
+ .header {{
278
+ background: var(--bg-surface);
279
+ border-bottom: 1px solid var(--border);
280
+ position: sticky;
281
+ top: 0;
282
+ z-index: 100;
283
+ backdrop-filter: blur(20px);
284
+ }}
285
+ .header-inner {{
286
+ max-width: 1400px;
287
+ margin: 0 auto;
288
+ display: flex;
289
+ align-items: center;
290
+ justify-content: space-between;
291
+ padding: 16px 32px;
292
+ }}
293
+ .brand-group {{
294
+ display: flex;
295
+ align-items: center;
296
+ gap: 14px;
297
+ }}
298
+ .brand-glyph {{
299
+ width: 38px;
300
+ height: 38px;
301
+ min-width: 38px;
302
+ min-height: 38px;
303
+ border-radius: 10px;
304
+ background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
305
+ display: flex;
306
+ align-items: center;
307
+ justify-content: center;
308
+ color: white;
309
+ box-shadow: 0 0 20px var(--primary-glow);
310
+ overflow: hidden;
311
+ flex-shrink: 0;
312
+ }}
313
+ .brand-glyph svg {{
314
+ width: 20px !important;
315
+ height: 20px !important;
316
+ max-width: 20px !important;
317
+ max-height: 20px !important;
318
+ stroke: currentColor;
319
+ fill: none;
320
+ stroke-width: 2;
321
+ }}
322
+ .brand-text h1 {{
323
+ font-size: 17px;
324
+ font-weight: 800;
325
+ letter-spacing: -0.4px;
326
+ display: flex;
327
+ align-items: center;
328
+ gap: 8px;
329
+ }}
330
+ .brand-text p {{
331
+ font-size: 12px;
332
+ color: var(--text-muted);
333
+ }}
334
+ .header-actions {{
335
+ display: flex;
336
+ align-items: center;
337
+ gap: 12px;
338
+ }}
339
+ .btn {{
340
+ display: inline-flex;
341
+ align-items: center;
342
+ gap: 8px;
343
+ padding: 8px 16px;
344
+ border-radius: 8px;
345
+ font-size: 13px;
346
+ font-weight: 600;
347
+ cursor: pointer;
348
+ border: 1px solid var(--border);
349
+ background: var(--card);
350
+ color: var(--text);
351
+ transition: all 0.2s var(--ease);
352
+ text-decoration: none;
353
+ }}
354
+ .btn:hover {{
355
+ background: var(--card-hover);
356
+ border-color: rgba(255,255,255,0.18);
357
+ }}
358
+ .btn-primary {{
359
+ background: linear-gradient(135deg, #4f46e5, #6366f1);
360
+ border-color: var(--primary);
361
+ color: white;
362
+ box-shadow: 0 4px 16px var(--primary-glow);
363
+ }}
364
+ .btn-primary:hover {{
365
+ background: linear-gradient(135deg, #4338ca, #4f46e5);
366
+ }}
367
+
368
+ .nav-tabs {{
369
+ display: flex;
370
+ gap: 6px;
371
+ border-bottom: 1px solid var(--border);
372
+ background: var(--bg-surface);
373
+ padding: 0 32px;
374
+ max-width: 1400px;
375
+ margin: 0 auto;
376
+ }}
377
+ .tab-btn {{
378
+ padding: 14px 20px;
379
+ background: transparent;
380
+ border: none;
381
+ border-bottom: 2px solid transparent;
382
+ color: var(--text-muted);
383
+ font-size: 13.5px;
384
+ font-weight: 600;
385
+ cursor: pointer;
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 8px;
389
+ transition: all 0.2s var(--ease);
390
+ }}
391
+ .tab-btn:hover {{ color: var(--text); }}
392
+ .tab-btn.active {{
393
+ color: var(--primary-light);
394
+ border-bottom-color: var(--primary);
395
+ }}
396
+ .tab-count {{
397
+ background: var(--card-hover);
398
+ font-size: 11px;
399
+ padding: 1px 7px;
400
+ border-radius: 10px;
401
+ color: var(--text-muted);
402
+ font-family: var(--font-mono);
403
+ }}
404
+
405
+ .container {{
406
+ max-width: 1400px;
407
+ margin: 0 auto;
408
+ padding: 32px 32px 64px 32px;
409
+ }}
410
+ .tab-panel {{ display: none; }}
411
+ .tab-panel.active {{ display: block; }}
412
+
413
+ /* HERO SCORE SECTION */
414
+ .hero-grid {{
415
+ display: grid;
416
+ grid-template-columns: 320px 1fr;
417
+ gap: 24px;
418
+ margin-bottom: 32px;
419
+ }}
420
+ .score-card {{
421
+ background: var(--card);
422
+ border: 1px solid var(--border);
423
+ border-radius: var(--radius-lg);
424
+ padding: 30px;
425
+ display: flex;
426
+ flex-direction: column;
427
+ align-items: center;
428
+ justify-content: center;
429
+ position: relative;
430
+ overflow: hidden;
431
+ }}
432
+ .score-card::before {{
433
+ content: '';
434
+ position: absolute;
435
+ top: -60px;
436
+ width: 220px;
437
+ height: 220px;
438
+ background: radial-gradient(circle, var(--primary-glow) 0%, transparent 70%);
439
+ pointer-events: none;
440
+ }}
441
+ .score-ring-wrap {{
442
+ position: relative;
443
+ width: 170px;
444
+ height: 170px;
445
+ margin: 16px 0;
446
+ }}
447
+ .score-ring-wrap svg {{ transform: rotate(-90deg); }}
448
+ .score-ring-bg {{
449
+ fill: none;
450
+ stroke: var(--border);
451
+ stroke-width: 12;
452
+ }}
453
+ .score-ring-progress {{
454
+ fill: none;
455
+ stroke-width: 12;
456
+ stroke-linecap: round;
457
+ stroke-dasharray: 440;
458
+ transition: stroke-dashoffset 1.2s var(--ease), stroke 0.5s;
459
+ }}
460
+ .score-ring-val {{
461
+ position: absolute;
462
+ inset: 0;
463
+ display: flex;
464
+ flex-direction: column;
465
+ align-items: center;
466
+ justify-content: center;
467
+ }}
468
+ .score-ring-num {{
469
+ font-size: 50px;
470
+ font-weight: 800;
471
+ letter-spacing: -1.5px;
472
+ line-height: 1;
473
+ font-variant-numeric: tabular-nums;
474
+ }}
475
+ .score-status-badge {{
476
+ padding: 4px 12px;
477
+ border-radius: 100px;
478
+ font-size: 11px;
479
+ font-weight: 700;
480
+ letter-spacing: 0.5px;
481
+ text-transform: uppercase;
482
+ margin-top: 8px;
483
+ }}
484
+
485
+ /* METRICS ROW */
486
+ .metrics-grid {{
487
+ display: grid;
488
+ grid-template-columns: repeat(4, 1fr);
489
+ gap: 16px;
490
+ margin-bottom: 20px;
491
+ }}
492
+ .metric-card {{
493
+ background: var(--card);
494
+ border: 1px solid var(--border);
495
+ border-radius: var(--radius);
496
+ padding: 20px;
497
+ }}
498
+ .metric-icon-box {{
499
+ width: 34px;
500
+ height: 34px;
501
+ border-radius: 8px;
502
+ background: rgba(255,255,255,0.03);
503
+ border: 1px solid var(--border);
504
+ display: flex;
505
+ align-items: center;
506
+ justify-content: center;
507
+ margin-bottom: 12px;
508
+ color: var(--primary-light);
509
+ }}
510
+ .metric-title {{
511
+ font-size: 12.5px;
512
+ color: var(--text-muted);
513
+ font-weight: 500;
514
+ }}
515
+ .metric-val {{
516
+ font-size: 26px;
517
+ font-weight: 800;
518
+ margin: 4px 0;
519
+ letter-spacing: -0.5px;
520
+ }}
521
+ .metric-sub {{
522
+ font-size: 11.5px;
523
+ font-weight: 600;
524
+ color: var(--emerald);
525
+ }}
526
+
527
+ .exec-banner {{
528
+ background: var(--card);
529
+ border: 1px solid var(--border);
530
+ border-radius: var(--radius-lg);
531
+ padding: 22px 26px;
532
+ }}
533
+ .exec-banner-title {{
534
+ font-size: 13.5px;
535
+ font-weight: 700;
536
+ text-transform: uppercase;
537
+ letter-spacing: 0.8px;
538
+ color: var(--primary-light);
539
+ display: flex;
540
+ align-items: center;
541
+ justify-content: space-between;
542
+ margin-bottom: 14px;
543
+ }}
544
+ .exec-cards-row {{
545
+ display: grid;
546
+ grid-template-columns: repeat(3, 1fr);
547
+ gap: 16px;
548
+ }}
549
+ .exec-card {{
550
+ background: var(--bg-surface);
551
+ border: 1px solid var(--border);
552
+ border-radius: 10px;
553
+ padding: 16px;
554
+ }}
555
+ .exec-card h5 {{
556
+ font-size: 13.5px;
557
+ font-weight: 700;
558
+ margin-bottom: 4px;
559
+ }}
560
+ .exec-card p {{
561
+ font-size: 12px;
562
+ color: var(--text-muted);
563
+ line-height: 1.5;
564
+ }}
565
+ .gain-text {{
566
+ margin-top: 10px;
567
+ font-size: 11.5px;
568
+ color: var(--emerald);
569
+ font-weight: 600;
570
+ display: flex;
571
+ align-items: center;
572
+ gap: 5px;
573
+ }}
574
+
575
+ .section-header {{
576
+ display: flex;
577
+ align-items: center;
578
+ justify-content: space-between;
579
+ margin: 32px 0 18px 0;
580
+ }}
581
+ .section-header h3 {{
582
+ font-size: 17px;
583
+ font-weight: 800;
584
+ letter-spacing: -0.4px;
585
+ }}
586
+
587
+ .layer-grid {{
588
+ display: grid;
589
+ grid-template-columns: repeat(4, 1fr);
590
+ gap: 18px;
591
+ margin-bottom: 28px;
592
+ }}
593
+ .layer-card {{
594
+ background: var(--card);
595
+ border: 1px solid var(--border);
596
+ border-radius: var(--radius);
597
+ padding: 22px;
598
+ transition: transform 0.2s var(--ease), border-color 0.2s var(--ease);
599
+ }}
600
+ .layer-card:hover {{
601
+ transform: translateY(-2px);
602
+ border-color: var(--border-highlight);
603
+ }}
604
+ .layer-top {{
605
+ display: flex;
606
+ align-items: center;
607
+ justify-content: space-between;
608
+ margin-bottom: 12px;
609
+ }}
610
+ .layer-title {{
611
+ font-size: 13.5px;
612
+ font-weight: 600;
613
+ color: var(--text-muted);
614
+ }}
615
+ .layer-score {{
616
+ font-size: 28px;
617
+ font-weight: 800;
618
+ letter-spacing: -0.6px;
619
+ font-variant-numeric: tabular-nums;
620
+ }}
621
+ .track-bar {{
622
+ height: 6px;
623
+ background: var(--border);
624
+ border-radius: 3px;
625
+ overflow: hidden;
626
+ margin: 12px 0;
627
+ }}
628
+ .fill-bar {{
629
+ height: 100%;
630
+ border-radius: 3px;
631
+ transition: width 0.8s var(--ease);
632
+ }}
633
+
634
+ .charts-row {{
635
+ display: grid;
636
+ grid-template-columns: 1fr 1fr 1.2fr;
637
+ gap: 20px;
638
+ margin-bottom: 36px;
639
+ }}
640
+ .chart-box {{
641
+ background: var(--card);
642
+ border: 1px solid var(--border);
643
+ border-radius: var(--radius);
644
+ padding: 22px;
645
+ }}
646
+ .chart-box h4 {{
647
+ font-size: 12px;
648
+ font-weight: 700;
649
+ text-transform: uppercase;
650
+ letter-spacing: 0.6px;
651
+ color: var(--text-dim);
652
+ margin-bottom: 16px;
653
+ }}
654
+ .chart-canvas-wrap {{
655
+ position: relative;
656
+ height: 190px;
657
+ }}
658
+
659
+ /* FILTER BAR */
660
+ .filter-bar {{
661
+ background: var(--card);
662
+ border: 1px solid var(--border);
663
+ border-radius: var(--radius);
664
+ padding: 12px 16px;
665
+ display: flex;
666
+ align-items: center;
667
+ justify-content: space-between;
668
+ gap: 16px;
669
+ margin-bottom: 16px;
670
+ flex-wrap: wrap;
671
+ }}
672
+ .search-wrap {{
673
+ display: flex;
674
+ align-items: center;
675
+ gap: 8px;
676
+ background: var(--bg);
677
+ border: 1px solid var(--border);
678
+ padding: 8px 14px;
679
+ border-radius: 8px;
680
+ flex: 1;
681
+ min-width: 240px;
682
+ }}
683
+ .search-wrap input {{
684
+ background: transparent;
685
+ border: none;
686
+ outline: none;
687
+ color: var(--text);
688
+ font-size: 13px;
689
+ width: 100%;
690
+ font-family: var(--font);
691
+ }}
692
+ .pill-group {{
693
+ display: flex;
694
+ gap: 6px;
695
+ flex-wrap: wrap;
696
+ }}
697
+ .pill-btn {{
698
+ padding: 6px 12px;
699
+ border-radius: 7px;
700
+ font-size: 12px;
701
+ font-weight: 600;
702
+ background: var(--bg-surface);
703
+ border: 1px solid var(--border);
704
+ color: var(--text-muted);
705
+ cursor: pointer;
706
+ transition: all 0.2s;
707
+ }}
708
+ .pill-btn:hover, .pill-btn.active {{
709
+ background: var(--card-active);
710
+ border-color: var(--border-highlight);
711
+ color: var(--text);
712
+ }}
713
+
714
+ /* ISSUES TABLE */
715
+ .table-shell {{
716
+ background: var(--card);
717
+ border: 1px solid var(--border);
718
+ border-radius: var(--radius-lg);
719
+ overflow: hidden;
720
+ margin-bottom: 36px;
721
+ }}
722
+ table.diag-table {{
723
+ width: 100%;
724
+ border-collapse: collapse;
725
+ font-size: 13.5px;
726
+ }}
727
+ table.diag-table thead {{
728
+ background: var(--bg-surface);
729
+ }}
730
+ table.diag-table th {{
731
+ padding: 14px 20px;
732
+ text-align: left;
733
+ font-size: 11.5px;
734
+ font-weight: 700;
735
+ text-transform: uppercase;
736
+ letter-spacing: 0.6px;
737
+ color: var(--text-dim);
738
+ border-bottom: 1px solid var(--border);
739
+ }}
740
+ table.diag-table td {{
741
+ padding: 16px 20px;
742
+ border-bottom: 1px solid var(--border);
743
+ vertical-align: middle;
744
+ }}
745
+ table.diag-table tr.main-row {{
746
+ cursor: pointer;
747
+ transition: background 0.15s;
748
+ }}
749
+ table.diag-table tr.main-row:hover {{
750
+ background: var(--card-hover);
751
+ }}
752
+ table.diag-table tr.expanded-row {{
753
+ display: none;
754
+ background: var(--bg-surface);
755
+ }}
756
+ table.diag-table tr.expanded-row.show {{
757
+ display: table-row;
758
+ }}
759
+
760
+ .rule-token {{
761
+ font-family: var(--font-mono);
762
+ font-size: 12px;
763
+ font-weight: 700;
764
+ color: var(--primary-light);
765
+ }}
766
+ .row-issue-title {{
767
+ font-weight: 700;
768
+ color: var(--text);
769
+ }}
770
+ .row-issue-file {{
771
+ font-size: 11.5px;
772
+ color: var(--text-dim);
773
+ font-family: var(--font-mono);
774
+ margin-top: 2px;
775
+ }}
776
+
777
+ .badge-tag {{
778
+ display: inline-flex;
779
+ align-items: center;
780
+ padding: 3px 8px;
781
+ border-radius: 5px;
782
+ font-size: 10px;
783
+ font-weight: 700;
784
+ letter-spacing: 0.4px;
785
+ text-transform: uppercase;
786
+ }}
787
+ .sev-critical {{ background: var(--crimson-bg); color: var(--crimson); border: 1px solid rgba(244,63,94,0.3); }}
788
+ .sev-high {{ background: var(--amber-bg); color: var(--amber); border: 1px solid rgba(245,158,11,0.3); }}
789
+ .sev-medium {{ background: rgba(99,102,241,0.1); color: var(--primary-light); border: 1px solid rgba(99,102,241,0.25); }}
790
+ .sev-ready {{ background: var(--emerald-bg); color: var(--emerald); border: 1px solid rgba(16,185,129,0.3); }}
791
+ .sev-neutral {{ background: rgba(255,255,255,0.05); color: var(--text-muted); border: 1px solid var(--border); }}
792
+
793
+ .layer-frontend {{ background: rgba(99,102,241,0.1); color: #818cf8; border: 1px solid rgba(99,102,241,0.3); }}
794
+ .layer-backend {{ background: rgba(16,185,129,0.1); color: #34d399; border: 1px solid rgba(16,185,129,0.3); }}
795
+ .layer-database {{ background: rgba(245,158,11,0.1); color: #fbbf24; border: 1px solid rgba(245,158,11,0.3); }}
796
+ .layer-infra {{ background: rgba(6,182,212,0.1); color: #22d3ee; border: 1px solid rgba(6,182,212,0.3); }}
797
+
798
+ .diff-wrapper {{ padding: 22px; }}
799
+ .diff-desc {{
800
+ font-size: 13.5px;
801
+ color: var(--text-muted);
802
+ margin-bottom: 14px;
803
+ line-height: 1.55;
804
+ }}
805
+ .diff-split {{
806
+ display: grid;
807
+ grid-template-columns: 1fr 1fr;
808
+ gap: 16px;
809
+ }}
810
+ .diff-pane {{
811
+ background: var(--code-bg);
812
+ border: 1px solid var(--border);
813
+ border-radius: 10px;
814
+ overflow: hidden;
815
+ }}
816
+ .diff-pane-head {{
817
+ padding: 8px 14px;
818
+ display: flex;
819
+ align-items: center;
820
+ justify-content: space-between;
821
+ font-size: 11.5px;
822
+ font-weight: 700;
823
+ border-bottom: 1px solid var(--border);
824
+ background: rgba(255, 255, 255, 0.02);
825
+ }}
826
+ .diff-pane-head.bad {{ color: var(--crimson); }}
827
+ .diff-pane-head.good {{ color: var(--emerald); }}
828
+ pre.diff-code {{
829
+ padding: 14px;
830
+ font-family: var(--font-mono);
831
+ font-size: 12px;
832
+ line-height: 1.65;
833
+ color: #cbd5e1;
834
+ white-space: pre-wrap;
835
+ word-break: break-word;
836
+ }}
837
+ .diff-footer {{
838
+ margin-top: 12px;
839
+ display: flex;
840
+ align-items: center;
841
+ justify-content: space-between;
842
+ flex-wrap: wrap;
843
+ gap: 8px;
844
+ }}
845
+ .diff-gain {{
846
+ font-size: 12px;
847
+ color: var(--emerald);
848
+ font-weight: 600;
849
+ display: flex;
850
+ align-items: center;
851
+ gap: 5px;
852
+ }}
853
+ .diff-link {{
854
+ font-size: 11.5px;
855
+ color: var(--primary-light);
856
+ text-decoration: none;
857
+ font-weight: 600;
858
+ }}
859
+ .diff-link:hover {{ text-decoration: underline; }}
860
+ .copy-chip {{
861
+ padding: 3px 8px;
862
+ font-size: 10.5px;
863
+ border-radius: 4px;
864
+ border: 1px solid var(--border);
865
+ background: var(--card);
866
+ color: var(--text-muted);
867
+ cursor: pointer;
868
+ }}
869
+ .copy-chip:hover {{ color: var(--text); border-color: var(--primary); }}
870
+
871
+ /* SIMULATOR & HOTSPOTS */
872
+ .panel-box {{
873
+ background: var(--card);
874
+ border: 1px solid var(--border);
875
+ border-radius: var(--radius-lg);
876
+ padding: 26px;
877
+ margin-bottom: 32px;
878
+ }}
879
+ .sim-head {{
880
+ display: flex;
881
+ align-items: center;
882
+ justify-content: space-between;
883
+ margin-bottom: 22px;
884
+ border-bottom: 1px solid var(--border);
885
+ padding-bottom: 18px;
886
+ }}
887
+ .sim-chips {{
888
+ display: flex;
889
+ align-items: center;
890
+ gap: 28px;
891
+ }}
892
+ .sim-chip {{ text-align: center; }}
893
+ .sim-chip .lbl {{
894
+ font-size: 11px;
895
+ color: var(--text-dim);
896
+ text-transform: uppercase;
897
+ font-weight: 700;
898
+ letter-spacing: 0.5px;
899
+ }}
900
+ .sim-chip .val {{
901
+ font-size: 38px;
902
+ font-weight: 800;
903
+ line-height: 1;
904
+ margin-top: 4px;
905
+ font-variant-numeric: tabular-nums;
906
+ }}
907
+ .sim-arrow {{
908
+ font-size: 20px;
909
+ color: var(--emerald);
910
+ }}
911
+ .sim-grid {{
912
+ display: grid;
913
+ grid-template-columns: 1fr 1fr;
914
+ gap: 12px;
915
+ }}
916
+ .sim-item {{
917
+ background: var(--bg-surface);
918
+ border: 1px solid var(--border);
919
+ border-radius: 10px;
920
+ padding: 14px 18px;
921
+ display: flex;
922
+ align-items: flex-start;
923
+ gap: 12px;
924
+ cursor: pointer;
925
+ transition: all 0.2s var(--ease);
926
+ }}
927
+ .sim-item:hover {{ border-color: var(--border-highlight); }}
928
+ .sim-item input[type="checkbox"] {{
929
+ margin-top: 3px;
930
+ accent-color: var(--primary);
931
+ width: 16px;
932
+ height: 16px;
933
+ }}
934
+ .sim-body {{ flex: 1; }}
935
+ .sim-title {{
936
+ font-size: 13.5px;
937
+ font-weight: 700;
938
+ display: flex;
939
+ align-items: center;
940
+ justify-content: space-between;
941
+ }}
942
+ .sim-desc {{
943
+ font-size: 11.5px;
944
+ color: var(--text-muted);
945
+ margin-top: 2px;
946
+ }}
947
+
948
+ .hotspot-grid {{
949
+ display: grid;
950
+ grid-template-columns: repeat(2, 1fr);
951
+ gap: 14px;
952
+ }}
953
+ .hotspot-item {{
954
+ background: var(--card);
955
+ border: 1px solid var(--border);
956
+ border-radius: var(--radius);
957
+ padding: 18px 20px;
958
+ display: flex;
959
+ align-items: center;
960
+ justify-content: space-between;
961
+ }}
962
+ .hotspot-name {{
963
+ font-family: var(--font-mono);
964
+ font-size: 13.5px;
965
+ font-weight: 700;
966
+ }}
967
+ .hotspot-dir {{
968
+ font-size: 11.5px;
969
+ color: var(--text-dim);
970
+ margin-top: 2px;
971
+ }}
972
+ .hotspot-tags {{
973
+ margin-top: 8px;
974
+ display: flex;
975
+ gap: 6px;
976
+ }}
977
+ .hotspot-score {{
978
+ font-size: 26px;
979
+ font-weight: 800;
980
+ font-variant-numeric: tabular-nums;
981
+ }}
982
+
983
+ .footer {{
984
+ border-top: 1px solid var(--border);
985
+ background: var(--bg-surface);
986
+ padding: 26px 32px;
987
+ text-align: center;
988
+ font-size: 12.5px;
989
+ color: var(--text-muted);
990
+ }}
991
+
992
+ @media (max-width: 1024px) {{
993
+ .hero-grid {{ grid-template-columns: 1fr; }}
994
+ .metrics-grid {{ grid-template-columns: repeat(2, 1fr); }}
995
+ .layer-grid {{ grid-template-columns: repeat(2, 1fr); }}
996
+ .charts-row {{ grid-template-columns: 1fr; }}
997
+ .diff-split {{ grid-template-columns: 1fr; }}
998
+ .sim-grid {{ grid-template-columns: 1fr; }}
999
+ .hotspot-grid {{ grid-template-columns: 1fr; }}
1000
+ .exec-cards-row {{ grid-template-columns: 1fr; }}
1001
+ }}
1002
+ </style>
1003
+ </head>
1004
+ <body>
1005
+
1006
+ <header class="header">
1007
+ <div class="header-inner">
1008
+ <div class="brand-group">
1009
+ <div class="brand-glyph">
1010
+ <svg viewBox="0 0 24 24" width="20" height="20"><path d="M12 14l3-3"/><path d="M3.34 16a10 10 0 1 1 17.32 0"/></svg>
1011
+ </div>
1012
+ <div class="brand-text">
1013
+ <h1>Performance Optimizer <span class="badge-tag sev-medium">v2.5</span></h1>
1014
+ <p>Production Latency & Scalability Audit • {esc(tech)}</p>
1015
+ </div>
1016
+ </div>
1017
+ <div class="header-actions">
1018
+ <button class="btn" onclick="toggleTheme()" aria-label="Toggle Theme">
1019
+ <span class="icon" id="themeSvg" style="width:14px;height:14px;">
1020
+ <svg viewBox="0 0 24 24" width="14" height="14"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41"/></svg>
1021
+ </span>
1022
+ <span>Theme</span>
1023
+ </button>
1024
+ <button class="btn" onclick="window.print()">
1025
+ <span class="icon" style="width:14px;height:14px;"><svg viewBox="0 0 24 24"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg></span>
1026
+ <span>PDF Print</span>
1027
+ </button>
1028
+ <button class="btn btn-primary" onclick="exportJSON()">
1029
+ <span class="icon" style="width:14px;height:14px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></span>
1030
+ <span>Export JSON</span>
1031
+ </button>
1032
+ </div>
1033
+ </div>
1034
+ </header>
1035
+
1036
+ <nav class="nav-tabs">
1037
+ <button class="tab-btn active" onclick="switchTab('dashboard', this)">
1038
+ <span class="icon" style="width:15px;height:15px;"><rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/></svg></span>
1039
+ <span>Executive Health</span>
1040
+ </button>
1041
+ <button class="tab-btn" onclick="switchTab('issues', this)">
1042
+ <span class="icon" style="width:15px;height:15px;"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
1043
+ <span>Defect Explorer</span>
1044
+ <span class="tab-count">{total}</span>
1045
+ </button>
1046
+ <button class="tab-btn" onclick="switchTab('simulator', this)">
1047
+ <span class="icon" style="width:15px;height:15px;"><line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/><line x1="1" y1="14" x2="7" y2="14"/><line x1="9" y1="8" x2="15" y2="8"/><line x1="17" y1="16" x2="23" y2="16"/></svg></span>
1048
+ <span>Fix Simulator</span>
1049
+ <span class="tab-count" style="background:var(--emerald-bg);color:var(--emerald);">Interactive</span>
1050
+ </button>
1051
+ <button class="tab-btn" onclick="switchTab('files', this)">
1052
+ <span class="icon" style="width:15px;height:15px;"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg></span>
1053
+ <span>File Hotspots</span>
1054
+ <span class="tab-count">{len(worst_files)}</span>
1055
+ </button>
1056
+ <button class="tab-btn" onclick="switchTab('cicd', this)">
1057
+ <span class="icon" style="width:15px;height:15px;"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></span>
1058
+ <span>CI/CD Governance</span>
1059
+ </button>
1060
+ </nav>
1061
+
1062
+ <div class="container">
1063
+
1064
+ <!-- TAB 1: EXECUTIVE HEALTH -->
1065
+ <section id="dashboard-tab" class="tab-panel active">
1066
+ <div class="hero-grid">
1067
+ <div class="score-card">
1068
+ <div style="font-size:11.5px;font-weight:700;color:var(--text-dim);letter-spacing:1px;text-transform:uppercase;">Overall System Health</div>
1069
+ <div class="score-ring-wrap">
1070
+ <svg width="170" height="170" viewBox="0 0 170 170">
1071
+ <circle class="score-ring-bg" cx="85" cy="85" r="70" />
1072
+ <circle id="heroScoreCircle" class="score-ring-progress" cx="85" cy="85" r="70" stroke="{s_color}" stroke-dashoffset="{ring_offset}" />
1073
+ </svg>
1074
+ <div class="score-ring-val">
1075
+ <span id="heroScoreNum" class="score-ring-num" style="color:{s_color};">{overall}</span>
1076
+ <span style="font-size:12px;color:var(--text-dim);font-weight:600;">/ 100</span>
1077
+ </div>
1078
+ </div>
1079
+ <div class="score-status-badge {status_badge_class}">{status_text}</div>
1080
+ <div style="font-size:12.5px;color:var(--text-muted);margin-top:10px;text-align:center;">
1081
+ {total} performance defects detected across {files_scanned} files analyzed ({duration}s).
1082
+ </div>
1083
+ </div>
1084
+
1085
+ <div>
1086
+ <div class="metrics-grid">
1087
+ <div class="metric-card">
1088
+ <div class="metric-icon-box">
1089
+ <span class="icon"><svg viewBox="0 0 24 24" width="18" height="18"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg></span>
1090
+ </div>
1091
+ <div class="metric-title">DOM Re-render Waste</div>
1092
+ <div class="metric-val" style="color:var(--crimson);">-{metrics.get('dom_render_waste_pct', 0)}%</div>
1093
+ <div class="metric-sub">Up to 3x render speedup</div>
1094
+ </div>
1095
+ <div class="metric-card">
1096
+ <div class="metric-icon-box">
1097
+ <span class="icon"><svg viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></svg></span>
1098
+ </div>
1099
+ <div class="metric-title">Query Latency Waste</div>
1100
+ <div class="metric-val" style="color:var(--amber);">+{metrics.get('db_query_reduction_pct', 0)}%</div>
1101
+ <div class="metric-sub">N+1 & unindexed lookups</div>
1102
+ </div>
1103
+ <div class="metric-card">
1104
+ <div class="metric-icon-box">
1105
+ <span class="icon"><svg viewBox="0 0 24 24"><rect x="2" y="8" width="20" height="8" rx="2"/><path d="M6 19v-3"/><path d="M10 19v-3"/><path d="M14 19v-3"/><path d="M18 19v-3"/></svg></span>
1106
+ </div>
1107
+ <div class="metric-title">Heap Memory Leaks</div>
1108
+ <div class="metric-val" style="color:var(--crimson);">{metrics.get('memory_leak_count', 0)}</div>
1109
+ <div class="metric-sub">Unbounded RxJS & arrays</div>
1110
+ </div>
1111
+ <div class="metric-card">
1112
+ <div class="metric-icon-box">
1113
+ <span class="icon"><svg viewBox="0 0 24 24"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/></svg></span>
1114
+ </div>
1115
+ <div class="metric-title">Bundle Bloat Potential</div>
1116
+ <div class="metric-val" style="color:var(--primary-light);">{metrics.get('bundle_reduction_kb', 0)} KB</div>
1117
+ <div class="metric-sub">Eager routes & * imports</div>
1118
+ </div>
1119
+ </div>
1120
+
1121
+ <div class="exec-banner">
1122
+ <div class="exec-banner-title">
1123
+ <span>Primary Action Items for Leadership</span>
1124
+ <span style="font-size:11px;color:var(--text-dim);">Ranked by Impact/Effort Ratio</span>
1125
+ </div>
1126
+ <div class="exec-cards-row">{exec_cards}</div>
1127
+ </div>
1128
+ </div>
1129
+ </div>
1130
+
1131
+ <div class="section-header">
1132
+ <h3>Architectural Layer Breakdown</h3>
1133
+ <span style="font-size:12px;color:var(--text-dim);">Frontend 40% • Backend 30% • Database 20% • Cloud 10%</span>
1134
+ </div>
1135
+ <div class="layer-grid">
1136
+ <div class="layer-card">
1137
+ <div class="layer-top">
1138
+ <span class="badge-tag layer-frontend">Frontend UI</span>
1139
+ <span style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);">{by_layer.get('frontend', 0)} issues</span>
1140
+ </div>
1141
+ <div class="layer-score" style="color:{score_color(s.get('frontend', 100))};">{s.get('frontend', 100)}<span style="font-size:13px;color:var(--text-dim);">/100</span></div>
1142
+ <div class="track-bar"><div class="fill-bar" style="width:{s.get('frontend', 100)}%;background:{score_color(s.get('frontend', 100))};"></div></div>
1143
+ <div style="font-size:11.5px;color:var(--text-muted);">Change detection, loops, teardown</div>
1144
+ </div>
1145
+
1146
+ <div class="layer-card">
1147
+ <div class="layer-top">
1148
+ <span class="badge-tag layer-backend">Node.js / Seneca</span>
1149
+ <span style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);">{by_layer.get('backend', 0)} issues</span>
1150
+ </div>
1151
+ <div class="layer-score" style="color:{score_color(s.get('backend', 100))};">{s.get('backend', 100)}<span style="font-size:13px;color:var(--text-dim);">/100</span></div>
1152
+ <div class="track-bar"><div class="fill-bar" style="width:{s.get('backend', 100)}%;background:{score_color(s.get('backend', 100))};"></div></div>
1153
+ <div style="font-size:11.5px;color:var(--text-muted);">Event loop, N+1 queries, sockets</div>
1154
+ </div>
1155
+
1156
+ <div class="layer-card">
1157
+ <div class="layer-top">
1158
+ <span class="badge-tag layer-database">SQL / Database</span>
1159
+ <span style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);">{by_layer.get('database', 0)} issues</span>
1160
+ </div>
1161
+ <div class="layer-score" style="color:{score_color(s.get('database', 100))};">{s.get('database', 100)}<span style="font-size:13px;color:var(--text-dim);">/100</span></div>
1162
+ <div class="track-bar"><div class="fill-bar" style="width:{s.get('database', 100)}%;background:{score_color(s.get('database', 100))};"></div></div>
1163
+ <div style="font-size:11.5px;color:var(--text-muted);">Indexes, deep OFFSET, plan cache</div>
1164
+ </div>
1165
+
1166
+ <div class="layer-card">
1167
+ <div class="layer-top">
1168
+ <span class="badge-tag layer-infra">AWS / Cloud</span>
1169
+ <span style="font-size:12px;color:var(--text-dim);font-family:var(--font-mono);">{by_layer.get('infra', 0)} issues</span>
1170
+ </div>
1171
+ <div class="layer-score" style="color:{score_color(s.get('infra', 100))};">{s.get('infra', 100)}<span style="font-size:13px;color:var(--text-dim);">/100</span></div>
1172
+ <div class="track-bar"><div class="fill-bar" style="width:{s.get('infra', 100)}%;background:{score_color(s.get('infra', 100))};"></div></div>
1173
+ <div style="font-size:11.5px;color:var(--text-muted);">Lambda CPU throttle, RDS Proxy</div>
1174
+ </div>
1175
+ </div>
1176
+
1177
+ <div class="charts-row">
1178
+ <div class="chart-box">
1179
+ <h4>Defect Severity Profile</h4>
1180
+ <div class="chart-canvas-wrap"><canvas id="chartSeverity"></canvas></div>
1181
+ </div>
1182
+ <div class="chart-box">
1183
+ <h4>Layer Distribution</h4>
1184
+ <div class="chart-canvas-wrap"><canvas id="chartLayer"></canvas></div>
1185
+ </div>
1186
+ <div class="chart-box">
1187
+ <h4>Projected Efficiency Improvements</h4>
1188
+ <div class="chart-canvas-wrap"><canvas id="chartProjected"></canvas></div>
1189
+ </div>
1190
+ </div>
1191
+ </section>
1192
+
1193
+ <!-- TAB 2: DEFECT EXPLORER -->
1194
+ <section id="issues-tab" class="tab-panel">
1195
+ <div class="section-header">
1196
+ <h3>Defect Diagnostic Console with Code Patches</h3>
1197
+ <span style="font-size:12.5px;color:var(--text-dim);">Click any record to expand the source patch</span>
1198
+ </div>
1199
+
1200
+ <div class="filter-bar">
1201
+ <div class="search-wrap">
1202
+ <span class="icon" style="width:14px;height:14px;color:var(--text-dim);"><svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
1203
+ <input type="text" id="searchInput" placeholder="Search by title, rule ID, filename, or keyword..." onkeyup="filterIssuesTable()">
1204
+ </div>
1205
+ <div class="pill-group">
1206
+ <button class="pill-btn active" onclick="setSeverityFilter('all', this)">All ({total})</button>
1207
+ <button class="pill-btn" onclick="setSeverityFilter('critical', this)" style="color:var(--crimson);">Critical ({by_sev.get('critical', 0)})</button>
1208
+ <button class="pill-btn" onclick="setSeverityFilter('high', this)" style="color:var(--amber);">High ({by_sev.get('high', 0)})</button>
1209
+ <button class="pill-btn" onclick="setSeverityFilter('medium', this)">Medium ({by_sev.get('medium', 0)})</button>
1210
+ </div>
1211
+ <div class="pill-group">
1212
+ <button class="pill-btn active" onclick="setLayerFilter('all', this)">All Layers</button>
1213
+ <button class="pill-btn" onclick="setLayerFilter('frontend', this)">Frontend ({by_layer.get('frontend', 0)})</button>
1214
+ <button class="pill-btn" onclick="setLayerFilter('backend', this)">Backend ({by_layer.get('backend', 0)})</button>
1215
+ <button class="pill-btn" onclick="setLayerFilter('database', this)">Database ({by_layer.get('database', 0)})</button>
1216
+ <button class="pill-btn" onclick="setLayerFilter('infra', this)">Infra ({by_layer.get('infra', 0)})</button>
1217
+ </div>
1218
+ </div>
1219
+
1220
+ <div class="table-shell">
1221
+ <table class="diag-table" id="issuesTable">
1222
+ <thead>
1223
+ <tr>
1224
+ <th width="80">Rule ID</th>
1225
+ <th>Issue Title & Source File</th>
1226
+ <th width="120">Severity</th>
1227
+ <th width="120">Layer</th>
1228
+ <th width="110">Impact / Effort</th>
1229
+ <th width="80">Rank</th>
1230
+ <th width="80">Action</th>
1231
+ </tr>
1232
+ </thead>
1233
+ <tbody id="issuesTableBody">
1234
+ {table_rows}
1235
+ </tbody>
1236
+ </table>
1237
+ </div>
1238
+ </section>
1239
+
1240
+ <!-- TAB 3: FIX SIMULATOR -->
1241
+ <section id="simulator-tab" class="tab-panel">
1242
+ <div class="panel-box">
1243
+ <div class="sim-head">
1244
+ <div>
1245
+ <h3 style="font-size:18px;font-weight:800;letter-spacing:-0.4px;">Fix Simulator & Sprint ROI Planner</h3>
1246
+ <p style="font-size:13px;color:var(--text-muted);margin-top:4px;">
1247
+ Toggle planned fixes below to simulate live score improvement, estimated latency reduction, and sprint velocity.
1248
+ </p>
1249
+ </div>
1250
+ <div class="sim-chips">
1251
+ <div class="sim-chip">
1252
+ <div class="lbl">Baseline Score</div>
1253
+ <div class="val" style="color:{s_color};">{overall}</div>
1254
+ </div>
1255
+ <div class="sim-arrow">→</div>
1256
+ <div class="sim-chip">
1257
+ <div class="lbl">Simulated Rating</div>
1258
+ <div class="val" id="simScoreVal" style="color:var(--emerald);">{overall}</div>
1259
+ </div>
1260
+ </div>
1261
+ </div>
1262
+
1263
+ <div class="sim-grid">
1264
+ {sim_checklist}
1265
+ </div>
1266
+ </div>
1267
+ </section>
1268
+
1269
+ <!-- TAB 4: FILE HOTSPOTS -->
1270
+ <section id="files-tab" class="tab-panel">
1271
+ <div class="section-header">
1272
+ <h3>File Defect Density Heatmap</h3>
1273
+ <span style="font-size:12px;color:var(--text-dim);">Files with highest concentration of performance risks</span>
1274
+ </div>
1275
+ <div class="hotspot-grid">
1276
+ {file_cards}
1277
+ </div>
1278
+ </section>
1279
+
1280
+ <!-- TAB 5: CI/CD GOVERNANCE -->
1281
+ <section id="cicd-tab" class="tab-panel">
1282
+ <div class="panel-box">
1283
+ <h3 style="font-size:18px;font-weight:800;margin-bottom:12px;letter-spacing:-0.4px;">Automated CI/CD Quality Gate Setup</h3>
1284
+ <p style="font-size:13.5px;color:var(--text-muted);line-height:1.6;margin-bottom:24px;">
1285
+ Integrate Performance Optimizer into your GitHub Actions, GitLab CI, or Jenkins pipeline to block performance regressions on pull requests.
1286
+ </p>
1287
+
1288
+ <div class="diff-pane" style="margin-bottom:20px;">
1289
+ <div class="diff-pane-head good">
1290
+ <span class="head-tag">.github/workflows/perf-gate.yml</span>
1291
+ <button class="copy-chip" onclick="copySnippet(this)">Copy YAML</button>
1292
+ </div>
1293
+ <pre class="diff-code">name: Performance Gate
1294
+ on: [pull_request]
1295
+
1296
+ jobs:
1297
+ performance-audit:
1298
+ runs-on: ubuntu-latest
1299
+ steps:
1300
+ - uses: actions/checkout@v4
1301
+ - name: Set up Python
1302
+ uses: actions/setup-python@v5
1303
+ with:
1304
+ python-version: '3.11'
1305
+
1306
+ - name: Run Performance Optimizer Gate
1307
+ run: |
1308
+ python optimizer.py \
1309
+ --frontend ./frontend \
1310
+ --backend ./backend \
1311
+ --fail-on critical \
1312
+ --min-score 75 \
1313
+ --json \
1314
+ --markdown-summary pr_perf_comment.md
1315
+
1316
+ - name: Post PR Comment
1317
+ uses: marocchino/sticky-pull-request-comment@v2
1318
+ if: always()
1319
+ with:
1320
+ path: pr_perf_comment.md</pre>
1321
+ </div>
1322
+ </div>
1323
+ </section>
1324
+
1325
+ </div>
1326
+
1327
+ <footer class="footer">
1328
+ <p>Performance Optimizer v2.5 • Zero SonarQube Bloat • {esc(tech)} • Scan: {esc(scan_time)} • {files_scanned} files analyzed</p>
1329
+ </footer>
1330
+
1331
+ <script>
1332
+ function switchTab(tabId, btn) {{
1333
+ document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
1334
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
1335
+ if (btn) btn.classList.add('active');
1336
+ const target = document.getElementById(tabId + '-tab');
1337
+ if (target) target.classList.add('active');
1338
+ }}
1339
+
1340
+ let currentTheme = 'dark';
1341
+ function toggleTheme() {{
1342
+ currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
1343
+ document.documentElement.setAttribute('data-theme', currentTheme);
1344
+ const icon = document.getElementById('themeSvg');
1345
+ if (currentTheme === 'dark') {{
1346
+ icon.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14"><circle cx="12" cy="12" r="4"/><path d="M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M4.93 19.07l1.41-1.41m11.32-11.32l1.41-1.41"/></svg>';
1347
+ }} else {{
1348
+ icon.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
1349
+ }}
1350
+ }}
1351
+
1352
+ function toggleRow(rowId) {{
1353
+ const row = document.getElementById(rowId);
1354
+ if (row) {{
1355
+ row.classList.toggle('show');
1356
+ }}
1357
+ }}
1358
+
1359
+ function copySnippet(btn) {{
1360
+ const code = btn.closest('.diff-pane').querySelector('pre').textContent;
1361
+ navigator.clipboard.writeText(code).then(() => {{
1362
+ const orig = btn.textContent;
1363
+ btn.textContent = 'Copied';
1364
+ setTimeout(() => btn.textContent = orig, 1500);
1365
+ }});
1366
+ }}
1367
+
1368
+ let activeSev = 'all';
1369
+ let activeLayer = 'all';
1370
+
1371
+ function setSeverityFilter(sev, btn) {{
1372
+ activeSev = sev;
1373
+ btn.parentElement.querySelectorAll('.pill-btn').forEach(b => b.classList.remove('active'));
1374
+ btn.classList.add('active');
1375
+ filterIssuesTable();
1376
+ }}
1377
+
1378
+ function setLayerFilter(layer, btn) {{
1379
+ activeLayer = layer;
1380
+ btn.parentElement.querySelectorAll('.pill-btn').forEach(b => b.classList.remove('active'));
1381
+ btn.classList.add('active');
1382
+ filterIssuesTable();
1383
+ }}
1384
+
1385
+ function filterIssuesTable() {{
1386
+ const query = (document.getElementById('searchInput').value || '').toLowerCase();
1387
+ const rows = document.querySelectorAll('#issuesTableBody tr.main-row');
1388
+
1389
+ rows.forEach(r => {{
1390
+ const sev = r.dataset.sev;
1391
+ const layer = r.dataset.layer;
1392
+ const search = r.dataset.search || '';
1393
+ const nextExpRow = r.nextElementSibling;
1394
+
1395
+ const matchesSev = (activeSev === 'all' || sev === activeSev);
1396
+ const matchesLayer = (activeLayer === 'all' || layer === activeLayer);
1397
+ const matchesQuery = !query || search.includes(query) || r.textContent.toLowerCase().includes(query);
1398
+
1399
+ if (matchesSev && matchesLayer && matchesQuery) {{
1400
+ r.style.display = '';
1401
+ }} else {{
1402
+ r.style.display = 'none';
1403
+ if (nextExpRow && nextExpRow.classList.contains('expanded-row')) {{
1404
+ nextExpRow.classList.remove('show');
1405
+ }}
1406
+ }}
1407
+ }});
1408
+ }}
1409
+
1410
+ const baselineScore = {overall};
1411
+ function recalculateSimScore() {{
1412
+ let score = baselineScore;
1413
+ const checkboxes = document.querySelectorAll('.sim-grid input[type="checkbox"]');
1414
+ checkboxes.forEach(cb => {{
1415
+ if (cb.checked) {{
1416
+ score += parseInt(cb.dataset.points || 5, 10);
1417
+ }}
1418
+ }});
1419
+ score = Math.min(100, score);
1420
+ const valEl = document.getElementById('simScoreVal');
1421
+ valEl.textContent = score;
1422
+ valEl.style.color = score >= 80 ? 'var(--emerald)' : score >= 60 ? 'var(--amber)' : 'var(--crimson)';
1423
+ }}
1424
+
1425
+ function exportJSON() {{
1426
+ const data = {{
1427
+ meta: {json.dumps(self.meta)},
1428
+ scores: {json.dumps(self.scores)},
1429
+ issues: {json.dumps([i.to_dict() for i in self.issues])}
1430
+ }};
1431
+ const blob = new Blob([JSON.stringify(data, null, 2)], {{ type: 'application/json' }});
1432
+ const url = URL.createObjectURL(blob);
1433
+ const a = document.createElement('a');
1434
+ a.href = url;
1435
+ a.download = 'performance_report.json';
1436
+ a.click();
1437
+ }}
1438
+
1439
+ window.addEventListener('DOMContentLoaded', () => {{
1440
+ new Chart(document.getElementById('chartSeverity'), {{
1441
+ type: 'doughnut',
1442
+ data: {{
1443
+ labels: ['Critical', 'High', 'Medium', 'Low'],
1444
+ datasets: [{{
1445
+ data: {sev_chart_data},
1446
+ backgroundColor: ['#f43f5e', '#f59e0b', '#6366f1', '#64748b'],
1447
+ borderWidth: 0
1448
+ }}]
1449
+ }},
1450
+ options: {{
1451
+ responsive: true,
1452
+ maintainAspectRatio: false,
1453
+ plugins: {{ legend: {{ position: 'right', labels: {{ color: '#94a3b8', boxWidth: 10, font: {{ size: 11 }} }} }} }},
1454
+ cutout: '72%'
1455
+ }}
1456
+ }});
1457
+
1458
+ new Chart(document.getElementById('chartLayer'), {{
1459
+ type: 'bar',
1460
+ data: {{
1461
+ labels: ['Frontend', 'Backend', 'Database', 'Infra'],
1462
+ datasets: [{{
1463
+ data: {layer_chart_data},
1464
+ backgroundColor: ['#6366f1', '#10b981', '#f59e0b', '#06b6d4'],
1465
+ borderRadius: 5,
1466
+ borderWidth: 0
1467
+ }}]
1468
+ }},
1469
+ options: {{
1470
+ responsive: true,
1471
+ maintainAspectRatio: false,
1472
+ plugins: {{ legend: {{ display: false }} }},
1473
+ scales: {{
1474
+ x: {{ ticks: {{ color: '#64748b', font: {{ size: 11 }} }}, grid: {{ display: false }} }},
1475
+ y: {{ ticks: {{ color: '#64748b', font: {{ size: 11 }} }}, grid: {{ color: 'rgba(255,255,255,0.04)' }} }}
1476
+ }}
1477
+ }}
1478
+ }});
1479
+
1480
+ new Chart(document.getElementById('chartProjected'), {{
1481
+ type: 'bar',
1482
+ data: {{
1483
+ labels: ['API Latency', 'DB Queries', 'DOM Cycles', 'Heap Leaks', 'Bundle Size'],
1484
+ datasets: [
1485
+ {{ label: 'Current Overhead', data: [100, 100, 100, 100, 100], backgroundColor: 'rgba(244,63,94,0.3)', borderRadius: 4, borderWidth: 0 }},
1486
+ {{ label: 'After Remediations', data: [15, 8, 12, 0, 48], backgroundColor: 'rgba(16,185,129,0.75)', borderRadius: 4, borderWidth: 0 }}
1487
+ ]
1488
+ }},
1489
+ options: {{
1490
+ responsive: true,
1491
+ maintainAspectRatio: false,
1492
+ plugins: {{ legend: {{ labels: {{ color: '#94a3b8', boxWidth: 10, font: {{ size: 11 }} }} }} }},
1493
+ scales: {{
1494
+ x: {{ ticks: {{ color: '#64748b', font: {{ size: 10 }} }}, grid: {{ display: false }} }},
1495
+ y: {{ ticks: {{ color: '#64748b', font: {{ size: 10 }} }}, grid: {{ color: 'rgba(255,255,255,0.04)' }} }}
1496
+ }}
1497
+ }}
1498
+ }});
1499
+ }});
1500
+ </script>
1501
+ </body>
1502
+ </html>'''