git-repo-analyzer-test 1.0.0

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 (65) hide show
  1. package/.github/copilot-instructions.md +108 -0
  2. package/.idea/aianalyzer.iml +9 -0
  3. package/.idea/misc.xml +6 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/API_REFERENCE.md +244 -0
  7. package/ENHANCEMENTS.md +282 -0
  8. package/README.md +179 -0
  9. package/USAGE.md +189 -0
  10. package/analysis.txt +0 -0
  11. package/bin/cli.js +135 -0
  12. package/docs/SONARCLOUD_ANALYSIS_COVERED.md +144 -0
  13. package/docs/SonarCloud_Presentation_Points.md +81 -0
  14. package/docs/UI_IMPROVEMENTS.md +117 -0
  15. package/package-lock_cmd.json +542 -0
  16. package/package.json +44 -0
  17. package/package_command.json +16 -0
  18. package/public/analysis-options.json +31 -0
  19. package/public/images/README.txt +2 -0
  20. package/public/images/rws-logo.png +0 -0
  21. package/public/index.html +2433 -0
  22. package/repositories.example.txt +17 -0
  23. package/sample-repos.txt +20 -0
  24. package/src/analyzers/accessibility.js +47 -0
  25. package/src/analyzers/cicd-enhanced.js +113 -0
  26. package/src/analyzers/codeReview-enhanced.js +599 -0
  27. package/src/analyzers/codeReview-enhanced.js:Zone.Identifier +3 -0
  28. package/src/analyzers/codeReview.js +171 -0
  29. package/src/analyzers/codeReview.js:Zone.Identifier +3 -0
  30. package/src/analyzers/documentation-enhanced.js +137 -0
  31. package/src/analyzers/performance-enhanced.js +747 -0
  32. package/src/analyzers/performance-enhanced.js:Zone.Identifier +3 -0
  33. package/src/analyzers/performance.js +211 -0
  34. package/src/analyzers/performance.js:Zone.Identifier +3 -0
  35. package/src/analyzers/performance_cmd.js +216 -0
  36. package/src/analyzers/quality-enhanced.js +386 -0
  37. package/src/analyzers/quality-enhanced.js:Zone.Identifier +3 -0
  38. package/src/analyzers/quality.js +92 -0
  39. package/src/analyzers/quality.js:Zone.Identifier +3 -0
  40. package/src/analyzers/security-enhanced.js +512 -0
  41. package/src/analyzers/security-enhanced.js:Zone.Identifier +3 -0
  42. package/src/analyzers/snyk-ai.js:Zone.Identifier +3 -0
  43. package/src/analyzers/sonarcloud.js +928 -0
  44. package/src/analyzers/vulnerability.js +185 -0
  45. package/src/analyzers/vulnerability.js:Zone.Identifier +3 -0
  46. package/src/cli.js:Zone.Identifier +3 -0
  47. package/src/config.js +43 -0
  48. package/src/core/analyzerEngine.js +68 -0
  49. package/src/core/reportGenerator.js +21 -0
  50. package/src/gemini.js +321 -0
  51. package/src/github/client.js +124 -0
  52. package/src/github/client.js:Zone.Identifier +3 -0
  53. package/src/index.js +93 -0
  54. package/src/index_cmd.js +130 -0
  55. package/src/openai.js +297 -0
  56. package/src/report/generator.js +459 -0
  57. package/src/report/generator_cmd.js +459 -0
  58. package/src/report/pdf-generator.js +387 -0
  59. package/src/report/pdf-generator.js:Zone.Identifier +3 -0
  60. package/src/server.js +431 -0
  61. package/src/server.js:Zone.Identifier +3 -0
  62. package/src/server_cmd.js +434 -0
  63. package/src/sonarcloud/client.js +365 -0
  64. package/src/sonarcloud/scanner.js +171 -0
  65. package/src.zip +0 -0
@@ -0,0 +1,2433 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Analyze Git repositories for code quality, SonarCloud metrics, and insights. Enter a repo URL to get quality gate, issues, and export to PDF.">
7
+ <meta name="theme-color" content="#6A1B9A">
8
+ <title>Repository Analyzer</title>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
12
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
13
+ <style>
14
+ * {
15
+ margin: 0;
16
+ padding: 0;
17
+ box-sizing: border-box;
18
+ }
19
+
20
+ :root {
21
+ --bg-body: #f0f4ff;
22
+ --bg-body-grad: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(230, 0, 108, 0.12), transparent),
23
+ radial-gradient(ellipse 60% 40% at 100% 50%, rgba(138, 79, 220, 0.1), transparent),
24
+ radial-gradient(ellipse 50% 30% at 0% 80%, rgba(106, 27, 154, 0.1), transparent);
25
+ --bg-surface: rgba(255, 255, 255, 0.98);
26
+ --bg-elevated: #f8fafc;
27
+ --bg-input: #fafafa;
28
+ --text-primary: #1e293b;
29
+ --text-secondary: #475569;
30
+ --text-muted: #64748b;
31
+ --border: #e2e8f0;
32
+ --border-focus: #E6006C;
33
+ --shadow: rgba(0, 0, 0, 0.08);
34
+ --shadow-strong: rgba(0, 0, 0, 0.15);
35
+ --accent-from: #E6006C;
36
+ --accent-to: #8A4FDC;
37
+ }
38
+
39
+ html.theme-dark {
40
+ --bg-body: #0f0a14;
41
+ --bg-body-grad: radial-gradient(ellipse 80% 50% at 50% -20%, rgba(230, 0, 108, 0.2), transparent),
42
+ radial-gradient(ellipse 60% 40% at 100% 50%, rgba(138, 79, 220, 0.15), transparent),
43
+ radial-gradient(ellipse 50% 30% at 0% 80%, rgba(106, 27, 154, 0.15), transparent);
44
+ --bg-surface: rgba(26, 18, 32, 0.98);
45
+ --bg-elevated: #1a1220;
46
+ --bg-input: #251a30;
47
+ --text-primary: #f1f5f9;
48
+ --text-secondary: #cbd5e1;
49
+ --text-muted: #94a3b8;
50
+ --border: #334155;
51
+ --border-focus: #c084fc;
52
+ --shadow: rgba(0, 0, 0, 0.3);
53
+ --shadow-strong: rgba(0, 0, 0, 0.5);
54
+ --accent-from: #f472b6;
55
+ --accent-to: #a78bfa;
56
+ }
57
+
58
+ body {
59
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
60
+ min-height: 100vh;
61
+ padding: 24px;
62
+ background: var(--bg-body);
63
+ background-image: var(--bg-body-grad);
64
+ color: var(--text-primary);
65
+ transition: background 0.35s ease, color 0.25s ease;
66
+ }
67
+
68
+ .top-bar {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ padding: 14px 24px;
73
+ margin: -24px -24px 24px -24px;
74
+ background: var(--bg-surface);
75
+ box-shadow: 0 1px 0 var(--border);
76
+ position: relative;
77
+ z-index: 10;
78
+ transition: background 0.35s ease, box-shadow 0.25s ease;
79
+ }
80
+ .top-bar-logo {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: 10px;
84
+ text-decoration: none;
85
+ color: inherit;
86
+ font-weight: 700;
87
+ font-size: 1.25rem;
88
+ letter-spacing: -0.02em;
89
+ background: linear-gradient(135deg, #6A1B9A 0%, #E6006C 50%, #8A4FDC 100%);
90
+ -webkit-background-clip: text;
91
+ -webkit-text-fill-color: transparent;
92
+ background-clip: text;
93
+ }
94
+ .top-bar-logo svg,
95
+ .top-bar-logo img {
96
+ flex-shrink: 0;
97
+ }
98
+ .top-bar-logo img.top-bar-logo-img {
99
+ height: 48px;
100
+ width: auto;
101
+ display: block;
102
+ vertical-align: middle;
103
+ }
104
+ .top-bar-nav {
105
+ display: flex;
106
+ align-items: center;
107
+ gap: 8px;
108
+ }
109
+ .top-bar-btn {
110
+ padding: 8px 18px;
111
+ border-radius: 10px;
112
+ font-size: 0.9rem;
113
+ font-weight: 600;
114
+ font-family: inherit;
115
+ cursor: pointer;
116
+ transition: background 0.2s, color 0.2s;
117
+ border: none;
118
+ }
119
+ .top-bar-btn.signin {
120
+ background: linear-gradient(135deg, #E6006C 0%, #8A4FDC 100%);
121
+ color: white;
122
+ }
123
+ .top-bar-btn.signin:hover {
124
+ opacity: 0.92;
125
+ transform: translateY(-1px);
126
+ }
127
+ .top-bar-btn.signout {
128
+ background: var(--bg-elevated);
129
+ color: var(--text-secondary);
130
+ border: 1px solid var(--border);
131
+ }
132
+ .top-bar-btn.signout:hover {
133
+ background: var(--border);
134
+ color: var(--text-primary);
135
+ }
136
+
137
+ .theme-toggle {
138
+ display: inline-flex;
139
+ align-items: center;
140
+ gap: 4px;
141
+ padding: 6px 12px;
142
+ border-radius: 999px;
143
+ border: 1px solid var(--border);
144
+ background: var(--bg-elevated);
145
+ cursor: pointer;
146
+ font-size: 0.85rem;
147
+ color: var(--text-secondary);
148
+ transition: background 0.2s, color 0.2s, border-color 0.2s;
149
+ }
150
+ .theme-toggle:hover {
151
+ color: var(--text-primary);
152
+ background: var(--border);
153
+ }
154
+ .theme-toggle svg { width: 18px; height: 18px; opacity: 0.9; }
155
+ html.theme-dark .theme-toggle .icon-light { display: none; }
156
+ html:not(.theme-dark) .theme-toggle .icon-dark { display: none; }
157
+
158
+ .container {
159
+ background: var(--bg-surface);
160
+ border-radius: 24px;
161
+ box-shadow: 0 25px 80px var(--shadow-strong), 0 0 0 1px var(--border);
162
+ max-width: 1100px;
163
+ margin: 0 auto;
164
+ padding: 48px 40px;
165
+ backdrop-filter: blur(20px);
166
+ transition: background 0.35s ease, box-shadow 0.25s ease;
167
+ }
168
+
169
+ .header {
170
+ text-align: center;
171
+ margin-bottom: 48px;
172
+ padding-bottom: 32px;
173
+ border-bottom: 1px solid var(--border);
174
+ }
175
+
176
+ .header h1 {
177
+ font-size: 2.75rem;
178
+ font-weight: 800;
179
+ letter-spacing: -0.03em;
180
+ margin-bottom: 12px;
181
+ background: linear-gradient(135deg, #6A1B9A 0%, #E6006C 50%, #8A4FDC 100%);
182
+ -webkit-background-clip: text;
183
+ -webkit-text-fill-color: transparent;
184
+ background-clip: text;
185
+ }
186
+
187
+ .header p {
188
+ color: var(--text-muted);
189
+ font-size: 1.125rem;
190
+ font-weight: 500;
191
+ max-width: 420px;
192
+ margin: 0 auto;
193
+ line-height: 1.5;
194
+ }
195
+
196
+ .input-section {
197
+ margin-bottom: 32px;
198
+ }
199
+
200
+ .input-group {
201
+ display: flex;
202
+ gap: 12px;
203
+ margin-bottom: 16px;
204
+ }
205
+
206
+ .input-group input {
207
+ flex: 1;
208
+ padding: 16px 20px;
209
+ border: 2px solid var(--border);
210
+ border-radius: 14px;
211
+ font-size: 1rem;
212
+ font-family: inherit;
213
+ background: var(--bg-surface);
214
+ color: var(--text-primary);
215
+ transition: border-color 0.2s, box-shadow 0.2s;
216
+ }
217
+
218
+ .input-group input:focus {
219
+ outline: none;
220
+ border-color: var(--border-focus);
221
+ box-shadow: 0 0 0 4px rgba(230, 0, 108, 0.2);
222
+ }
223
+
224
+ .input-group input::placeholder {
225
+ color: var(--text-muted);
226
+ }
227
+
228
+ .prompt-field {
229
+ margin-top: 20px;
230
+ }
231
+
232
+ .prompt-label-row {
233
+ display: flex;
234
+ align-items: center;
235
+ justify-content: space-between;
236
+ flex-wrap: wrap;
237
+ gap: 8px;
238
+ margin-bottom: 10px;
239
+ }
240
+ .prompt-field .prompt-label {
241
+ font-size: 0.9rem;
242
+ font-weight: 600;
243
+ color: var(--text-secondary);
244
+ }
245
+ .prompt-actions {
246
+ display: inline-flex;
247
+ align-items: center;
248
+ gap: 4px;
249
+ }
250
+ .prompt-actions-sep {
251
+ color: var(--text-muted);
252
+ font-size: 0.85rem;
253
+ }
254
+ .prompt-field .link-btn {
255
+ background: none;
256
+ border: none;
257
+ padding: 0;
258
+ font-size: 0.85rem;
259
+ color: var(--accent);
260
+ cursor: pointer;
261
+ text-decoration: underline;
262
+ }
263
+ .prompt-field .link-btn:hover {
264
+ color: var(--accent-hover, var(--accent));
265
+ }
266
+
267
+ .prompt-options {
268
+ display: grid;
269
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
270
+ gap: 10px 20px;
271
+ padding: 16px 20px;
272
+ border: 2px solid var(--border);
273
+ border-radius: 14px;
274
+ background: var(--bg-input);
275
+ }
276
+
277
+ .prompt-option {
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 10px;
281
+ cursor: pointer;
282
+ font-size: 0.9rem;
283
+ color: var(--text-primary);
284
+ user-select: none;
285
+ }
286
+
287
+ .prompt-option input[type="checkbox"] {
288
+ width: 18px;
289
+ height: 18px;
290
+ accent-color: #E6006C;
291
+ cursor: pointer;
292
+ }
293
+ .prompt-option-with-hint { display: flex; flex-direction: column; gap: 4px; }
294
+ .option-hint {
295
+ font-size: 0.8rem;
296
+ color: var(--text-secondary, #64748b);
297
+ margin-left: 28px;
298
+ padding: 8px 10px;
299
+ background: var(--bg-input, #f8fafc);
300
+ border-radius: 8px;
301
+ border-left: 3px solid var(--border, #e2e8f0);
302
+ }
303
+ .option-hint ul { margin: 0; padding-left: 1.2em; }
304
+ .option-hint li { margin: 2px 0; }
305
+
306
+ .input-group button {
307
+ padding: 16px 36px;
308
+ background: linear-gradient(135deg, #E6006C 0%, #8A4FDC 100%);
309
+ color: white;
310
+ border: none;
311
+ border-radius: 14px;
312
+ font-size: 1rem;
313
+ font-weight: 700;
314
+ font-family: inherit;
315
+ cursor: pointer;
316
+ transition: transform 0.2s, box-shadow 0.2s;
317
+ box-shadow: 0 4px 14px rgba(230, 0, 108, 0.4);
318
+ }
319
+
320
+ .input-group button:hover {
321
+ transform: translateY(-2px);
322
+ box-shadow: 0 8px 24px rgba(230, 0, 108, 0.45);
323
+ }
324
+
325
+ .input-group button:disabled {
326
+ opacity: 0.6;
327
+ cursor: not-allowed;
328
+ transform: none;
329
+ }
330
+
331
+ #loadingDiv {
332
+ display: none;
333
+ text-align: center;
334
+ padding: 56px 24px;
335
+ }
336
+
337
+ .spinner {
338
+ width: 56px;
339
+ height: 56px;
340
+ border: 4px solid var(--border);
341
+ border-top-color: var(--accent-from);
342
+ border-radius: 50%;
343
+ animation: spin 0.9s linear infinite;
344
+ margin: 0 auto 24px;
345
+ }
346
+
347
+ #loadingDiv p {
348
+ color: #E6006C;
349
+ font-weight: 600;
350
+ font-size: 1.1rem;
351
+ }
352
+
353
+ @keyframes spin {
354
+ to { transform: rotate(360deg); }
355
+ }
356
+
357
+ #resultsDiv {
358
+ display: none;
359
+ }
360
+
361
+ .repo-title {
362
+ font-size: 1.75rem;
363
+ font-weight: 700;
364
+ color: var(--text-primary);
365
+ margin-bottom: 32px;
366
+ padding-bottom: 16px;
367
+ border-bottom: 2px solid var(--border);
368
+ letter-spacing: -0.02em;
369
+ }
370
+
371
+ .scores-grid {
372
+ display: grid;
373
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
374
+ gap: 20px;
375
+ margin-bottom: 40px;
376
+ }
377
+
378
+ .score-card {
379
+ background: linear-gradient(135deg, #E6006C 0%, #8A4FDC 100%);
380
+ color: white;
381
+ padding: 28px;
382
+ border-radius: 16px;
383
+ text-align: center;
384
+ box-shadow: 0 8px 24px rgba(230, 0, 108, 0.25);
385
+ }
386
+
387
+ .score-card.excellent {
388
+ background: linear-gradient(135deg, #059669 0%, #10b981 100%);
389
+ box-shadow: 0 8px 24px rgba(5, 150, 105, 0.25);
390
+ }
391
+
392
+ .score-card.good {
393
+ background: linear-gradient(135deg, #0284c7 0%, #0ea5e9 100%);
394
+ box-shadow: 0 8px 24px rgba(2, 132, 199, 0.25);
395
+ }
396
+
397
+ .score-card.fair {
398
+ background: linear-gradient(135deg, #d97706 0%, #f59e0b 100%);
399
+ box-shadow: 0 8px 24px rgba(217, 119, 6, 0.25);
400
+ }
401
+
402
+ .score-card.poor {
403
+ background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
404
+ box-shadow: 0 8px 24px rgba(220, 38, 38, 0.25);
405
+ }
406
+
407
+ .score-label {
408
+ font-size: 0.9rem;
409
+ opacity: 0.95;
410
+ margin-bottom: 10px;
411
+ font-weight: 600;
412
+ }
413
+
414
+ .score-main {
415
+ display: flex;
416
+ justify-content: center;
417
+ align-items: center;
418
+ gap: 12px;
419
+ margin: 12px 0;
420
+ }
421
+
422
+ .score-value {
423
+ font-size: 2.5rem;
424
+ font-weight: 800;
425
+ letter-spacing: -0.02em;
426
+ }
427
+
428
+ .score-rating {
429
+ font-size: 1.75rem;
430
+ font-weight: 700;
431
+ opacity: 0.95;
432
+ }
433
+
434
+ .score-bar {
435
+ width: 100%;
436
+ height: 6px;
437
+ background: rgba(255, 255, 255, 0.25);
438
+ border-radius: 4px;
439
+ margin-top: 14px;
440
+ overflow: hidden;
441
+ }
442
+
443
+ .score-bar-fill {
444
+ height: 100%;
445
+ background: rgba(255, 255, 255, 0.9);
446
+ border-radius: 4px;
447
+ }
448
+
449
+ .section {
450
+ background: var(--bg-elevated);
451
+ padding: 28px;
452
+ border-radius: 16px;
453
+ margin-bottom: 28px;
454
+ border: 1px solid var(--border);
455
+ box-shadow: 0 1px 3px var(--shadow);
456
+ transition: background 0.35s ease, border-color 0.25s ease;
457
+ }
458
+
459
+ .section h3 {
460
+ color: var(--text-primary);
461
+ margin-bottom: 20px;
462
+ font-size: 1.25rem;
463
+ font-weight: 700;
464
+ display: flex;
465
+ align-items: center;
466
+ gap: 10px;
467
+ letter-spacing: -0.01em;
468
+ }
469
+
470
+ .section-icon {
471
+ font-size: 1.35em;
472
+ }
473
+
474
+ .findings-list {
475
+ background: var(--bg-surface);
476
+ border: 1px solid var(--border);
477
+ border-radius: 8px;
478
+ overflow: hidden;
479
+ margin-bottom: 20px;
480
+ }
481
+
482
+ .finding-item {
483
+ padding: 15px;
484
+ border-bottom: 1px solid var(--border);
485
+ display: flex;
486
+ gap: 15px;
487
+ }
488
+
489
+ .finding-item:last-child {
490
+ border-bottom: none;
491
+ }
492
+
493
+ .finding-severity {
494
+ padding: 4px 10px;
495
+ border-radius: 4px;
496
+ font-size: 0.85em;
497
+ font-weight: bold;
498
+ min-width: 80px;
499
+ text-align: center;
500
+ }
501
+
502
+ .severity-critical {
503
+ background: #ffebee;
504
+ color: #d32f2f;
505
+ }
506
+
507
+ .severity-high {
508
+ background: #fff3e0;
509
+ color: #f57c00;
510
+ }
511
+
512
+ .severity-medium {
513
+ background: #fff9c4;
514
+ color: #f57f17;
515
+ }
516
+
517
+ .severity-low {
518
+ background: #e8f5e9;
519
+ color: #388e3c;
520
+ }
521
+
522
+ .finding-content {
523
+ flex: 1;
524
+ }
525
+
526
+ .finding-title {
527
+ font-weight: bold;
528
+ color: var(--text-primary);
529
+ margin-bottom: 5px;
530
+ }
531
+
532
+ .finding-location {
533
+ font-size: 0.9em;
534
+ color: #666;
535
+ margin-bottom: 5px;
536
+ font-family: monospace;
537
+ background: #f5f5f5;
538
+ padding: 2px 6px;
539
+ border-radius: 3px;
540
+ display: inline-block;
541
+ }
542
+
543
+ .finding-description {
544
+ font-size: 0.95em;
545
+ color: #555;
546
+ margin-bottom: 10px;
547
+ }
548
+
549
+ .remediation {
550
+ background: #f0f7ff;
551
+ border-left: 3px solid #2196f3;
552
+ padding: 10px 15px;
553
+ border-radius: 4px;
554
+ font-size: 0.95em;
555
+ color: #1565c0;
556
+ }
557
+
558
+ .remediation-steps {
559
+ list-style: none;
560
+ padding: 0 0 0 20px;
561
+ }
562
+
563
+ .remediation-steps li {
564
+ margin: 5px 0;
565
+ position: relative;
566
+ }
567
+
568
+ .remediation-steps li:before {
569
+ content: "✓";
570
+ position: absolute;
571
+ left: -20px;
572
+ color: #4caf50;
573
+ font-weight: bold;
574
+ }
575
+
576
+ .metrics-grid {
577
+ display: grid;
578
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
579
+ gap: 15px;
580
+ margin: 20px 0;
581
+ }
582
+
583
+ .metric-card {
584
+ background: white;
585
+ padding: 15px;
586
+ border-radius: 8px;
587
+ border: 1px solid #e0e0e0;
588
+ }
589
+
590
+ .metric-label {
591
+ font-size: 0.75em;
592
+ color: #666;
593
+ margin-bottom: 8px;
594
+ }
595
+
596
+ .metric-value {
597
+ font-size: 0.95em;
598
+ font-weight: bold;
599
+ color: #333;
600
+ word-break: break-word;
601
+ }
602
+
603
+ .owasp-list {
604
+ display: grid;
605
+ gap: 15px;
606
+ margin: 20px 0;
607
+ }
608
+
609
+ .owasp-item {
610
+ background: white;
611
+ padding: 15px;
612
+ border-radius: 8px;
613
+ border-left: 5px solid #ff6b6b;
614
+ }
615
+
616
+ .owasp-title {
617
+ font-weight: bold;
618
+ margin-bottom: 8px;
619
+ font-size: 1.05em;
620
+ }
621
+
622
+ .owasp-rank {
623
+ display: inline-block;
624
+ background: #ff6b6b;
625
+ color: white;
626
+ padding: 2px 8px;
627
+ border-radius: 4px;
628
+ font-size: 0.85em;
629
+ margin-right: 10px;
630
+ }
631
+
632
+ .recommendations {
633
+ background: var(--bg-surface);
634
+ border: 1px solid var(--border);
635
+ border-radius: 8px;
636
+ overflow: hidden;
637
+ margin: 20px 0;
638
+ }
639
+
640
+ .recommendation-item {
641
+ padding: 12px 15px;
642
+ border-bottom: 1px solid var(--border);
643
+ display: flex;
644
+ gap: 10px;
645
+ }
646
+
647
+ .recommendation-item:last-child {
648
+ border-bottom: none;
649
+ }
650
+
651
+ .recommendation-priority {
652
+ padding: 2px 8px;
653
+ border-radius: 4px;
654
+ font-size: 0.85em;
655
+ font-weight: bold;
656
+ min-width: 70px;
657
+ }
658
+
659
+ .priority-critical {
660
+ background: #ffcdd2;
661
+ color: #c62828;
662
+ }
663
+
664
+ .priority-high {
665
+ background: #ffe0b2;
666
+ color: #e65100;
667
+ }
668
+
669
+ .priority-medium {
670
+ background: #fff9c4;
671
+ color: #f57f17;
672
+ }
673
+
674
+ .priority-low {
675
+ background: #c8e6c9;
676
+ color: #2e7d32;
677
+ }
678
+
679
+ .action-buttons {
680
+ display: flex;
681
+ gap: 16px;
682
+ margin-top: 36px;
683
+ justify-content: center;
684
+ flex-wrap: wrap;
685
+ }
686
+
687
+ .btn {
688
+ padding: 14px 28px;
689
+ border: none;
690
+ border-radius: 14px;
691
+ font-size: 1rem;
692
+ font-weight: 600;
693
+ font-family: inherit;
694
+ cursor: pointer;
695
+ transition: transform 0.2s, box-shadow 0.2s;
696
+ }
697
+
698
+ .btn:hover {
699
+ transform: translateY(-2px);
700
+ }
701
+
702
+ .btn-primary {
703
+ background: linear-gradient(135deg, #E6006C 0%, #8A4FDC 100%);
704
+ color: white;
705
+ box-shadow: 0 4px 14px rgba(230, 0, 108, 0.35);
706
+ }
707
+
708
+ .btn-primary:hover {
709
+ box-shadow: 0 8px 24px rgba(230, 0, 108, 0.4);
710
+ }
711
+
712
+ .btn-secondary {
713
+ background: var(--bg-elevated);
714
+ color: var(--text-primary);
715
+ border: 2px solid var(--border);
716
+ }
717
+
718
+ .btn-secondary:hover {
719
+ background: var(--border);
720
+ }
721
+
722
+ #errorDiv {
723
+ display: none;
724
+ background: #fef2f2;
725
+ color: #b91c1c;
726
+ padding: 16px 20px;
727
+ border-radius: 14px;
728
+ margin-bottom: 24px;
729
+ border: 1px solid #fecaca;
730
+ font-weight: 500;
731
+ }
732
+
733
+ .error-close {
734
+ float: right;
735
+ cursor: pointer;
736
+ font-weight: bold;
737
+ font-size: 1.25rem;
738
+ line-height: 1;
739
+ opacity: 0.8;
740
+ }
741
+ /* SonarCloud colored dashboard */
742
+ .sonar-dashboard {
743
+ display: grid;
744
+ gap: 24px;
745
+ }
746
+ .sonar-kpi-row {
747
+ display: grid;
748
+ grid-template-columns: repeat(3, 1fr);
749
+ gap: 16px;
750
+ width: 100%;
751
+ }
752
+ .sonar-kpi {
753
+ padding: 20px;
754
+ border-radius: 12px;
755
+ color: white;
756
+ text-align: center;
757
+ box-shadow: 0 4px 14px rgba(0,0,0,0.12);
758
+ }
759
+ .sonar-kpi .kpi-value { font-size: 2em; font-weight: 800; line-height: 1.2; }
760
+ .sonar-kpi .kpi-label { font-size: 0.85em; opacity: 0.95; margin-top: 6px; }
761
+ .sonar-kpi.score { background: linear-gradient(135deg, #E6006C 0%, #8A4FDC 100%); }
762
+ .sonar-kpi.gate-ok { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
763
+ .sonar-kpi.gate-error { background: linear-gradient(135deg, #eb3349 0%, #f45c43 100%); }
764
+ .sonar-kpi.gate-none { background: linear-gradient(135deg, #475569 0%, #64748b 100%); color: white; }
765
+ .sonar-kpi.loc { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
766
+ .sonar-kpi.issues { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
767
+ .sonar-charts-row {
768
+ display: grid;
769
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
770
+ gap: 24px;
771
+ }
772
+ .sonar-chart-card {
773
+ background: var(--bg-surface);
774
+ border-radius: 12px;
775
+ padding: 20px;
776
+ box-shadow: 0 2px 12px var(--shadow);
777
+ border: 1px solid var(--border);
778
+ }
779
+ .sonar-chart-card h4 { margin-bottom: 16px; color: var(--text-secondary); font-size: 1em; }
780
+ .sonar-chart-card canvas { max-height: 220px; }
781
+ .sonar-metrics-tiles {
782
+ display: grid;
783
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
784
+ gap: 12px;
785
+ width: 100%;
786
+ }
787
+ .sonar-tile {
788
+ padding: 14px;
789
+ border-radius: 10px;
790
+ text-align: center;
791
+ font-weight: 600;
792
+ color: var(--text-primary);
793
+ border: none;
794
+ }
795
+ .sonar-tile.loc { background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); }
796
+ .sonar-tile.bugs { background: linear-gradient(135deg, #fecaca 0%, #fca5a5 100%); }
797
+ .sonar-tile.vuln { background: linear-gradient(135deg, #fed7aa 0%, #fdba74 100%); }
798
+ .sonar-tile.smells { background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); }
799
+ .sonar-tile.hotspots { background: linear-gradient(135deg, #e9d5ff 0%, #d8b4fe 100%); }
800
+ .sonar-tile.dup { background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%); }
801
+ .sonar-tile.cov { background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%); }
802
+ .sonar-tile.complexity { background: linear-gradient(135deg, #fce7f3 0%, #fbcfe8 100%); }
803
+ .sonar-tile.overall { background: linear-gradient(135deg, #E6006C 0%, #8A4FDC 100%); color: white; }
804
+ .sonar-tile.overall .tile-label { color: rgba(255,255,255,0.95); }
805
+ .sonar-tile .tile-label { font-size: 0.75em; color: var(--text-secondary); font-weight: 500; margin-bottom: 4px; }
806
+ .sonar-tile .tile-value { font-size: 1.25em; }
807
+ html.theme-dark .gemini-dashboard .sonar-tile,
808
+ html.theme-dark .openai-dashboard .sonar-tile {
809
+ color: #0f172a;
810
+ border: 1px solid rgba(0,0,0,0.08);
811
+ }
812
+ html.theme-dark .gemini-dashboard .sonar-tile .tile-label,
813
+ html.theme-dark .openai-dashboard .sonar-tile .tile-label { color: #1e293b; font-weight: 600; }
814
+ html.theme-dark .gemini-dashboard .sonar-tile .tile-value,
815
+ html.theme-dark .openai-dashboard .sonar-tile .tile-value { color: #0f172a; font-weight: 800; }
816
+ html.theme-dark .gemini-dashboard .sonar-tile.overall,
817
+ html.theme-dark .openai-dashboard .sonar-tile.overall { color: #fff; border-color: rgba(255,255,255,0.2); }
818
+ html.theme-dark .gemini-dashboard .sonar-tile.overall .tile-label,
819
+ html.theme-dark .openai-dashboard .sonar-tile.overall .tile-label,
820
+ html.theme-dark .gemini-dashboard .sonar-tile.overall .tile-value,
821
+ html.theme-dark .openai-dashboard .sonar-tile.overall .tile-value { color: #fff; }
822
+
823
+ .feature-highlights {
824
+ display: grid;
825
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
826
+ gap: 20px;
827
+ margin-top: 32px;
828
+ padding-top: 32px;
829
+ border-top: 1px solid var(--border);
830
+ }
831
+ .feature-item {
832
+ display: flex;
833
+ align-items: flex-start;
834
+ gap: 12px;
835
+ }
836
+ .feature-item .icon { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg, #fce7f3 0%, #f5d0fe 100%); color: #8A4FDC; font-size: 1.25rem; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
837
+ .feature-item .text { font-size: 0.9rem; color: var(--text-secondary); line-height: 1.5; }
838
+ .feature-item .text strong { color: var(--text-primary); display: block; margin-bottom: 2px; font-size: 0.95rem; }
839
+
840
+ .results-bar {
841
+ display: flex;
842
+ flex-wrap: wrap;
843
+ align-items: center;
844
+ justify-content: space-between;
845
+ gap: 16px;
846
+ margin-bottom: 24px;
847
+ padding: 16px 20px;
848
+ background: var(--bg-elevated);
849
+ border-radius: 14px;
850
+ border: 1px solid var(--border);
851
+ }
852
+ .results-bar .repo-name { font-size: 1.25rem; font-weight: 700; color: var(--text-primary); font-family: ui-monospace, monospace; }
853
+ .results-bar .meta { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
854
+ .results-bar .timestamp { font-size: 0.85rem; color: var(--text-muted); }
855
+ .results-bar .copy-btn {
856
+ padding: 8px 16px;
857
+ border-radius: 10px;
858
+ border: 1px solid var(--border);
859
+ background: var(--bg-surface);
860
+ color: var(--text-secondary);
861
+ font-size: 0.875rem;
862
+ font-weight: 600;
863
+ cursor: pointer;
864
+ font-family: inherit;
865
+ transition: background 0.2s, color 0.2s;
866
+ }
867
+ .results-bar .copy-btn:hover { background: var(--border); color: var(--text-primary); }
868
+ .results-bar .copy-btn.copied { background: #ecfdf5; color: #059669; border-color: #a7f3d0; }
869
+
870
+ .new-repo-bar {
871
+ display: flex;
872
+ flex-wrap: wrap;
873
+ gap: 12px;
874
+ align-items: center;
875
+ margin-bottom: 20px;
876
+ padding: 16px 20px;
877
+ background: var(--bg-surface);
878
+ border-radius: 12px;
879
+ border: 1px solid var(--border);
880
+ box-shadow: 0 1px 3px var(--shadow);
881
+ }
882
+ .new-repo-bar .analysis-type-group {
883
+ margin-bottom: 0;
884
+ }
885
+ .new-repo-bar input {
886
+ flex: 1;
887
+ min-width: 180px;
888
+ padding: 10px 14px;
889
+ border: 1px solid var(--border);
890
+ border-radius: 8px;
891
+ font-size: 0.9rem;
892
+ font-family: inherit;
893
+ background: var(--bg-elevated);
894
+ color: var(--text-primary);
895
+ }
896
+ .new-repo-bar input:focus {
897
+ outline: none;
898
+ border-color: var(--accent-to);
899
+ background: var(--bg-surface);
900
+ box-shadow: 0 0 0 2px rgba(138, 79, 220, 0.15);
901
+ }
902
+ .new-repo-bar input::placeholder { color: var(--text-muted); }
903
+ .new-repo-bar .btn-analyze-cta {
904
+ padding: 10px 22px;
905
+ background: linear-gradient(135deg, #E6006C 0%, #8A4FDC 100%);
906
+ color: white;
907
+ border: none;
908
+ border-radius: 8px;
909
+ font-size: 0.9rem;
910
+ font-weight: 700;
911
+ font-family: inherit;
912
+ cursor: pointer;
913
+ white-space: nowrap;
914
+ box-shadow: 0 2px 8px rgba(230, 0, 108, 0.25);
915
+ }
916
+ .new-repo-bar .btn-analyze-cta:hover:not(:disabled) {
917
+ opacity: 0.95;
918
+ transform: translateY(-1px);
919
+ }
920
+ .new-repo-bar .btn-analyze-cta:disabled {
921
+ opacity: 0.6;
922
+ cursor: not-allowed;
923
+ }
924
+
925
+ .app-footer {
926
+ text-align: center;
927
+ padding: 32px 24px 24px;
928
+ margin-top: 24px;
929
+ border-top: 1px solid #e5e7eb;
930
+ color: #94a3b8;
931
+ font-size: 0.875rem;
932
+ }
933
+ .app-footer a { color: #8A4FDC; text-decoration: none; font-weight: 500; }
934
+ .app-footer a:hover { text-decoration: underline; }
935
+ .app-footer .brand { font-weight: 600; color: var(--text-muted); margin-bottom: 8px; }
936
+ .rws-logo {
937
+ display: inline-flex;
938
+ align-items: center;
939
+ gap: 6px;
940
+ vertical-align: middle;
941
+ }
942
+ .rws-logo .rws-icon {
943
+ width: 24px;
944
+ height: 18px;
945
+ flex-shrink: 0;
946
+ }
947
+ .rws-logo .rws-text {
948
+ font-size: 0.95em;
949
+ font-weight: 700;
950
+ text-transform: lowercase;
951
+ color: var(--text-primary);
952
+ letter-spacing: 0.02em;
953
+ }
954
+
955
+ .toast {
956
+ position: fixed;
957
+ bottom: 24px;
958
+ left: 50%;
959
+ transform: translateX(-50%) translateY(80px);
960
+ padding: 14px 24px;
961
+ background: linear-gradient(135deg, #6A1B9A 0%, #8A4FDC 100%);
962
+ color: white;
963
+ border-radius: 12px;
964
+ font-size: 0.9rem;
965
+ font-weight: 500;
966
+ box-shadow: 0 10px 40px rgba(0,0,0,0.3);
967
+ z-index: 1000;
968
+ opacity: 0;
969
+ transition: transform 0.3s, opacity 0.3s;
970
+ }
971
+ .toast.show { transform: translateX(-50%) translateY(0); opacity: 1; }
972
+
973
+ .analysis-type-group {
974
+ display: inline-flex;
975
+ align-items: stretch;
976
+ margin-bottom: 16px;
977
+ border-radius: 10px;
978
+ border: 1px solid var(--border);
979
+ background: var(--bg-elevated);
980
+ padding: 3px;
981
+ gap: 0;
982
+ }
983
+ .analysis-type-group > span.label {
984
+ display: none;
985
+ align-self: center;
986
+ margin-right: 12px;
987
+ margin-left: 4px;
988
+ font-size: 0.9rem;
989
+ font-weight: 600;
990
+ color: var(--text-muted);
991
+ }
992
+ .input-section .analysis-type-group {
993
+ display: inline-flex;
994
+ flex-wrap: wrap;
995
+ }
996
+ .input-section .analysis-type-group > span.label {
997
+ display: inline;
998
+ }
999
+ .analysis-type-btn {
1000
+ padding: 8px 18px;
1001
+ border: none;
1002
+ border-radius: 8px;
1003
+ background: transparent;
1004
+ font-size: 0.9rem;
1005
+ font-weight: 600;
1006
+ font-family: inherit;
1007
+ cursor: pointer;
1008
+ color: var(--text-muted);
1009
+ transition: background 0.2s, color 0.2s, box-shadow 0.2s;
1010
+ }
1011
+ .analysis-type-btn:hover:not(.active) {
1012
+ color: var(--text-secondary);
1013
+ background: var(--border);
1014
+ }
1015
+ .analysis-type-btn.active {
1016
+ background: var(--bg-surface);
1017
+ color: #6A1B9A;
1018
+ box-shadow: 0 1px 3px var(--shadow);
1019
+ }
1020
+ .analysis-type-btn .analysis-badge {
1021
+ margin-left: 6px;
1022
+ padding: 1px 6px;
1023
+ font-size: 0.65rem;
1024
+ font-weight: 600;
1025
+ border-radius: 6px;
1026
+ background: #e0e7ff;
1027
+ color: #4338ca;
1028
+ }
1029
+ .analysis-type-btn.active .analysis-badge {
1030
+ background: #c7d2fe;
1031
+ color: #4338ca;
1032
+ }
1033
+ .gemini-quota-hint {
1034
+ font-size: 0.8rem;
1035
+ color: var(--text-muted);
1036
+ margin: -4px 0 12px 0;
1037
+ }
1038
+ .gemini-content {
1039
+ background: var(--bg-elevated);
1040
+ border: 1px solid var(--border);
1041
+ border-radius: 12px;
1042
+ padding: 20px;
1043
+ white-space: pre-wrap;
1044
+ text-align: left;
1045
+ font-size: 0.95rem;
1046
+ line-height: 1.6;
1047
+ color: var(--text-primary);
1048
+ max-height: 70vh;
1049
+ overflow-y: auto;
1050
+ }
1051
+ .gemini-content.openai-report { white-space: normal; }
1052
+ .gemini-dashboard { margin-bottom: 20px; width: 100%; }
1053
+ .openai-dashboard {
1054
+ margin-bottom: 20px;
1055
+ width: 100%;
1056
+ }
1057
+ .openai-category-scores-full {
1058
+ width: 100%;
1059
+ }
1060
+ .openai-category-scores-full h4 {
1061
+ margin: 0 0 12px 0;
1062
+ font-size: 1rem;
1063
+ color: var(--text-secondary);
1064
+ }
1065
+ .openai-chart-card {
1066
+ background: var(--bg-surface);
1067
+ border: 1px solid var(--border);
1068
+ border-radius: 12px;
1069
+ padding: 20px;
1070
+ box-shadow: 0 1px 3px var(--shadow);
1071
+ }
1072
+ .openai-chart-card h4 {
1073
+ margin: 0 0 16px 0;
1074
+ font-size: 1rem;
1075
+ color: var(--text-secondary);
1076
+ }
1077
+ .openai-chart-card canvas { max-height: 260px; }
1078
+ .openai-ratings-tiles {
1079
+ display: grid;
1080
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
1081
+ gap: 10px;
1082
+ }
1083
+ .openai-tile {
1084
+ background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
1085
+ border: 1px solid #bae6fd;
1086
+ border-radius: 10px;
1087
+ padding: 12px;
1088
+ text-align: center;
1089
+ }
1090
+ .openai-tile .label { font-size: 0.75rem; color: #0369a1; font-weight: 600; text-transform: capitalize; display: block; margin-bottom: 4px; }
1091
+ .openai-tile .value { font-size: 1.5rem; font-weight: 800; color: #0c4a6e; }
1092
+ .openai-tile.rating-5 .value { color: #059669; }
1093
+ .openai-tile.rating-4 .value { color: #0284c7; }
1094
+ .openai-tile.rating-3 .value { color: #d97706; }
1095
+ .openai-tile.rating-2 .value { color: #dc2626; }
1096
+ .openai-tile.rating-1 .value { color: #991b1b; }
1097
+ .openai-report {
1098
+ background: var(--bg-surface);
1099
+ border: 1px solid var(--border);
1100
+ border-radius: 12px;
1101
+ padding: 24px 28px;
1102
+ font-size: 0.925rem;
1103
+ line-height: 1.75;
1104
+ color: var(--text-primary);
1105
+ max-height: 60vh;
1106
+ overflow-y: auto;
1107
+ }
1108
+ .openai-report .md-h2 {
1109
+ font-size: 1.05rem;
1110
+ font-weight: 700;
1111
+ color: #1e293b;
1112
+ margin: 1.5em 0 0.5em 0;
1113
+ padding-bottom: 6px;
1114
+ border-bottom: 2px solid #e2e8f0;
1115
+ }
1116
+ .openai-report .md-h2:first-child { margin-top: 0; }
1117
+ .openai-report .md-p { margin: 0.5em 0 1em 0; }
1118
+ .openai-report .md-list-item {
1119
+ padding-left: 1.2em;
1120
+ margin: 0.35em 0;
1121
+ position: relative;
1122
+ }
1123
+ .openai-report .md-list-item::before {
1124
+ content: '•';
1125
+ position: absolute;
1126
+ left: 0;
1127
+ color: #64748b;
1128
+ font-weight: 700;
1129
+ }
1130
+ .openai-report .md-strong { font-weight: 700; color: var(--text-primary); }
1131
+ /* OpenAI-only: red = required actions, yellow = warning, green = good */
1132
+ .openai-report .report-good,
1133
+ .openai-report .md-h2.report-good { color: #059669; }
1134
+ .openai-report .report-good .md-strong { color: #047857; }
1135
+ .openai-report .report-warning,
1136
+ .openai-report .md-h2.report-warning { color: #b45309; }
1137
+ .openai-report .report-warning .md-strong { color: #92400e; }
1138
+ .openai-report .report-action,
1139
+ .openai-report .md-h2.report-action { color: #dc2626; }
1140
+ .openai-report .report-action .md-strong { color: #b91c1c; }
1141
+ .merged-report-content .merged-model-body.report-color-coded .report-good,
1142
+ .merged-report-content .merged-model-body.report-color-coded .md-h2.report-good { color: #059669; }
1143
+ .merged-report-content .merged-model-body.report-color-coded .report-warning,
1144
+ .merged-report-content .merged-model-body.report-color-coded .md-h2.report-warning { color: #b45309; }
1145
+ .merged-report-content .merged-model-body.report-color-coded .report-action,
1146
+ .merged-report-content .merged-model-body.report-color-coded .md-h2.report-action { color: #dc2626; }
1147
+ .merged-report-content .merged-model-block { margin-bottom: 2em; padding-bottom: 1.5em; border-bottom: 1px solid var(--border-color, #e2e8f0); }
1148
+ .merged-report-content .merged-model-block:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; }
1149
+ .merged-report-content .merged-model-title { font-size: 1.1rem; margin: 0 0 12px 0; color: var(--text-primary); }
1150
+ .merged-report-content .merged-model-body { margin-top: 8px; }
1151
+ .merged-sonar-tiles { width: 100%; box-sizing: border-box; }
1152
+ .merged-sonar-tiles .sonar-tile { min-width: 0; }
1153
+ @media (max-width: 640px) {
1154
+ .merged-sonar-tiles { grid-template-columns: repeat(2, 1fr) !important; }
1155
+ }
1156
+ </style>
1157
+ </head>
1158
+ <body>
1159
+ <header class="top-bar" role="banner">
1160
+ <a href="#" class="top-bar-logo" aria-label="Repository Analyzer home">
1161
+ <img src="images/rws-logo.png" alt="rws" class="top-bar-logo-img" onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
1162
+ <svg width="40" height="40" viewBox="0 0 32 22" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="display: none;">
1163
+ <circle cx="10" cy="11" r="8" fill="#8A4FDC"/>
1164
+ <circle cx="22" cy="11" r="8" fill="#E6006C"/>
1165
+ </svg>
1166
+ <span>Repository Analyzer</span>
1167
+ </a>
1168
+ <nav class="top-bar-nav" aria-label="Account and theme">
1169
+ <button type="button" class="theme-toggle" id="themeToggle" onclick="toggleTheme()" aria-label="Toggle light/dark theme" title="Toggle theme">
1170
+ <span class="icon-light" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/></svg></span>
1171
+ <span class="icon-dark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg></span>
1172
+ <span id="themeToggleLabel">Dark</span>
1173
+ </button>
1174
+ <!-- Sign in/out options hidden for now -->
1175
+ <!-- <button type="button" class="top-bar-btn signin" id="topBarSignIn" onclick="handleSignIn()" style="display: none;">Sign in</button>
1176
+ <button type="button" class="top-bar-btn signout" id="topBarSignOut" onclick="handleSignOut()" style="display: none;">Sign out</button> -->
1177
+ </nav>
1178
+ </header>
1179
+ <div class="container">
1180
+ <div class="header">
1181
+ <h1>Repository Analyzer</h1>
1182
+ <p>Analyze any Git repository — code quality, metrics, and insights at a glance</p>
1183
+ </div>
1184
+
1185
+ <div id="errorDiv">
1186
+ <span class="error-close" onclick="hideError()">&times;</span>
1187
+ <span id="errorMessage"></span>
1188
+ </div>
1189
+
1190
+ <div class="input-section" id="inputSection">
1191
+ <p id="geminiQuotaHint" class="gemini-quota-hint" style="display: none;">Free tier: wait 1–2 minutes between Gemini analyses to avoid quota errors.</p>
1192
+ <div class="input-group">
1193
+ <input type="text" id="repoInput" placeholder="Enter repo URL or owner/repo (e.g. facebook/react)" aria-label="Repository URL or owner/repo" />
1194
+ <button type="button" id="analyzeBtn" onclick="analyzeRepository()" aria-label="Analyze repository">Analyze</button>
1195
+ </div>
1196
+ <div class="prompt-field">
1197
+ <div class="prompt-label-row">
1198
+ <span class="prompt-label">Analysis options</span>
1199
+ <span class="prompt-actions">
1200
+ <button type="button" class="link-btn" id="selectAllOptionsBtn" onclick="selectAllAnalysisOptions(true)" aria-label="Select all options">Select all</button>
1201
+ <span class="prompt-actions-sep" aria-hidden="true">|</span>
1202
+ <button type="button" class="link-btn" id="deselectAllOptionsBtn" onclick="selectAllAnalysisOptions(false)" aria-label="Deselect all options">Deselect all</button>
1203
+ </span>
1204
+ </div>
1205
+ <div class="prompt-options" id="analysisOptionsContainer"></div>
1206
+ </div>
1207
+ <div class="feature-highlights" id="featureHighlights">
1208
+ <div class="feature-item">
1209
+ <div class="icon" aria-hidden="true">📊</div>
1210
+ <div class="text"><strong>Quality metrics</strong>Score, quality gate, and SonarCloud data in one view.</div>
1211
+ </div>
1212
+ <div class="feature-item">
1213
+ <div class="icon" aria-hidden="true">📈</div>
1214
+ <div class="text"><strong>Charts & breakdown</strong>Issues, coverage, and severity at a glance.</div>
1215
+ </div>
1216
+ <div class="feature-item">
1217
+ <div class="icon" aria-hidden="true">📄</div>
1218
+ <div class="text"><strong>Export to PDF</strong>Download a full report for sharing or records.</div>
1219
+ </div>
1220
+ </div>
1221
+ </div>
1222
+
1223
+ <div id="loadingDiv">
1224
+ <div class="spinner"></div>
1225
+ <p id="loadingMessage">Analyzing repository…</p>
1226
+ </div>
1227
+
1228
+ <div id="resultsDiv">
1229
+ <div class="results-bar" id="resultsBar">
1230
+ <span class="repo-name" id="repoName"></span>
1231
+ <div class="meta">
1232
+ <span class="timestamp" id="resultsTimestamp"></span>
1233
+ <button type="button" class="copy-btn" id="copyRepoBtn" onclick="copyRepoToClipboard()">Copy repo</button>
1234
+ </div>
1235
+ </div>
1236
+
1237
+ <div class="scores-grid" id="summaryGrid"></div>
1238
+
1239
+ <!-- SonarCloud Analysis Section -->
1240
+ <div class="section" id="sonarSection">
1241
+ <h3><span class="section-icon">📊</span> Code Quality Analysis</h3>
1242
+ <div id="sonarCloudContent"></div>
1243
+ </div>
1244
+
1245
+ <!-- Gemini AI Analysis Section (shown when analysis mode is Gemini) -->
1246
+ <div class="section" id="geminiSection" style="display: none;">
1247
+ <h3><span class="section-icon">✨</span> Gemini AI Analysis</h3>
1248
+ <div id="geminiDashboard" class="gemini-dashboard" style="display: none;">
1249
+ <div class="openai-category-scores-full">
1250
+ <h4>Category scores</h4>
1251
+ <div id="geminiRatingsTiles" class="openai-ratings-tiles"></div>
1252
+ </div>
1253
+ </div>
1254
+ <div id="geminiContent" class="gemini-content"></div>
1255
+ </div>
1256
+
1257
+ <div class="section" id="openaiSection" style="display: none;">
1258
+ <h3><span class="section-icon">🤖</span> Open AI Analysis</h3>
1259
+ <div id="openaiDashboard" class="openai-dashboard" style="display: none;">
1260
+ <div class="openai-category-scores-full">
1261
+ <h4>Category scores</h4>
1262
+ <div id="openaiRatingsTiles" class="openai-ratings-tiles"></div>
1263
+ </div>
1264
+ </div>
1265
+ <div id="openaiContent" class="openai-report"></div>
1266
+ </div>
1267
+
1268
+ <!-- Analysis Summary (all models in one view when multiple selected) -->
1269
+ <div class="section" id="mergedReportSection" style="display: none;">
1270
+ <h3><span class="section-icon">📋</span> Analysis Summary</h3>
1271
+ <div id="mergedReportContent" class="merged-report-content openai-report"></div>
1272
+ </div>
1273
+
1274
+ <div class="action-buttons">
1275
+ <button class="btn btn-primary" onclick="goBack()">🔄 Analyze Another</button>
1276
+ <button class="btn btn-primary" id="pdfBtn" onclick="exportToPDF()">📄 Export to PDF</button>
1277
+ </div>
1278
+ </div>
1279
+
1280
+ <footer class="app-footer">
1281
+ <div class="brand">Repository Analyzer</div>
1282
+ <span>Powered by <span class="rws-logo" aria-label="rws"><svg class="rws-icon" viewBox="0 0 32 22" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><circle cx="10" cy="11" r="8" fill="#8A4FDC"/><circle cx="22" cy="11" r="8" fill="#E6006C"/></svg><span class="rws-text">rws</span></span></span>
1283
+ <span style="margin: 0 8px;">·</span>
1284
+ <span id="footerYear"></span>
1285
+ </footer>
1286
+ </div>
1287
+
1288
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
1289
+
1290
+ <script>
1291
+ document.getElementById('footerYear').textContent = new Date().getFullYear();
1292
+
1293
+ var SESSION_KEY = 'repoAnalyzerLoggedIn';
1294
+ function isLoggedIn() {
1295
+ return sessionStorage.getItem(SESSION_KEY) === 'true';
1296
+ }
1297
+ function setSessionLoggedIn(value) {
1298
+ sessionStorage.setItem(SESSION_KEY, value ? 'true' : 'false');
1299
+ updateTopBarAuth();
1300
+ }
1301
+ function updateTopBarAuth() {
1302
+ var signedIn = isLoggedIn();
1303
+ var signInBtn = document.getElementById('topBarSignIn');
1304
+ var signOutBtn = document.getElementById('topBarSignOut');
1305
+ if (signInBtn) signInBtn.style.display = signedIn ? 'none' : 'inline-block';
1306
+ if (signOutBtn) signOutBtn.style.display = signedIn ? 'inline-block' : 'none';
1307
+ }
1308
+ function handleSignIn() {
1309
+ setSessionLoggedIn(true);
1310
+ }
1311
+ function handleSignOut() {
1312
+ setSessionLoggedIn(false);
1313
+ }
1314
+ updateTopBarAuth();
1315
+
1316
+ var THEME_KEY = 'repoAnalyzerTheme';
1317
+ function getTheme() {
1318
+ var saved = localStorage.getItem(THEME_KEY);
1319
+ if (saved === 'dark' || saved === 'light') return saved;
1320
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) return 'dark';
1321
+ return 'light';
1322
+ }
1323
+ function setTheme(theme) {
1324
+ theme = theme === 'dark' ? 'dark' : 'light';
1325
+ localStorage.setItem(THEME_KEY, theme);
1326
+ document.documentElement.classList.toggle('theme-dark', theme === 'dark');
1327
+ var label = document.getElementById('themeToggleLabel');
1328
+ if (label) label.textContent = theme === 'dark' ? 'Light' : 'Dark';
1329
+ }
1330
+ function toggleTheme() {
1331
+ setTheme(document.documentElement.classList.contains('theme-dark') ? 'light' : 'dark');
1332
+ }
1333
+ setTheme(getTheme());
1334
+
1335
+ var ANALYSIS_OPTIONS = [];
1336
+
1337
+ function getOptionByKey(key) {
1338
+ return ANALYSIS_OPTIONS.find(function (o) { return o.key === key; }) || null;
1339
+ }
1340
+
1341
+ function renderAnalysisCheckboxes() {
1342
+ var container = document.getElementById('analysisOptionsContainer');
1343
+ if (!container) return;
1344
+ container.innerHTML = ANALYSIS_OPTIONS.map(function (o) {
1345
+ var id = 'opt_' + o.key;
1346
+ var modelAttr = (o.modelName != null && o.modelName !== '') ? ' data-model="' + String(o.modelName).replace(/"/g, '&quot;') + '"' : '';
1347
+ var hint = (o.hint != null && String(o.hint).trim() !== '') ? String(o.hint).trim() : '';
1348
+ var labelClass = hint ? 'prompt-option prompt-option-with-hint' : 'prompt-option';
1349
+ var hintHtml = hint ? '<div class="option-hint" aria-describedby="' + id + '">' + hint + '</div>' : '';
1350
+ return '<label class="' + labelClass + '"><input type="checkbox" name="' + o.key + '" value="' + o.key + '" id="' + id + '"' + modelAttr + ' /> ' + (o.prompt || o.value) + '</label>' + hintHtml;
1351
+ }).join('');
1352
+ }
1353
+
1354
+ function loadAnalysisOptionsFromJson() {
1355
+ fetch('/analysis-options.json')
1356
+ .then(function (res) { return res.json(); })
1357
+ .then(function (rows) {
1358
+ var seen = {};
1359
+ ANALYSIS_OPTIONS = rows.filter(function (r) {
1360
+ if (seen[r.key]) return false;
1361
+ seen[r.key] = true;
1362
+ return true;
1363
+ }).map(function (r) { return { key: r.key, prompt: r.value, modelName: r.modelName, hint: r.hint }; });
1364
+ renderAnalysisCheckboxes();
1365
+ })
1366
+ .catch(function () {
1367
+ var fallback = [
1368
+ { "key": "architecture", "value" : "Architecture Review", "modelName": "openai" },
1369
+ { "key": "folder_structure", "value" : "Folder Structure Review", "modelName": "openai" },
1370
+ { "key": "code_smells", "value" : "Code Smells", "modelName": "openai" },
1371
+ { "key": "improvement_suggestions", "value" : "Improvement Suggestions", "modelName": "openai" },
1372
+ { "key": "deep_logic", "value" : "Deep Logic Analysis", "modelName": "openai" },
1373
+ { "key": "best_practices", "value" : "Best Practices Validation", "modelName": "openai" },
1374
+ { "key": "performance Review & Bottlenecks", "value" : "Performance Review & Bottlenecks", "modelName": "openai" },
1375
+ { "key": "dependencies", "value" : "Dependency Analysis", "modelName": "openai" },
1376
+ { "key": "documentation", "value" : "Documentation Quality Review", "modelName": "openai" },
1377
+ { "key": "prioritized_actions", "value" : "Prioritized Action Items", "modelName": "openai" },
1378
+ { "key": "code_quality", "value" : "Code Quality Summary", "modelName": "openai" },
1379
+ { "key": "security", "value" : "Security Review (AI Deep Scan)", "modelName": "openai" },
1380
+ { "key": "complexity", "value" : "Code Complexity Analysis", "modelName": "openai" },
1381
+ { "key": "test_coverage", "value" : "Test Coverage & Quality Review", "modelName": "openai" },
1382
+ { "key": "dead_code", "value" : "Dead Code & Unused Assets", "modelName": "openai" },
1383
+ { "key": "architecture_smells", "value" : "Architecture Smells Detection", "modelName": "openai" },
1384
+ { "key": "logging", "value" : "Error Handling & Logging Review", "modelName": "openai" },
1385
+ { "key": "db_queries", "value" : "Database Query Efficiency Review", "modelName": "openai" },
1386
+ { "key": "cloud_readiness", "value" : "Cloud & Deployment Readiness", "modelName": "openai" },
1387
+ { "key": "style_consistency", "value" : "Code Style Consistency", "modelName": "openai" },
1388
+ { "key": "maintainability", "value" : "Maintainability & Readability", "modelName": "openai" },
1389
+ { "key": "endpoint_review", "value" : "API Endpoint Quality Review", "modelName": "openai" },
1390
+ { "key": "config_review", "value" : "Configuration & Environment Review", "modelName": "openai" },
1391
+ { "key": "security_issues", "value" : "Security Issues (Static Analysis)", "modelName": "sonar" },
1392
+ { "key": "performance_issues", "value" : "Performance Issues (Static Analysis)", "modelName": "sonar" },
1393
+ { "key": "api_usage_review", "value" : "API Usage Review (Static Analysis)", "modelName": "sonar" },
1394
+ { "key": "risk_score", "value" : "Overall Risk Scoring", "modelName": "openai" },
1395
+ { "key": "summary", "value" : "Management Summary (High-Level)", "modelName": "openai" }
1396
+ ];
1397
+ ANALYSIS_OPTIONS = fallback.map(function (r) { return { key: r.key, prompt: r.value, modelName: r.modelName, hint: r.hint }; });
1398
+ renderAnalysisCheckboxes();
1399
+ });
1400
+ }
1401
+
1402
+ function getSelectedAnalysisOptions() {
1403
+ var selected = [];
1404
+ ANALYSIS_OPTIONS.forEach(function (o) {
1405
+ var el = document.getElementById('opt_' + o.key);
1406
+ if (el && el.checked) selected.push(o.key);
1407
+ });
1408
+ var matched = selected.map(function (key) {
1409
+ var opt = getOptionByKey(key);
1410
+ return opt ? { key: opt.key, prompt: opt.prompt, modelName: opt.modelName } : null;
1411
+ }).filter(Boolean);
1412
+ return matched;
1413
+ }
1414
+
1415
+ function selectAllAnalysisOptions(checked) {
1416
+ var container = document.getElementById('analysisOptionsContainer');
1417
+ if (!container) return;
1418
+ container.querySelectorAll('input[type="checkbox"]').forEach(function (cb) { cb.checked = !!checked; });
1419
+ }
1420
+
1421
+ function getSelectedValuesForApi() {
1422
+ var selected = getSelectedAnalysisOptions();
1423
+ return selected.map(function (o) { return o.prompt; });
1424
+ }
1425
+
1426
+ function getAnalysisPromptFromCheckboxes() {
1427
+ var selected = getSelectedAnalysisOptions();
1428
+ var list = selected.map(function (o) { return '- ' + o.prompt; }).join('\n');
1429
+ return 'You are a senior software engineer. Analyze the following GitHub repository code for:\n' + list + '\n\nBe concise and actionable.\n\nRepository: {{REPO_INFO}}\nFiles analyzed: {{FILE_LIST}}\n\nCode and config excerpts:\n{{CODE}}';
1430
+ }
1431
+
1432
+ /** Group selected options by modelName. Options with modelName "openai" are sent to both OpenAI and Gemini. Returns { sonar: [], gemini: [], openai: [] }. */
1433
+ function getSelectedOptionsGroupedByModel() {
1434
+ var selected = getSelectedAnalysisOptions();
1435
+ var byModel = { sonar: [], gemini: [], openai: [] };
1436
+ selected.forEach(function (o) {
1437
+ var m = (o.modelName || '').toLowerCase().trim();
1438
+ if (m === 'sonar') byModel.sonar.push(o);
1439
+ else if (m === 'openai' || m === 'gemini') {
1440
+ byModel.openai.push(o);
1441
+ byModel.gemini.push(o);
1442
+ }
1443
+ });
1444
+ return byModel;
1445
+ }
1446
+ function buildAccessibilityPrompt(repoInfo) {
1447
+ return `
1448
+ You are an expert in Web Accessibility and WCAG 2.1 AA compliance.
1449
+
1450
+ Analyze the UI layer of the following repository:
1451
+
1452
+ ${repoInfo}
1453
+
1454
+ The repository may contain:
1455
+ - React (JSX / TSX)
1456
+ - .NET (Razor / Blazor / ASPX)
1457
+ - Java (JSP / Thymeleaf)
1458
+
1459
+ Perform Accessibility Compliance analysis focusing on:
1460
+
1461
+ 1. HTML Semantics
1462
+ - Proper use of semantic tags (header, nav, main, section, footer)
1463
+ - Avoid misuse of div/span
1464
+
1465
+ 2. ARIA Roles
1466
+ - Correct use of ARIA roles and attributes
1467
+ - Avoid redundant or incorrect ARIA
1468
+
1469
+ 3. Forms & Labels
1470
+ - All inputs must have labels
1471
+ - Proper association (for/id)
1472
+ - Accessibility for screen readers
1473
+
1474
+ 4. Accessibility Issues
1475
+ - Missing alt text in images
1476
+ - Keyboard accessibility issues
1477
+ - Focus handling problems
1478
+ - Button vs div misuse
1479
+ - Link accessibility
1480
+ - Color contrast (if identifiable)
1481
+
1482
+ Return STRICT JSON:
1483
+
1484
+ {
1485
+ "score": number (0-100),
1486
+ "summary": "short summary",
1487
+ "issues": [
1488
+ {
1489
+ "severity": "high | medium | low",
1490
+ "category": "semantic | aria | forms | keyboard | contrast",
1491
+ "issue": "description",
1492
+ "file": "file path if possible",
1493
+ "recommendation": "fix suggestion"
1494
+ }
1495
+ ]
1496
+ }
1497
+ `;
1498
+ }
1499
+ /** Build full analysis prompt for a list of options (e.g. for one model). Options: [{ key, prompt }]. */
1500
+ function buildPromptForModel(options) {
1501
+ if (!options || options.length === 0) return '';
1502
+ var list = options.map(function (o) { return '- ' + o.prompt; }).join('\n');
1503
+ var hasRiskScore = options.some(function (o) { return o.key === 'risk_score'; });
1504
+ var extra = '';
1505
+ var hasAccessibility = options.some(o => o.key === 'accessibility');
1506
+ if (hasAccessibility) {
1507
+ return buildAccessibilityPrompt("{{REPO_INFO}}");
1508
+ }
1509
+ if (hasRiskScore && options.length > 1) {
1510
+ var otherNames = options.filter(function (o) { return o.key !== 'risk_score'; }).map(function (o) { return o.prompt; });
1511
+ extra = '\n\nFor Overall Risk Scoring: first give a risk score (1-5 or Low/Medium/High) for each of the other selected areas, using the exact same names so they can be matched (e.g. "Folder Structure Review: 2", "Security Review (AI Deep Scan): 3"). Then provide the detailed analysis for each area.';
1512
+ }
1513
+ return 'You are a senior software engineer. Analyze the following GitHub repository code for:\n' + list + '\n\nBe concise and actionable.' + extra + '\n\nRepository: {{REPO_INFO}}\nFiles analyzed: {{FILE_LIST}}\n\nCode and config excerpts:\n{{CODE}}';
1514
+ }
1515
+
1516
+ loadAnalysisOptionsFromJson();
1517
+
1518
+ document.getElementById('repoInput').addEventListener('keydown', function (e) {
1519
+ if (e.key === 'Enter') { e.preventDefault(); analyzeRepository(); }
1520
+ });
1521
+
1522
+ let lastAnalysisData = null;
1523
+ window.analysisMode = 'sonar'; // 'sonar' | 'gemini' | 'openai'
1524
+ let geminiCooldownEnd = 0;
1525
+ let geminiCooldownTimer = null;
1526
+
1527
+ function startGeminiCooldown(seconds) {
1528
+ geminiCooldownEnd = Date.now() + seconds * 1000;
1529
+ if (geminiCooldownTimer) clearInterval(geminiCooldownTimer);
1530
+ function tick() {
1531
+ const left = Math.ceil((geminiCooldownEnd - Date.now()) / 1000);
1532
+ if (left <= 0 || getAnalysisMode() !== 'gemini') {
1533
+ clearInterval(geminiCooldownTimer);
1534
+ geminiCooldownTimer = null;
1535
+ setAnalyzeButtonState(true, 'Analyze');
1536
+ return;
1537
+ }
1538
+ const m = Math.floor(left / 60);
1539
+ const s = left % 60;
1540
+ setAnalyzeButtonState(false, 'Retry in ' + m + ':' + (s < 10 ? '0' : '') + s);
1541
+ }
1542
+ tick();
1543
+ geminiCooldownTimer = setInterval(tick, 1000);
1544
+ }
1545
+
1546
+ function setAnalyzeButtonState(enabled, label) {
1547
+ var btn = document.getElementById('analyzeBtn');
1548
+ if (btn) {
1549
+ btn.disabled = !enabled;
1550
+ btn.textContent = label;
1551
+ }
1552
+ }
1553
+
1554
+ function setAnalysisMode(mode) {
1555
+ window.analysisMode = mode;
1556
+ const isSonar = mode === 'sonar';
1557
+ ['btnAnalysisSonar', 'btnAnalysisGemini', 'btnAnalysisOpenAI'].forEach(function (id) {
1558
+ const el = document.getElementById(id);
1559
+ if (!el) return;
1560
+ const active =
1561
+ (id.includes('Sonar') && mode === 'sonar') ||
1562
+ (id.includes('Gemini') && mode === 'gemini') ||
1563
+ (id.includes('OpenAI') && mode === 'openai');
1564
+ el.classList.toggle('active', active);
1565
+ el.setAttribute('aria-pressed', active ? 'true' : 'false');
1566
+ });
1567
+ const hint = document.getElementById('geminiQuotaHint');
1568
+ if (hint) hint.style.display = mode === 'gemini' ? 'block' : 'none';
1569
+ if (mode !== 'gemini') setAnalyzeButtonState(true, 'Analyze');
1570
+ else if (geminiCooldownEnd > Date.now() && !geminiCooldownTimer) {
1571
+ function tick() {
1572
+ const left = Math.ceil((geminiCooldownEnd - Date.now()) / 1000);
1573
+ if (left <= 0 || getAnalysisMode() !== 'gemini') {
1574
+ if (geminiCooldownTimer) clearInterval(geminiCooldownTimer);
1575
+ geminiCooldownTimer = null;
1576
+ setAnalyzeButtonState(true, 'Analyze');
1577
+ return;
1578
+ }
1579
+ const m = Math.floor(left / 60);
1580
+ const s = left % 60;
1581
+ setAnalyzeButtonState(false, 'Retry in ' + m + ':' + (s < 10 ? '0' : '') + s);
1582
+ }
1583
+ tick();
1584
+ geminiCooldownTimer = setInterval(tick, 1000);
1585
+ }
1586
+ }
1587
+
1588
+ function getAnalysisMode() {
1589
+ return window.analysisMode || 'sonar';
1590
+ }
1591
+
1592
+ function escapeHtml(s) {
1593
+ if (s == null) return '';
1594
+ const div = document.createElement('div');
1595
+ div.textContent = s;
1596
+ return div.innerHTML;
1597
+ }
1598
+
1599
+ async function analyzeNewRepo() {
1600
+ const input = document.getElementById('newRepoInput');
1601
+ const repoInput = (input && input.value) ? input.value.trim() : '';
1602
+ if (!repoInput) {
1603
+ showError('Enter a repository URL or owner/repo');
1604
+ return;
1605
+ }
1606
+ hideError();
1607
+ document.getElementById('resultsDiv').style.display = 'none';
1608
+ document.getElementById('loadingDiv').style.display = 'block';
1609
+ var byModel = getSelectedOptionsGroupedByModel();
1610
+ var modelsToRun = [];
1611
+ if (byModel.gemini.length > 0) modelsToRun.push('gemini');
1612
+ if (byModel.openai.length > 0) modelsToRun.push('openai');
1613
+ if (byModel.sonar.length > 0) modelsToRun.push('sonar');
1614
+ if (modelsToRun.length === 0) {
1615
+ document.getElementById('loadingDiv').style.display = 'none';
1616
+ document.getElementById('resultsDiv').style.display = 'block';
1617
+ showError('Select at least one analysis option.');
1618
+ return;
1619
+ }
1620
+ var loadingEl = document.getElementById('loadingMessage');
1621
+ var merged = { repository: repoInput, analysisMode: modelsToRun.length === 1 ? modelsToRun[0] : 'multi', sonar: null, gemini: null, openai: null };
1622
+ try {
1623
+ for (var i = 0; i < modelsToRun.length; i++) {
1624
+ var model = modelsToRun[i];
1625
+ if (model === 'sonar') {
1626
+ loadingEl.textContent = 'Analyzing with SonarCloud…';
1627
+ var bodySonar = { repository: repoInput, analysisOptions: byModel.sonar.map(function (o) { return o.prompt; }) };
1628
+ var resSonar = await fetch('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodySonar) });
1629
+ if (!resSonar.ok) { var d = await resSonar.json(); throw new Error(d.error || 'SonarCloud analysis failed'); }
1630
+ merged.sonar = await resSonar.json();
1631
+ } else if (model === 'gemini') {
1632
+ loadingEl.textContent = 'Analyzing with Gemini AI…';
1633
+ var promptGemini = buildPromptForModel(byModel.gemini);
1634
+ var bodyGemini = { repository: repoInput, prompt: promptGemini, analysisOptions: byModel.gemini.map(function (o) { return o.prompt; }) };
1635
+ var resGemini = await fetch('/api/gemini-analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyGemini) });
1636
+ if (!resGemini.ok) { var d = await resGemini.json(); throw new Error(d.error || 'Gemini analysis failed'); }
1637
+ merged.gemini = await resGemini.json();
1638
+ } else if (model === 'openai') {
1639
+ loadingEl.textContent = 'Analyzing with Open AI…';
1640
+ var promptOpenai = buildPromptForModel(byModel.openai);
1641
+ var bodyOpenai = { repository: repoInput, prompt: promptOpenai, analysisOptions: byModel.openai.map(function (o) { return o.prompt; }) };
1642
+ var resOpenai = await fetch('/api/openai-analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyOpenai) });
1643
+ if (!resOpenai.ok) { var d = await resOpenai.json(); throw new Error(d.error || 'Open AI analysis failed'); }
1644
+ merged.openai = await resOpenai.json();
1645
+ }
1646
+ }
1647
+ merged.repository = (merged.sonar && merged.sonar.repository) || (merged.gemini && merged.gemini.repository) || (merged.openai && merged.openai.repository) || repoInput;
1648
+ merged.geminiSelectedOptions = byModel.gemini.map(function (o) { return o.prompt; });
1649
+ merged.openaiSelectedOptions = byModel.openai.map(function (o) { return o.prompt; });
1650
+ merged.sonarSelectedOptions = byModel.sonar.map(function (o) { return o.prompt; });
1651
+ // Normalize: copy from nested API responses so showResults finds them for single-model runs
1652
+ if (merged.gemini) {
1653
+ merged.geminiAnalysis = merged.gemini.geminiAnalysis;
1654
+ merged.geminiRatings = merged.gemini.geminiRatings;
1655
+ }
1656
+ if (merged.openai) {
1657
+ merged.openaiAnalysis = merged.openai.openaiAnalysis;
1658
+ merged.openaiRatings = merged.openai.openaiRatings;
1659
+ }
1660
+ if (merged.sonar) {
1661
+ merged.analysis = merged.sonar.analysis;
1662
+ }
1663
+ document.getElementById('loadingDiv').style.display = 'none';
1664
+ document.getElementById('resultsDiv').style.display = 'block';
1665
+ showResults(merged);
1666
+ if (input) input.value = '';
1667
+ } catch (error) {
1668
+ document.getElementById('loadingDiv').style.display = 'none';
1669
+ document.getElementById('resultsDiv').style.display = 'block';
1670
+ showError(error.message);
1671
+ if ((error.message || '').toLowerCase().includes('free tier') || (error.message || '').toLowerCase().includes('gemini')) {
1672
+ startGeminiCooldown(90);
1673
+ }
1674
+ }
1675
+ }
1676
+
1677
+ function showError(message) {
1678
+ document.getElementById('errorDiv').style.display = 'block';
1679
+ document.getElementById('errorMessage').textContent = message;
1680
+ }
1681
+
1682
+ function hideError() {
1683
+ document.getElementById('errorDiv').style.display = 'none';
1684
+ }
1685
+
1686
+ function showToast(message) {
1687
+ const toast = document.getElementById('toast');
1688
+ if (!toast) return;
1689
+ toast.textContent = message;
1690
+ toast.classList.add('show');
1691
+ setTimeout(function () { toast.classList.remove('show'); }, 2800);
1692
+ }
1693
+
1694
+ function copyRepoToClipboard() {
1695
+ const repo = lastAnalysisData?.repository || document.getElementById('repoName')?.textContent?.trim();
1696
+ if (!repo) return;
1697
+ const url = repo.includes('/') && !repo.includes('.') ? 'https://github.com/' + repo : repo;
1698
+ const btn = document.getElementById('copyRepoBtn');
1699
+ navigator.clipboard.writeText(url).then(function () {
1700
+ if (btn) { btn.textContent = 'Copied!'; btn.classList.add('copied'); setTimeout(function () { btn.textContent = 'Copy repo'; btn.classList.remove('copied'); }, 2000); }
1701
+ showToast('Repository link copied to clipboard');
1702
+ }).catch(function () {
1703
+ showToast('Could not copy');
1704
+ });
1705
+ }
1706
+
1707
+ async function analyzeRepository() {
1708
+ const repoInput = document.getElementById('repoInput').value.trim();
1709
+ if (!repoInput) {
1710
+ showError('Please enter a repository URL or owner/repo');
1711
+ return;
1712
+ }
1713
+
1714
+ hideError();
1715
+ document.getElementById('loadingDiv').style.display = 'block';
1716
+ document.getElementById('resultsDiv').style.display = 'none';
1717
+
1718
+ var byModel = getSelectedOptionsGroupedByModel();
1719
+ var modelsToRun = [];
1720
+ if (byModel.gemini.length > 0) modelsToRun.push('gemini');
1721
+ if (byModel.openai.length > 0) modelsToRun.push('openai');
1722
+ if (byModel.sonar.length > 0) modelsToRun.push('sonar');
1723
+ if (modelsToRun.length === 0) {
1724
+ document.getElementById('loadingDiv').style.display = 'none';
1725
+ document.getElementById('inputSection').style.display = 'block';
1726
+ showError('Select at least one analysis option.');
1727
+ return;
1728
+ }
1729
+
1730
+ setAnalyzeButtonState(false, 'Analyzing');
1731
+
1732
+ var loadingEl = document.getElementById('loadingMessage');
1733
+ var merged = { repository: repoInput, analysisMode: modelsToRun.length === 1 ? modelsToRun[0] : 'multi', sonar: null, gemini: null, openai: null };
1734
+
1735
+ try {
1736
+ for (var i = 0; i < modelsToRun.length; i++) {
1737
+ var model = modelsToRun[i];
1738
+ if (model === 'sonar') {
1739
+ loadingEl.textContent = 'Analyzing with SonarCloud…';
1740
+ var bodySonar = { repository: repoInput, analysisOptions: byModel.sonar.map(function (o) { return o.prompt; }) };
1741
+ var resSonar = await fetch('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodySonar) });
1742
+ if (!resSonar.ok) { var d = await resSonar.json(); throw new Error(d.error || 'SonarCloud analysis failed'); }
1743
+ merged.sonar = await resSonar.json();
1744
+ } else if (model === 'gemini') {
1745
+ loadingEl.textContent = 'Analyzing with Gemini AI…';
1746
+ var promptGemini = buildPromptForModel(byModel.gemini);
1747
+ var bodyGemini = { repository: repoInput, prompt: promptGemini, analysisOptions: byModel.gemini.map(function (o) { return o.prompt; }) };
1748
+ var resGemini = await fetch('/api/gemini-analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyGemini) });
1749
+ if (!resGemini.ok) { var d = await resGemini.json(); throw new Error(d.error || 'Gemini analysis failed'); }
1750
+ merged.gemini = await resGemini.json();
1751
+ } else if (model === 'openai') {
1752
+ loadingEl.textContent = 'Analyzing with Open AI…';
1753
+ var promptOpenai = buildPromptForModel(byModel.openai);
1754
+ var bodyOpenai = { repository: repoInput, prompt: promptOpenai, analysisOptions: byModel.openai.map(function (o) { return o.prompt; }) };
1755
+ var resOpenai = await fetch('/api/openai-analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bodyOpenai) });
1756
+ if (!resOpenai.ok) { var d = await resOpenai.json(); throw new Error(d.error || 'Open AI analysis failed'); }
1757
+ merged.openai = await resOpenai.json();
1758
+ }
1759
+ }
1760
+
1761
+ merged.repository = (merged.sonar && merged.sonar.repository) || (merged.gemini && merged.gemini.repository) || (merged.openai && merged.openai.repository) || repoInput;
1762
+ merged.geminiSelectedOptions = byModel.gemini.map(function (o) { return o.prompt; });
1763
+ merged.openaiSelectedOptions = byModel.openai.map(function (o) { return o.prompt; });
1764
+ merged.sonarSelectedOptions = byModel.sonar.map(function (o) { return o.prompt; });
1765
+ // Normalize: copy from nested API responses so showResults finds them for single-model runs
1766
+ if (merged.gemini) {
1767
+ merged.geminiAnalysis = merged.gemini.geminiAnalysis;
1768
+ merged.geminiRatings = merged.gemini.geminiRatings;
1769
+ }
1770
+ if (merged.openai) {
1771
+ merged.openaiAnalysis = merged.openai.openaiAnalysis;
1772
+ merged.openaiRatings = merged.openai.openaiRatings;
1773
+ }
1774
+ if (merged.sonar) {
1775
+ merged.analysis = merged.sonar.analysis;
1776
+ }
1777
+ showResults(merged);
1778
+ setAnalyzeButtonState(true, 'Analyze');
1779
+ } catch (error) {
1780
+ document.getElementById('loadingDiv').style.display = 'none';
1781
+ document.getElementById('inputSection').style.display = 'block';
1782
+ showError(error.message);
1783
+ if ((error.message || '').toLowerCase().includes('free tier') || (error.message || '').toLowerCase().includes('gemini')) {
1784
+ startGeminiCooldown(90);
1785
+ } else {
1786
+ setAnalyzeButtonState(true, 'Analyze');
1787
+ }
1788
+ }
1789
+ }
1790
+
1791
+ async function loadSonarReportData(projectKey) {
1792
+ const container = document.getElementById('sonarCloudContent');
1793
+ if (!container || !projectKey) return;
1794
+ const statusDiv = document.createElement('div');
1795
+ statusDiv.id = 'sonarReportLoadStatus';
1796
+ statusDiv.style.cssText = 'text-align:center;padding:12px;color:#0369a1;';
1797
+ statusDiv.textContent = 'Loading report from SonarCloud…';
1798
+ container.appendChild(statusDiv);
1799
+ try {
1800
+ const res = await fetch('/api/sonar/report-data?projectKey=' + encodeURIComponent(projectKey));
1801
+ const data = await res.json();
1802
+ statusDiv.remove();
1803
+ if (data.available && data.metrics && (data.metrics.ncloc != null || data.metrics.lines != null)) {
1804
+ const m = data.metrics || {};
1805
+ // Quality Gate column not shown in report (commented out)
1806
+ let html = `
1807
+ <div style="background: #f8f9fa; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
1808
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
1809
+ <div><div style="font-size: 0.85em; color: #666; margin-bottom: 5px;">Quality Score</div><div style="font-size: 2em; font-weight: bold; color: #E6006C;">${data.score != null ? data.score + '/10' : 'Not available'}</div><div style="font-size: 1em; color: #666;">${data.rating || 'Not available'}</div></div>
1810
+ <div><div style="font-size: 0.85em; color: #666; margin-bottom: 5px;">Project</div><div style="font-size: 0.95em; font-family: monospace; word-break: break-all;">${data.projectKey || 'Not available'}</div></div>
1811
+ </div>
1812
+ </div>
1813
+ <div style="margin-bottom: 20px;"><strong style="display: block; margin-bottom: 10px;">Metrics</strong>
1814
+ <div class="metrics-grid">
1815
+ <div class="metric-card"><div class="metric-label">Lines of Code</div><div class="metric-value">${m.ncloc ?? m.lines ?? 0}</div></div>
1816
+ <div class="metric-card"><div class="metric-label">Bugs</div><div class="metric-value">${m.bugs ?? 0}</div></div>
1817
+ <div class="metric-card"><div class="metric-label">Vulnerabilities</div><div class="metric-value">${m.vulnerabilities ?? 0}</div></div>
1818
+ <div class="metric-card"><div class="metric-label">Security Hotspots</div><div class="metric-value">${m.securityHotspots ?? 0}</div></div>
1819
+ <div class="metric-card"><div class="metric-label">Duplication</div><div class="metric-value">${m.duplicatedLinesDensity != null ? m.duplicatedLinesDensity + '%' : 'N/A'}</div></div>
1820
+ <div class="metric-card"><div class="metric-label">Coverage</div><div class="metric-value">${m.coverage != null ? m.coverage + '%' : 'N/A'}</div></div>
1821
+ <div class="metric-card"><div class="metric-label">Complexity</div><div class="metric-value">${m.complexity ?? 0}</div></div>
1822
+ </div>
1823
+ </div>`;
1824
+ /* Quality Gate Conditions not shown in report
1825
+ if (data.qualityGate?.conditions?.length > 0) {
1826
+ html += '<strong style="display: block; margin: 15px 0 8px 0;">Quality Gate Conditions</strong><div class="findings-list">';
1827
+ data.qualityGate.conditions.forEach((c) => {
1828
+ const condColor = c.status === 'OK' ? '#22c55e' : '#ef4444';
1829
+ html += `<div class="finding-item"><div style="color: ${condColor}; font-weight: bold;">${c.status}</div><div class="finding-content"><div class="finding-title">${c.metricKey}</div><div class="finding-description">${c.errorThreshold != null ? 'Threshold: ' + c.errorThreshold : ''}</div></div></div>`;
1830
+ });
1831
+ html += '</div>';
1832
+ }
1833
+ */
1834
+ if (data.issues?.items?.length > 0) {
1835
+ html += `<strong style="display: block; margin: 20px 0 10px 0;">Issues (${data.issues.total} total)</strong>`;
1836
+ data.issues.items.forEach((issue) => {
1837
+ const sev = (issue.severity || 'INFO').toLowerCase();
1838
+ html += `<div style="background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px; margin-bottom: 10px;"><div style="display: flex; align-items: center; gap: 10px; margin-bottom: 6px;"><span class="finding-severity severity-${sev}">${issue.severity}</span><span style="font-size: 0.85em; color: #666;">${issue.type}</span></div><div class="finding-description">${issue.message || ''}</div>${issue.component ? `<div class="finding-location">${issue.component}${issue.line ? ':' + issue.line : ''}</div>` : ''}</div>`;
1839
+ });
1840
+ }
1841
+ if (data.recommendations?.length > 0) {
1842
+ html += '<strong style="margin: 20px 0; display: block;">Recommendations</strong><div class="recommendations">';
1843
+ data.recommendations.forEach((rec) => {
1844
+ html += `<div class="recommendation-item"><div class="recommendation-priority priority-${(rec.priority || 'MEDIUM').toLowerCase()}">${rec.priority || 'MEDIUM'}</div><div><strong>${rec.category}:</strong> ${rec.action}</div></div>`;
1845
+ });
1846
+ html += '</div>';
1847
+ }
1848
+ html += `<div style="margin-top: 12px;"><button type="button" onclick="refreshSonarCloud()" style="padding: 10px 20px; background: #0ea5e9; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Refresh SonarCloud results</button> <span style="font-size: 0.85em; color: #64748b;">Re-fetches without re-scanning.</span></div>`;
1849
+ container.innerHTML = html;
1850
+ } else {
1851
+ const helpHtml = `<div style="margin-top: 16px; padding: 12px; background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; font-size: 0.85em; text-align: left;">
1852
+ <strong style="color: #0369a1;">Find your Project Key on SonarCloud:</strong>
1853
+ <ol style="margin: 8px 0 0 0; padding-left: 18px;">
1854
+ <li>Open <a href="https://sonarcloud.io" target="_blank" rel="noopener">sonarcloud.io</a> → your Organization → your project.</li>
1855
+ <li>In the browser URL, the value after <code>?id=</code> is the Project Key (e.g. <code>org_repo-name</code>).</li>
1856
+ <li>Set it in <code>.env</code> as <code>SONAR_PROJECT_KEY=that_exact_key</code>, restart the server, then try again.</li>
1857
+ </ol>
1858
+ <p style="margin: 8px 0 0 0;">Key you used: <code>${projectKey}</code></p>
1859
+ </div>`;
1860
+ container.innerHTML = `<div style="text-align: center; padding: 20px; color: #64748b;">${data.unavailableReason || 'Report not available.'}</div><div style="text-align: center; margin-top: 12px;"><button type="button" onclick="loadSonarReportData('${projectKey.replace(/'/g, "\\'")}')" style="padding: 10px 20px; background: #0284c7; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Load final report</button></div>${helpHtml}`;
1861
+ }
1862
+ } catch (err) {
1863
+ statusDiv.textContent = 'Failed to load: ' + (err.message || 'Request failed');
1864
+ statusDiv.style.color = '#b91c1c';
1865
+ }
1866
+ }
1867
+
1868
+ async function refreshSonarCloud() {
1869
+ const repo = window.currentRepository || document.getElementById('repoInput')?.value?.trim();
1870
+ const projectKey = window.currentAnalysis?.sonarCloud?.projectKey;
1871
+ const container = document.getElementById('sonarCloudContent');
1872
+ if (!container) return;
1873
+ if (!repo && !projectKey) {
1874
+ container.insertAdjacentHTML('beforeend', '<div style="margin-top:12px;color:#b91c1c;">No repository or project key. Run Analyze first.</div>');
1875
+ return;
1876
+ }
1877
+ const statusDiv = document.createElement('div');
1878
+ statusDiv.id = 'sonarRefreshStatus';
1879
+ statusDiv.style.cssText = 'margin-top:12px;padding:12px;color:#0369a1;';
1880
+ statusDiv.textContent = 'Refreshing SonarCloud results…';
1881
+ container.appendChild(statusDiv);
1882
+ try {
1883
+ const body = projectKey ? { projectKey } : { repository: repo };
1884
+ const res = await fetch('/api/sonar/refresh', {
1885
+ method: 'POST',
1886
+ headers: { 'Content-Type': 'application/json' },
1887
+ body: JSON.stringify(body),
1888
+ });
1889
+ const data = await res.json();
1890
+ statusDiv.remove();
1891
+ if (data.available && data.metrics && (data.metrics.ncloc != null || data.metrics.lines != null)) {
1892
+ if (window.currentAnalysis?.sonarCloud) {
1893
+ window.currentAnalysis.sonarCloud = { ...window.currentAnalysis.sonarCloud, ...data };
1894
+ }
1895
+ loadSonarReportData(data.projectKey || projectKey);
1896
+ } else {
1897
+ container.insertAdjacentHTML('beforeend', '<div style="margin-top:12px;color:#b91c1c;">' + (data.unavailableReason || 'Refresh failed') + '</div>');
1898
+ }
1899
+ } catch (err) {
1900
+ statusDiv.textContent = 'Refresh failed: ' + (err.message || 'Request failed');
1901
+ statusDiv.style.color = '#b91c1c';
1902
+ }
1903
+ }
1904
+
1905
+ async function runSonarScanFromApp() {
1906
+ const repo = window.currentRepository || document.getElementById('repoInput')?.value?.trim();
1907
+ if (!repo) {
1908
+ const el = document.getElementById('sonarRunScanStatus');
1909
+ if (el) { el.innerHTML = '<span style="color: #b91c1c;">No repository. Run Analyze first.</span>'; }
1910
+ return;
1911
+ }
1912
+ const makePublic = document.getElementById('sonarMakePublic')?.checked === true;
1913
+ const btn = document.getElementById('btnRunSonarScan');
1914
+ const statusEl = document.getElementById('sonarRunScanStatus');
1915
+ if (btn) { btn.disabled = true; btn.textContent = 'Running…'; }
1916
+ if (statusEl) { statusEl.innerHTML = ''; statusEl.style.color = ''; }
1917
+ try {
1918
+ const response = await fetch('/api/sonar/run-scan', {
1919
+ method: 'POST',
1920
+ headers: { 'Content-Type': 'application/json' },
1921
+ body: JSON.stringify({ repository: repo, makePublic }),
1922
+ });
1923
+ const data = await response.json().catch(() => ({}));
1924
+ if (statusEl) {
1925
+ if (response.ok) {
1926
+ statusEl.style.color = '#059669';
1927
+ statusEl.innerHTML = '✓ ' + (data.message || 'Analysis uploaded. Wait 1–2 minutes then click Retry.');
1928
+ } else {
1929
+ statusEl.style.color = '#b91c1c';
1930
+ statusEl.innerHTML = '✗ ' + (data.error || 'Run scan failed');
1931
+ }
1932
+ }
1933
+ } catch (err) {
1934
+ if (statusEl) { statusEl.style.color = '#b91c1c'; statusEl.textContent = '✗ ' + (err.message || 'Request failed'); }
1935
+ } finally {
1936
+ if (btn) { btn.disabled = false; btn.textContent = 'Run new SonarCloud analysis'; }
1937
+ }
1938
+ }
1939
+
1940
+ function buildMergedReportHtml(data) {
1941
+ var html = '';
1942
+ if (data.gemini) {
1943
+ var gText = data.gemini.geminiAnalysis != null ? String(data.gemini.geminiAnalysis) : 'No analysis returned.';
1944
+ var geminiTitle = (data.geminiSelectedOptions && data.geminiSelectedOptions.length) ? data.geminiSelectedOptions.join(', ') : 'Gemini AI Analysis';
1945
+ html += '<div class="merged-model-block"><h4 class="merged-model-title">✨ ' + escapeHtml(geminiTitle) + '</h4><div class="merged-model-body">' + formatOpenAIReport(gText) + '</div></div>';
1946
+ }
1947
+ if (data.openai) {
1948
+ var oText = data.openai.openaiAnalysis != null ? String(data.openai.openaiAnalysis) : 'No analysis returned.';
1949
+ var openaiTitle = (data.openaiSelectedOptions && data.openaiSelectedOptions.length) ? data.openaiSelectedOptions.join(', ') : 'Open AI Analysis';
1950
+ html += '<div class="merged-model-block"><h4 class="merged-model-title">🤖 ' + escapeHtml(openaiTitle) + '</h4><div class="merged-model-body report-color-coded">' + formatOpenAIReport(oText, true) + '</div></div>';
1951
+ }
1952
+ if (data.sonar && data.sonar.analysis && data.sonar.analysis.sonarCloud) {
1953
+ var sc = data.sonar.analysis.sonarCloud;
1954
+ var m = sc.metrics || {};
1955
+ var scoreDisplay = sc.score != null ? sc.score + '/10' : 'Not available';
1956
+ var rating = sc.rating || sc.overallSummary?.rating || 'Not available';
1957
+ var loc = (m.ncloc ?? m.lines ?? 0).toLocaleString();
1958
+ var bugs = (m.bugs ?? 0).toLocaleString();
1959
+ var vuln = (m.vulnerabilities ?? 0).toLocaleString();
1960
+ var sonarLink = sc.viewOnSonarCloud || (sc.projectKey ? 'https://sonarcloud.io/project/overview?id=' + encodeURIComponent(sc.projectKey) : '');
1961
+ var sonarTitle = (data.sonarSelectedOptions && data.sonarSelectedOptions.length) ? data.sonarSelectedOptions.join(', ') : 'SonarCloud Analysis';
1962
+ html += '<div class="merged-model-block"><h4 class="merged-model-title">📊 ' + escapeHtml(sonarTitle) + '</h4>';
1963
+ if (sc.available) {
1964
+ html += '<div class="merged-sonar-tiles" style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px;width:100%;">';
1965
+ html += '<div class="sonar-tile overall" style="padding:12px;"><div class="tile-label">Quality Score</div><div class="tile-value">' + scoreDisplay + '</div><div style="font-size:0.8em;opacity:0.9;">' + escapeHtml(rating) + '</div></div>';
1966
+ // Quality Gate tile not shown in report
1967
+ html += '<div class="sonar-tile loc" style="padding:12px;"><div class="tile-label">Lines of Code</div><div class="tile-value">' + loc + '</div></div>';
1968
+ html += '<div class="sonar-tile bugs" style="padding:12px;"><div class="tile-label">Bugs</div><div class="tile-value">' + bugs + '</div></div>';
1969
+ html += '<div class="sonar-tile vuln" style="padding:12px;"><div class="tile-label">Vulnerabilities</div><div class="tile-value">' + vuln + '</div></div>';
1970
+ // Code Smells tile not shown in report
1971
+ html += '</div>';
1972
+ } else {
1973
+ html += '<p style="color:#64748b;">' + escapeHtml(sc.unavailableReason || 'SonarCloud data not available.') + '</p>';
1974
+ }
1975
+ html += '</div>';
1976
+ }
1977
+ return html || '<p>No analysis content to display.</p>';
1978
+ }
1979
+
1980
+ function showResults(data) {
1981
+ document.getElementById('loadingDiv').style.display = 'none';
1982
+ document.getElementById('resultsDiv').style.display = 'block';
1983
+ var fh = document.getElementById('featureHighlights');
1984
+ if (fh) fh.style.display = 'none';
1985
+ window.currentRepository = data.repository;
1986
+ const isMulti = data.analysisMode === 'multi';
1987
+ // Use top-level or nested response so single-model (Gemini/OpenAI/Sonar) works
1988
+ const analysis = (isMulti && data.sonar) ? data.sonar.analysis : (data.analysis || (data.sonar && data.sonar.analysis));
1989
+ window.currentAnalysis = analysis;
1990
+ setAnalysisMode(isMulti ? 'sonar' : (data.analysisMode === 'gemini' ? 'gemini' : data.analysisMode === 'openai' ? 'openai' : 'sonar'));
1991
+
1992
+ const isGemini = data.analysisMode === 'gemini';
1993
+ const isOpenai = data.analysisMode === 'openai';
1994
+ const geminiAnalysis = data.geminiAnalysis != null ? data.geminiAnalysis : (data.gemini && data.gemini.geminiAnalysis);
1995
+ const openaiAnalysis = data.openaiAnalysis != null ? data.openaiAnalysis : (data.openai && data.openai.openaiAnalysis);
1996
+
1997
+ // Results bar: repo name, timestamp, copy button
1998
+ document.getElementById('repoName').textContent = data.repository;
1999
+ const now = new Date();
2000
+ const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2001
+ const dateStr = now.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
2002
+ document.getElementById('resultsTimestamp').textContent = `Analyzed ${dateStr} at ${timeStr}`;
2003
+
2004
+ const summaryGrid = document.getElementById('summaryGrid');
2005
+ const sonarSection = document.getElementById('sonarSection');
2006
+ const geminiSection = document.getElementById('geminiSection');
2007
+ const geminiContent = document.getElementById('geminiContent');
2008
+ const openaiSection = document.getElementById('openaiSection');
2009
+ const openaiContent = document.getElementById('openaiContent');
2010
+
2011
+ if (isGemini) {
2012
+ sonarSection.style.display = 'none';
2013
+ geminiSection.style.display = 'block';
2014
+ if (openaiSection) openaiSection.style.display = 'none';
2015
+ var geminiH3 = geminiSection ? geminiSection.querySelector('h3') : null;
2016
+ if (geminiH3 && data.geminiSelectedOptions && data.geminiSelectedOptions.length) geminiH3.innerHTML = '<span class="section-icon">✨</span> ' + data.geminiSelectedOptions.join(', ');
2017
+ summaryGrid.innerHTML = '';
2018
+ var geminiDashboard = document.getElementById('geminiDashboard');
2019
+ var geminiTilesContainer = document.getElementById('geminiRatingsTiles');
2020
+ if (geminiDashboard) geminiDashboard.style.display = 'none';
2021
+ if (geminiTilesContainer) geminiTilesContainer.innerHTML = '';
2022
+ var reportText = geminiAnalysis != null ? String(geminiAnalysis) : 'No analysis text returned.';
2023
+ if (geminiContent) {
2024
+ geminiContent.classList.add('openai-report');
2025
+ geminiContent.innerHTML = reportText ? formatOpenAIReport(reportText) : '';
2026
+ }
2027
+ } else if (isOpenai) {
2028
+ sonarSection.style.display = 'none';
2029
+ geminiSection.style.display = 'none';
2030
+ if (openaiSection) openaiSection.style.display = 'block';
2031
+ var openaiH3 = openaiSection ? openaiSection.querySelector('h3') : null;
2032
+ if (openaiH3 && data.openaiSelectedOptions && data.openaiSelectedOptions.length) openaiH3.innerHTML = '';
2033
+ summaryGrid.innerHTML = '';
2034
+ var dashboard = document.getElementById('openaiDashboard');
2035
+ var tilesContainer = document.getElementById('openaiRatingsTiles');
2036
+ if (dashboard) dashboard.style.display = 'none';
2037
+ if (tilesContainer) tilesContainer.innerHTML = '';
2038
+ if (openaiContent) {
2039
+ var reportText = openaiAnalysis != null ? String(openaiAnalysis) : 'No analysis text returned.';
2040
+ openaiContent.innerHTML = formatOpenAIReport(reportText, true);
2041
+ }
2042
+ } else {
2043
+ sonarSection.style.display = 'block';
2044
+ geminiSection.style.display = 'none';
2045
+ if (openaiSection) openaiSection.style.display = 'none';
2046
+ var sonarH3 = sonarSection ? sonarSection.querySelector('h3') : null;
2047
+ if (sonarH3 && data.sonarSelectedOptions && data.sonarSelectedOptions.length) sonarH3.innerHTML = '<span class="section-icon">📊</span> ' + data.sonarSelectedOptions.join(', ');
2048
+ }
2049
+
2050
+ // Summary cards and SonarCloud section — only when not Gemini and not Open AI
2051
+ if (!isGemini && !isOpenai) {
2052
+ let summaryCards = '';
2053
+ if (analysis?.sonarCloud?.available && (analysis.sonarCloud.score != null || analysis.sonarCloud.overallSummary)) {
2054
+ const sc = analysis.sonarCloud;
2055
+ const scScore = sc.score != null ? Math.round(sc.score * 10) : (sc.overallSummary?.score100 ?? null);
2056
+ const scDisplay = sc.score != null ? sc.score + '/10' : (sc.overallSummary?.score100 != null ? sc.overallSummary.score100 + '/100' : 'Not available');
2057
+ const rating = sc.rating || sc.overallSummary?.rating || 'Not available';
2058
+ summaryCards = `
2059
+ <div class="score-card">
2060
+ <div class="score-label">SonarCloud Score</div>
2061
+ <div class="score-main">
2062
+ <div class="score-value">${scDisplay}</div>
2063
+ <div class="score-rating">${rating}</div>
2064
+ </div>
2065
+ <div class="score-bar">
2066
+ <div class="score-bar-fill" style="width: ${scScore != null ? Math.min(100, scScore) : 0}%"></div>
2067
+ </div>
2068
+ </div>
2069
+ `;
2070
+ } else {
2071
+ summaryCards = `
2072
+ <div class="score-card">
2073
+ <div class="score-label">SonarCloud</div>
2074
+ <div class="score-main">
2075
+ <div class="score-value">Not available</div>
2076
+ <div class="score-rating">Not available</div>
2077
+ </div>
2078
+ <div class="score-bar"><div class="score-bar-fill" style="width: 0%"></div></div>
2079
+ </div>
2080
+ `;
2081
+ }
2082
+ if (!isMulti) summaryGrid.innerHTML = summaryCards;
2083
+
2084
+ // SonarCloud section — always show metrics grid when available so it appears in final analysis
2085
+ let sonarHTML = '';
2086
+ if (analysis && analysis.sonarCloud) {
2087
+ const sc = analysis.sonarCloud;
2088
+ const sonarLink = sc.viewOnSonarCloud || (sc.projectKey ? 'https://sonarcloud.io/project/overview?id=' + encodeURIComponent(sc.projectKey) : '');
2089
+ const noMetrics = sc.score == null || (sc.metrics && (sc.metrics.ncloc == null || sc.metrics.ncloc === 0));
2090
+ if (!sc.available) {
2091
+ sonarHTML = `<div style="text-align: center; padding: 20px; color: #64748b;">SonarCloud data not available. ${sc.unavailableReason || 'Set SONAR_TOKEN in .env and ensure the project is analyzed on SonarCloud.'}</div>
2092
+ <details style="max-width: 560px; margin: 16px auto; text-align: left; padding: 12px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0;">
2093
+ <summary style="cursor: pointer; font-weight: 600; color: #334155;">How to get a SonarCloud token</summary>
2094
+ <ol style="margin: 12px 0 0 0; padding-left: 20px; color: #475569; font-size: 0.9em; line-height: 1.6;">
2095
+ <li>Sign in at <a href="https://sonarcloud.io" target="_blank" rel="noopener">sonarcloud.io</a> (use your GitHub account).</li>
2096
+ <li>Go to <strong>Account → Security</strong> (<a href="https://sonarcloud.io/account/security" target="_blank" rel="noopener">sonarcloud.io/account/security</a>).</li>
2097
+ <li>Click <strong>Generate Tokens</strong>, name it (e.g. <code>GitRepoAnalyzer</code>), then generate.</li>
2098
+ <li>Copy the token and add it to your project’s <code>.env</code> file: <code>SONAR_TOKEN=your_token_here</code> (no quotes).</li>
2099
+ <li>Restart the server so it picks up the new <code>.env</code> value.</li>
2100
+ </ol>
2101
+ </details>`;
2102
+ if (sc.projectKey) {
2103
+ sonarHTML += `<div style="text-align: center; margin-top: 12px;"><button type="button" onclick="loadSonarReportData('${sc.projectKey.replace(/'/g, "\\'")}')" style="padding: 12px 24px; background: #0284c7; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Load final report from SonarCloud</button> <label style="display: inline-flex; align-items: center; gap: 8px; margin-left: 12px; cursor: pointer;"><input type="checkbox" id="sonarMakePublic" /> Make project public</label> <button type="button" onclick="runSonarScanFromApp()" id="btnRunSonarScan" style="padding: 12px 24px; background: #059669; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Run new SonarCloud analysis</button></div>`;
2104
+ }
2105
+ } else {
2106
+ const m = sc.metrics || {};
2107
+ const totalIssues = (sc.issues?.total ?? 0) || (Number(m.bugs)||0) + (Number(m.vulnerabilities)||0) + (Number(m.codeSmells)||0);
2108
+ sonarHTML = `
2109
+ <div class="sonar-dashboard">
2110
+ <div class="sonar-kpi-row">
2111
+ <div class="sonar-kpi score">
2112
+ <div class="kpi-value">${sc.score != null ? sc.score + '/10' : 'Not available'}</div>
2113
+ <div class="kpi-label">Quality Score · ${sc.rating || 'Not available'}</div>
2114
+ </div>
2115
+ <!-- Quality Gate KPI not shown in report -->
2116
+ <div class="sonar-kpi loc">
2117
+ <div class="kpi-value">${(m.ncloc ?? m.lines ?? 0).toLocaleString()}</div>
2118
+ <div class="kpi-label">Lines of Code</div>
2119
+ </div>
2120
+ <div class="sonar-kpi issues">
2121
+ <div class="kpi-value">${totalIssues.toLocaleString()}</div>
2122
+ <div class="kpi-label">Total Issues</div>
2123
+ </div>
2124
+ </div>
2125
+ <div class="sonar-charts-row">
2126
+ <div class="sonar-chart-card">
2127
+ <h4>Issues breakdown</h4>
2128
+ <canvas id="chartIssuesBreakdown" height="200"></canvas>
2129
+ </div>
2130
+ <div class="sonar-chart-card">
2131
+ <h4>Coverage & duplication</h4>
2132
+ <canvas id="chartCoverageDup" height="200"></canvas>
2133
+ </div>
2134
+ <div class="sonar-chart-card">
2135
+ <h4>Issues by severity</h4>
2136
+ <canvas id="chartSeverity" height="200"></canvas>
2137
+ </div>
2138
+ </div>
2139
+ <div style="margin-bottom: 8px;"><strong>Metrics</strong></div>
2140
+ <div class="sonar-metrics-tiles">
2141
+ <div class="sonar-tile loc"><div class="tile-label">Lines of Code</div><div class="tile-value">${(m.ncloc ?? m.lines ?? 0).toLocaleString()}</div></div>
2142
+ <div class="sonar-tile bugs"><div class="tile-label">Bugs</div><div class="tile-value">${m.bugs ?? 0}</div></div>
2143
+ <div class="sonar-tile vuln"><div class="tile-label">Vulnerabilities</div><div class="tile-value">${m.vulnerabilities ?? 0}</div></div>
2144
+ <!-- Code Smells tile hidden
2145
+ <div class="sonar-tile smells"><div class="tile-label">Code Smells</div><div class="tile-value">${m.codeSmells ?? 0}</div></div>
2146
+ -->
2147
+ <div class="sonar-tile hotspots"><div class="tile-label">Security Hotspots</div><div class="tile-value">${m.securityHotspots ?? 0}</div></div>
2148
+ <div class="sonar-tile dup"><div class="tile-label">Duplication</div><div class="tile-value">${m.duplicatedLinesDensity != null ? m.duplicatedLinesDensity + '%' : 'N/A'}</div></div>
2149
+ <div class="sonar-tile cov"><div class="tile-label">Coverage</div><div class="tile-value">${m.coverage != null ? m.coverage + '%' : 'N/A'}</div></div>
2150
+ <div class="sonar-tile complexity"><div class="tile-label">Complexity</div><div class="tile-value">${m.complexity ?? 0}</div></div>
2151
+ </div>
2152
+ </div>
2153
+ `;
2154
+ /* Quality Gate Conditions section commented out
2155
+ if (sc.qualityGate?.conditions?.length > 0) {
2156
+ sonarHTML += '<strong style="display: block; margin: 15px 0 8px 0;">Quality Gate Conditions</strong><div class="findings-list">';
2157
+ sc.qualityGate.conditions.forEach((c) => {
2158
+ const condColor = c.status === 'OK' ? '#22c55e' : '#ef4444';
2159
+ sonarHTML += `<div class="finding-item"><div style="color: ${condColor}; font-weight: bold;">${c.status}</div><div class="finding-content"><div class="finding-title">${c.metricKey}</div><div class="finding-description">${c.errorThreshold != null ? 'Threshold: ' + c.errorThreshold : ''}</div></div></div>`;
2160
+ });
2161
+ sonarHTML += '</div>';
2162
+ }
2163
+ */
2164
+ if (sc.issues?.items?.length > 0) {
2165
+ sonarHTML += `<strong style="display: block; margin: 20px 0 10px 0;">Recent issues (top 50 of ${sc.issues.total})</strong>
2166
+ <div style="overflow-x: auto; margin-top: 8px;">
2167
+ <table class="issues-table" style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
2168
+ <thead><tr style="background: #f1f5f9; text-align: left;">
2169
+ <th style="padding: 10px; border: 1px solid #e2e8f0;">Key</th>
2170
+ <th style="padding: 10px; border: 1px solid #e2e8f0;">Rule</th>
2171
+ <th style="padding: 10px; border: 1px solid #e2e8f0;">Severity</th>
2172
+ <th style="padding: 10px; border: 1px solid #e2e8f0;">File path</th>
2173
+ <th style="padding: 10px; border: 1px solid #e2e8f0;">Line</th>
2174
+ <th style="padding: 10px; border: 1px solid #e2e8f0;">Message</th>
2175
+ </tr></thead>
2176
+ <tbody>`;
2177
+ sc.issues.items.forEach((issue) => {
2178
+ const sev = (issue.severity || 'INFO').toLowerCase();
2179
+ const filePath = issue.component ? issue.component.replace(/^[^:]+:/, '') : '—';
2180
+ sonarHTML += `<tr>
2181
+ <td style="padding: 8px; border: 1px solid #e2e8f0; font-family: monospace; font-size: 0.85em;">${escapeHtml(issue.key || '—')}</td>
2182
+ <td style="padding: 8px; border: 1px solid #e2e8f0;">${escapeHtml(issue.rule || '—')}</td>
2183
+ <td style="padding: 8px; border: 1px solid #e2e8f0;"><span class="finding-severity severity-${sev}">${escapeHtml(issue.severity || '—')}</span></td>
2184
+ <td style="padding: 8px; border: 1px solid #e2e8f0; max-width: 200px; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(filePath)}">${escapeHtml(filePath)}</td>
2185
+ <td style="padding: 8px; border: 1px solid #e2e8f0;">${issue.line != null ? issue.line : '—'}</td>
2186
+ <td style="padding: 8px; border: 1px solid #e2e8f0;">${escapeHtml(issue.message || '—')}</td>
2187
+ </tr>`;
2188
+ });
2189
+ sonarHTML += '</tbody></table></div>';
2190
+ }
2191
+ sonarHTML += `<div style="margin-top: 12px;"><button type="button" onclick="refreshSonarCloud()" style="padding: 10px 20px; background: #0ea5e9; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Refresh SonarCloud results</button> <span style="font-size: 0.85em; color: #64748b;">Re-fetches metrics and issues without re-scanning.</span></div>`;
2192
+ if (sc.recommendations?.length > 0) {
2193
+ sonarHTML += '<strong style="margin: 20px 0; display: block;">Recommendations</strong><div class="recommendations">';
2194
+ sc.recommendations.forEach((rec) => {
2195
+ sonarHTML += `
2196
+ <div class="recommendation-item">
2197
+ <div class="recommendation-priority priority-${(rec.priority || 'MEDIUM').toLowerCase()}">${rec.priority || 'MEDIUM'}</div>
2198
+ <div><strong>${rec.category}:</strong> ${rec.action}</div>
2199
+ </div>
2200
+ `;
2201
+ });
2202
+ sonarHTML += '</div>';
2203
+ }
2204
+ if (noMetrics && (sonarLink || sc.projectKey)) {
2205
+ sonarHTML += `
2206
+ <div style="background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 24px; text-align: center; margin-top: 20px;">
2207
+ <div style="font-size: 1em; color: #0369a1; margin-bottom: 12px;">Metrics could not be loaded from API. Click <strong>Load final report</strong> to fetch from SonarCloud (public projects), or Retry after 1–2 min.</div>
2208
+ <div style="display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; align-items: center;">
2209
+ <button type="button" onclick="loadSonarReportData('${(sc.projectKey || '').replace(/'/g, "\\'")}')" style="padding: 12px 24px; background: #0c4a6e; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Load final report from SonarCloud</button>
2210
+ <label style="display: inline-flex; align-items: center; gap: 8px; cursor: pointer;"><input type="checkbox" id="sonarMakePublic" /> Make project public</label>
2211
+ <button type="button" onclick="runSonarScanFromApp()" id="btnRunSonarScan" style="padding: 12px 24px; background: #059669; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Run new SonarCloud analysis</button>
2212
+ <button type="button" onclick="analyzeRepository()" style="padding: 12px 24px; background: #0284c7; color: white; border: none; border-radius: 8px; font-weight: 600; cursor: pointer;">Retry fetching metrics</button>
2213
+ </div>
2214
+ <div id="sonarRunScanStatus" style="font-size: 0.9em; margin-top: 8px; min-height: 1.5em;"></div>
2215
+ </div>
2216
+ `;
2217
+ }
2218
+ }
2219
+ } else {
2220
+ sonarHTML = '<div style="text-align: center; padding: 20px; color: #94a3b8;">SonarCloud analysis not run.</div>';
2221
+ }
2222
+ document.getElementById('sonarCloudContent').innerHTML = sonarHTML;
2223
+
2224
+ if (analysis.sonarCloud?.available) {
2225
+ renderSonarCharts(analysis.sonarCloud);
2226
+ }
2227
+ } // end !isGemini
2228
+
2229
+ if (isMulti) {
2230
+ summaryGrid.innerHTML = '<div class="score-card"><div class="score-label">Analysis</div><div class="score-main"><div class="score-value">Analysis completed</div></div><div class="score-bar"><div class="score-bar-fill" style="width: 100%"></div></div></div>';
2231
+ sonarSection.style.display = 'none';
2232
+ geminiSection.style.display = 'none';
2233
+ if (openaiSection) openaiSection.style.display = 'none';
2234
+ var mergedEl = document.getElementById('mergedReportSection');
2235
+ var mergedContent = document.getElementById('mergedReportContent');
2236
+ if (mergedEl && mergedContent) {
2237
+ mergedContent.innerHTML = buildMergedReportHtml(data);
2238
+ mergedEl.style.display = 'block';
2239
+ }
2240
+ }
2241
+
2242
+ // Store data for PDF export
2243
+ lastAnalysisData = data;
2244
+ }
2245
+
2246
+ function renderSonarCharts(sc) {
2247
+ const m = sc.metrics || {};
2248
+ const bugs = Number(m.bugs) || 0, vuln = Number(m.vulnerabilities) || 0, smells = Number(m.codeSmells) || 0, hotspots = Number(m.securityHotspots) || 0;
2249
+ const coverage = parseFloat(m.coverage) || 0, dup = parseFloat(m.duplicatedLinesDensity) || 0;
2250
+
2251
+ function destroyChart(id) {
2252
+ const el = document.getElementById(id);
2253
+ if (el && typeof Chart !== 'undefined') {
2254
+ const existing = Chart.getChart(el);
2255
+ if (existing) existing.destroy();
2256
+ }
2257
+ }
2258
+ destroyChart('chartIssuesBreakdown');
2259
+ destroyChart('chartCoverageDup');
2260
+ destroyChart('chartSeverity');
2261
+
2262
+ const barCtx = document.getElementById('chartIssuesBreakdown');
2263
+ if (barCtx && window.Chart) {
2264
+ new Chart(barCtx, {
2265
+ type: 'bar',
2266
+ data: {
2267
+ labels: ['Bugs', 'Vulnerabilities', 'Code Smells', 'Hotspots'],
2268
+ datasets: [{ label: 'Count', data: [bugs, vuln, smells, hotspots], backgroundColor: ['#ef4444', '#f97316', '#eab308', '#a855f7'], borderWidth: 0 }]
2269
+ },
2270
+ options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true } } }
2271
+ });
2272
+ }
2273
+ const covCtx = document.getElementById('chartCoverageDup');
2274
+ if (covCtx && window.Chart) {
2275
+ new Chart(covCtx, {
2276
+ type: 'bar',
2277
+ data: {
2278
+ labels: ['Coverage %', 'Duplication %'],
2279
+ datasets: [{ label: '%', data: [coverage, dup], backgroundColor: ['#22c55e', '#f59e0b'], borderWidth: 0 }]
2280
+ },
2281
+ options: { indexAxis: 'y', responsive: true, maintainAspectRatio: true, plugins: { legend: { display: false } }, scales: { x: { max: 100, beginAtZero: true } } }
2282
+ });
2283
+ }
2284
+ const severityCounts = { BLOCKER: 0, CRITICAL: 0, MAJOR: 0, MINOR: 0, INFO: 0 };
2285
+ (sc.issues?.items || []).forEach(function (i) { const s = (i.severity || 'INFO').toUpperCase(); if (severityCounts[s] !== undefined) severityCounts[s]++; else severityCounts.INFO++; });
2286
+ const sevCtx = document.getElementById('chartSeverity');
2287
+ if (sevCtx && window.Chart) {
2288
+ const order = ['BLOCKER', 'CRITICAL', 'MAJOR', 'MINOR', 'INFO'];
2289
+ new Chart(sevCtx, {
2290
+ type: 'pie',
2291
+ data: {
2292
+ labels: order,
2293
+ datasets: [{ data: order.map(function (k) { return severityCounts[k] || 0; }), backgroundColor: ['#7f1d1d', '#b91c1c', '#ea580c', '#ca8a04', '#64748b'], borderWidth: 0 }]
2294
+ },
2295
+ options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'bottom' } } }
2296
+ });
2297
+ }
2298
+ }
2299
+
2300
+ function stripAnalysisSummaryLine(text) {
2301
+ if (!text || typeof text !== 'string') return text;
2302
+ var phrases = [
2303
+ 'Architecture Review', 'Folder Structure Review', 'Code Smells', 'Improvement Suggestions',
2304
+ 'Deep Logic Analysis', 'Best Practices Validation', 'Performance Review & Bottlenecks',
2305
+ 'Dependency Analysis', 'Documentation Quality Review', 'Prioritized Action Items',
2306
+ 'Code Quality Summary', 'Security Review (AI Deep Scan)', 'Code Complexity Analysis',
2307
+ 'Test Coverage & Quality Review', 'Dead Code & Unused Assets', 'Error Handling & Logging Review',
2308
+ 'Database Query Efficiency Review', 'Cloud & Deployment Readiness', 'Maintainability & Readability',
2309
+ 'API Endpoint Quality Review', 'Configuration & Environment Review', 'Management Summary (High-Level)'
2310
+ ];
2311
+ var lines = text.split(/\r?\n/);
2312
+ var kept = lines.filter(function(line) {
2313
+ var t = line.trim();
2314
+ if (!t) return true;
2315
+ var matchCount = phrases.filter(function(p) { return t.indexOf(p) !== -1; }).length;
2316
+ return matchCount < 3;
2317
+ });
2318
+ return kept.join('\n').trim();
2319
+ }
2320
+
2321
+ function formatOpenAIReport(text, applyColorCoding) {
2322
+ if (!text) return '';
2323
+ text = stripAnalysisSummaryLine(text);
2324
+ var escaped = escapeHtml(text);
2325
+ escaped = escaped
2326
+ .replace(/\*\*(.+?)\*\*/g, '<span class="md-strong">$1</span>')
2327
+ .replace(/^##\s+(.+)$/gm, '<div class="md-h2">$1</div>')
2328
+ .replace(/^[\s]*[-*]\s+(.+)$/gm, '<div class="md-list-item">$1</div>')
2329
+ .replace(/\n/g, '<br>');
2330
+ if (applyColorCoding) escaped = applyReportColorCoding(escaped);
2331
+ return escaped;
2332
+ }
2333
+ function applyReportColorCoding(html) {
2334
+ if (!html) return html;
2335
+ // Good (green)
2336
+ var goodPattern = /<div class="md-list-item">(Good|Positive|Strong|Well implemented|No issues|Good practice|Excellent|✅)([\s\S]*?)<\/div>/gi;
2337
+ html = html.replace(goodPattern, '<div class="md-list-item"><span class="report-good">$1$2</span></div>');
2338
+ goodPattern = /<div class="md-list-item"><span class="md-strong">(Good|Positive|Excellent)<\/span>([\s\S]*?)<\/div>/gi;
2339
+ html = html.replace(goodPattern, '<div class="md-list-item"><span class="report-good"><span class="md-strong">$1</span>$2</span></div>');
2340
+ goodPattern = /<div class="md-h2">(Good|Positive|Strengths)([\s\S]*?)<\/div>/gi;
2341
+ html = html.replace(goodPattern, '<div class="md-h2 report-good">$1$2</div>');
2342
+ // Warning (yellow)
2343
+ var warnPattern = /<div class="md-list-item">(Warning|Consider|Caution|Could improve|Attention|⚠️|Note:|Recommendation)([\s\S]*?)<\/div>/gi;
2344
+ html = html.replace(warnPattern, '<div class="md-list-item"><span class="report-warning">$1$2</span></div>');
2345
+ warnPattern = /<div class="md-list-item"><span class="md-strong">(Warning|Consider|Caution|Recommendation)<\/span>([\s\S]*?)<\/div>/gi;
2346
+ html = html.replace(warnPattern, '<div class="md-list-item"><span class="report-warning"><span class="md-strong">$1</span>$2</span></div>');
2347
+ warnPattern = /<div class="md-h2">(Warning|Considerations|Recommendations|Improvements)([\s\S]*?)<\/div>/gi;
2348
+ html = html.replace(warnPattern, '<div class="md-h2 report-warning">$1$2</div>');
2349
+ // Required actions / error (red)
2350
+ var actionPattern = /<div class="md-list-item">(Error|Errors|Required|Must|Action required|Necessary action|Necessary actions|Critical|Fix|Should fix|🔴|TODO|Needs|Issue:|Vulnerability)([\s\S]*?)<\/div>/gi;
2351
+ html = html.replace(actionPattern, '<div class="md-list-item"><span class="report-action">$1$2</span></div>');
2352
+ actionPattern = /<div class="md-list-item"><span class="md-strong">(Error|Required|Must|Critical|Fix|Action|Necessary action)<\/span>([\s\S]*?)<\/div>/gi;
2353
+ html = html.replace(actionPattern, '<div class="md-list-item"><span class="report-action"><span class="md-strong">$1</span>$2</span></div>');
2354
+ actionPattern = /<div class="md-h2">(Error|Errors|Required actions|Necessary actions|Action items|Critical|Issues to fix)([\s\S]*?)<\/div>/gi;
2355
+ html = html.replace(actionPattern, '<div class="md-h2 report-action">$1$2</div>');
2356
+ // Best Practices Validation: show in black, not green
2357
+ html = html.replace(/<div class="md-h2 report-good">(Best Practices Validation)([\s\S]*?)<\/div>/gi, '<div class="md-h2">$1$2</div>');
2358
+ html = html.replace(/<span class="report-good">([\s\S]*?)<\/span>/gi, function (_m, inner) {
2359
+ return inner.indexOf('Best Practices Validation') !== -1 ? inner : _m;
2360
+ });
2361
+ return html;
2362
+ }
2363
+
2364
+ async function exportToPDF() {
2365
+ if (!lastAnalysisData) {
2366
+ showError('No analysis data available');
2367
+ return;
2368
+ }
2369
+
2370
+ const btn = document.getElementById('pdfBtn');
2371
+ btn.disabled = true;
2372
+ btn.textContent = '⏳ Generating PDF...';
2373
+
2374
+ try {
2375
+ const response = await fetch('/api/export-pdf', {
2376
+ method: 'POST',
2377
+ headers: { 'Content-Type': 'application/json' },
2378
+ body: JSON.stringify(lastAnalysisData),
2379
+ });
2380
+
2381
+ if (!response.ok) {
2382
+ const data = await response.json();
2383
+ throw new Error(data.error || 'Failed to generate PDF');
2384
+ }
2385
+
2386
+ const blob = await response.blob();
2387
+ const url = window.URL.createObjectURL(blob);
2388
+ const a = document.createElement('a');
2389
+ a.href = url;
2390
+ a.download = `${lastAnalysisData.repository.replace('/', '-')}-report.pdf`;
2391
+ document.body.appendChild(a);
2392
+ a.click();
2393
+ window.URL.revokeObjectURL(url);
2394
+ document.body.removeChild(a);
2395
+
2396
+ btn.disabled = false;
2397
+ btn.textContent = '📄 Export to PDF';
2398
+ showToast('PDF downloaded');
2399
+ } catch (error) {
2400
+ btn.disabled = false;
2401
+ btn.textContent = '📄 Export to PDF';
2402
+ showError(error.message);
2403
+ }
2404
+ }
2405
+
2406
+ function goBack() {
2407
+ document.getElementById('inputSection').style.display = 'block';
2408
+ document.getElementById('resultsDiv').style.display = 'none';
2409
+ var fh = document.getElementById('featureHighlights');
2410
+ if (fh) fh.style.display = '';
2411
+ document.getElementById('repoInput').value = '';
2412
+ lastAnalysisData = null;
2413
+
2414
+ // Reset form: uncheck all analysis option checkboxes
2415
+ var container = document.getElementById('analysisOptionsContainer');
2416
+ if (container) {
2417
+ container.querySelectorAll('input[type="checkbox"]').forEach(function (cb) { cb.checked = false; });
2418
+ }
2419
+ setAnalysisMode('sonar');
2420
+ setAnalyzeButtonState(true, 'Analyze');
2421
+ hideError();
2422
+
2423
+ // Scroll to top of the form
2424
+ var inputSection = document.getElementById('inputSection');
2425
+ if (inputSection) {
2426
+ inputSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
2427
+ } else {
2428
+ window.scrollTo({ top: 0, behavior: 'smooth' });
2429
+ }
2430
+ }
2431
+ </script>
2432
+ </body>
2433
+ </html>