mc-gitpulse 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.
@@ -0,0 +1,1361 @@
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
+ <title>GitPulse - Ultimate Git UI</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="icon" type="image/x-icon"
9
+ href="https://mohan-chinnappan-n5.github.io/dfv/img/mc_favIcon.ico" />
10
+
11
+
12
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
14
+ <style>
15
+ body {
16
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Droid Sans Mono', 'Source Code Pro', monospace;
17
+ }
18
+
19
+ .commit-graph {
20
+ font-family: monospace;
21
+ white-space: pre;
22
+ line-height: 1.4;
23
+ }
24
+
25
+ .tree-item {
26
+ cursor: pointer;
27
+ user-select: none;
28
+ }
29
+
30
+ .tree-item:hover {
31
+ background-color: rgba(59, 130, 246, 0.1);
32
+ }
33
+
34
+ .health-excellent { color: #10b981; }
35
+ .health-good { color: #3b82f6; }
36
+ .health-needs-attention { color: #f59e0b; }
37
+ .health-poor { color: #ef4444; }
38
+
39
+ @keyframes pulse {
40
+ 0%, 100% { opacity: 1; }
41
+ 50% { opacity: 0.5; }
42
+ }
43
+
44
+ .loading {
45
+ animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
46
+ }
47
+
48
+ .tab-content {
49
+ display: none;
50
+ }
51
+
52
+ .tab-content.active {
53
+ display: block;
54
+ }
55
+
56
+ .stat-card {
57
+ transition: transform 0.2s;
58
+ }
59
+
60
+ .stat-card:hover {
61
+ transform: translateY(-2px);
62
+ }
63
+
64
+ .split-pane {
65
+ display: flex;
66
+ height: 600px;
67
+ gap: 1rem;
68
+ }
69
+
70
+ .split-left {
71
+ flex: 0 0 400px;
72
+ overflow-y: auto;
73
+ border-right: 1px solid #374151;
74
+ }
75
+
76
+ .split-right {
77
+ flex: 1;
78
+ overflow: hidden;
79
+ display: flex;
80
+ flex-direction: column;
81
+ }
82
+
83
+ #monaco-editor {
84
+ height: 100%;
85
+ border: 1px solid #374151;
86
+ border-radius: 0.5rem;
87
+ overflow: hidden;
88
+ }
89
+
90
+ .file-item {
91
+ cursor: pointer;
92
+ padding: 0.5rem;
93
+ border-radius: 0.25rem;
94
+ transition: background-color 0.2s;
95
+ }
96
+
97
+ .file-item:hover {
98
+ background-color: rgba(59, 130, 246, 0.1);
99
+ }
100
+
101
+ .file-item.selected {
102
+ background-color: rgba(59, 130, 246, 0.2);
103
+ }
104
+
105
+ .folder-item {
106
+ cursor: pointer;
107
+ user-select: none;
108
+ }
109
+
110
+ .folder-item:hover {
111
+ background-color: rgba(59, 130, 246, 0.1);
112
+ }
113
+
114
+ .folder-content {
115
+ display: none;
116
+ margin-left: 1rem;
117
+ }
118
+
119
+ .folder-content.expanded {
120
+ display: block;
121
+ }
122
+
123
+ .commit-node {
124
+ font-family: monospace;
125
+ white-space: pre;
126
+ }
127
+
128
+ /* Modal/Popup Styles */
129
+ .modal-overlay {
130
+ display: none;
131
+ position: fixed;
132
+ top: 0;
133
+ left: 0;
134
+ right: 0;
135
+ bottom: 0;
136
+ background-color: rgba(0, 0, 0, 0.75);
137
+ z-index: 9998;
138
+ animation: fadeIn 0.2s ease-in;
139
+ }
140
+
141
+ .modal-overlay.active {
142
+ display: flex;
143
+ align-items: center;
144
+ justify-content: center;
145
+ }
146
+
147
+ .modal {
148
+ background: #1f2937;
149
+ border: 1px solid #374151;
150
+ border-radius: 0.75rem;
151
+ padding: 1.5rem;
152
+ max-width: 500px;
153
+ width: 90%;
154
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
155
+ animation: slideUp 0.3s ease-out;
156
+ position: relative;
157
+ }
158
+
159
+ .modal-header {
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 0.75rem;
163
+ margin-bottom: 1rem;
164
+ padding-bottom: 1rem;
165
+ border-bottom: 1px solid #374151;
166
+ }
167
+
168
+ .modal-icon {
169
+ font-size: 1.5rem;
170
+ }
171
+
172
+ .modal-title {
173
+ font-size: 1.25rem;
174
+ font-weight: 600;
175
+ color: #f3f4f6;
176
+ }
177
+
178
+ .modal-body {
179
+ color: #d1d5db;
180
+ line-height: 1.6;
181
+ margin-bottom: 1.5rem;
182
+ }
183
+
184
+ .modal-footer {
185
+ display: flex;
186
+ gap: 0.75rem;
187
+ justify-content: flex-end;
188
+ }
189
+
190
+ .modal-btn {
191
+ padding: 0.5rem 1.25rem;
192
+ border-radius: 0.5rem;
193
+ font-weight: 500;
194
+ cursor: pointer;
195
+ transition: all 0.2s;
196
+ border: none;
197
+ font-size: 0.875rem;
198
+ }
199
+
200
+ .modal-btn-primary {
201
+ background: #3b82f6;
202
+ color: white;
203
+ }
204
+
205
+ .modal-btn-primary:hover {
206
+ background: #2563eb;
207
+ }
208
+
209
+ .modal-btn-danger {
210
+ background: #ef4444;
211
+ color: white;
212
+ }
213
+
214
+ .modal-btn-danger:hover {
215
+ background: #dc2626;
216
+ }
217
+
218
+ .modal-btn-success {
219
+ background: #10b981;
220
+ color: white;
221
+ }
222
+
223
+ .modal-btn-success:hover {
224
+ background: #059669;
225
+ }
226
+
227
+ .modal-btn-secondary {
228
+ background: #4b5563;
229
+ color: white;
230
+ }
231
+
232
+ .modal-btn-secondary:hover {
233
+ background: #374151;
234
+ }
235
+
236
+ @keyframes fadeIn {
237
+ from {
238
+ opacity: 0;
239
+ }
240
+ to {
241
+ opacity: 1;
242
+ }
243
+ }
244
+
245
+ @keyframes slideUp {
246
+ from {
247
+ transform: translateY(20px);
248
+ opacity: 0;
249
+ }
250
+ to {
251
+ transform: translateY(0);
252
+ opacity: 1;
253
+ }
254
+ }
255
+
256
+ /* Toast notification styles */
257
+ .toast-container {
258
+ position: fixed;
259
+ top: 5rem;
260
+ right: 1.5rem;
261
+ z-index: 9999;
262
+ display: flex;
263
+ flex-direction: column;
264
+ gap: 0.75rem;
265
+ }
266
+
267
+ .toast {
268
+ background: #1f2937;
269
+ border: 1px solid #374151;
270
+ border-left: 4px solid #3b82f6;
271
+ border-radius: 0.5rem;
272
+ padding: 1rem 1.25rem;
273
+ min-width: 300px;
274
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
275
+ animation: slideInRight 0.3s ease-out;
276
+ display: flex;
277
+ align-items: center;
278
+ gap: 0.75rem;
279
+ }
280
+
281
+ .toast.success {
282
+ border-left-color: #10b981;
283
+ }
284
+
285
+ .toast.error {
286
+ border-left-color: #ef4444;
287
+ }
288
+
289
+ .toast.warning {
290
+ border-left-color: #f59e0b;
291
+ }
292
+
293
+ .toast-icon {
294
+ font-size: 1.25rem;
295
+ flex-shrink: 0;
296
+ }
297
+
298
+ .toast-content {
299
+ flex: 1;
300
+ color: #f3f4f6;
301
+ }
302
+
303
+ .toast-close {
304
+ cursor: pointer;
305
+ color: #9ca3af;
306
+ font-size: 1.25rem;
307
+ flex-shrink: 0;
308
+ transition: color 0.2s;
309
+ }
310
+
311
+ .toast-close:hover {
312
+ color: #f3f4f6;
313
+ }
314
+
315
+ @keyframes slideInRight {
316
+ from {
317
+ transform: translateX(400px);
318
+ opacity: 0;
319
+ }
320
+ to {
321
+ transform: translateX(0);
322
+ opacity: 1;
323
+ }
324
+ }
325
+
326
+ scrollbar-width: thin;
327
+ scrollbar-color: #4b5563 #1f2937;
328
+
329
+ ::-webkit-scrollbar {
330
+ width: 8px;
331
+ height: 8px;
332
+ }
333
+
334
+ ::-webkit-scrollbar-track {
335
+ background: #1f2937;
336
+ }
337
+
338
+ ::-webkit-scrollbar-thumb {
339
+ background: #4b5563;
340
+ border-radius: 4px;
341
+ }
342
+
343
+ ::-webkit-scrollbar-thumb:hover {
344
+ background: #6b7280;
345
+ }
346
+ </style>
347
+ </head>
348
+ <body class="bg-gray-900 text-gray-100 min-h-screen">
349
+ <!-- Header -->
350
+ <header class="bg-gray-800 border-b border-gray-700 sticky top-0 z-50">
351
+ <div class="container mx-auto px-6 py-4">
352
+ <div class="flex items-center justify-between">
353
+ <div class="flex items-center space-x-4">
354
+ <h1 class="text-2xl font-bold text-blue-400">โšก GitPulse</h1>
355
+ <span class="text-sm text-gray-400 hidden sm:inline">The Ultimate Git Repository Analyzer</span>
356
+ </div>
357
+ <div class="flex items-center space-x-4">
358
+ <button onclick="refreshData()" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition flex items-center space-x-2">
359
+ <span>๐Ÿ”„</span>
360
+ <span>Refresh</span>
361
+ </button>
362
+ <div id="connection-status" class="flex items-center space-x-2">
363
+ <div class="w-2 h-2 bg-green-500 rounded-full"></div>
364
+ <span class="text-sm text-gray-400">Connected</span>
365
+ </div>
366
+ </div>
367
+ </div>
368
+ </div>
369
+ </header>
370
+
371
+ <!-- Loading Screen -->
372
+ <div id="loading" class="flex items-center justify-center h-screen">
373
+ <div class="text-center">
374
+ <div class="text-6xl mb-4 loading">โšก</div>
375
+ <p class="text-xl text-gray-400">Analyzing repository...</p>
376
+ </div>
377
+ </div>
378
+
379
+ <!-- Main Content -->
380
+ <div id="content" class="hidden">
381
+ <!-- Repository Info Bar -->
382
+ <div class="bg-gray-800 border-b border-gray-700">
383
+ <div class="container mx-auto px-6 py-4">
384
+ <div class="flex flex-wrap items-center gap-4">
385
+ <div class="flex items-center space-x-2">
386
+ <span class="text-gray-400">๐Ÿ“</span>
387
+ <span id="repo-path" class="text-sm font-mono"></span>
388
+ </div>
389
+ <div class="flex items-center space-x-2">
390
+ <span class="text-gray-400">๐ŸŒฟ</span>
391
+ <span id="current-branch" class="text-sm font-semibold text-green-400"></span>
392
+ </div>
393
+ <div id="repo-status" class="flex items-center space-x-2"></div>
394
+ </div>
395
+ </div>
396
+ </div>
397
+
398
+ <!-- Health Score Banner -->
399
+ <div id="health-banner" class="container mx-auto px-6 py-4"></div>
400
+
401
+ <!-- Navigation Tabs -->
402
+ <div class="bg-gray-800 border-b border-gray-700">
403
+ <div class="container mx-auto px-6">
404
+ <nav class="flex space-x-8" id="tabs">
405
+ <button class="tab-btn py-4 border-b-2 border-blue-500 text-blue-400" data-tab="overview">Overview</button>
406
+ <button class="tab-btn py-4 border-b-2 border-transparent hover:border-gray-600" data-tab="commits">Commits</button>
407
+ <button class="tab-btn py-4 border-b-2 border-transparent hover:border-gray-600" data-tab="graph">Git Graph</button>
408
+ <button class="tab-btn py-4 border-b-2 border-transparent hover:border-gray-600" data-tab="branches">Branches</button>
409
+ <button class="tab-btn py-4 border-b-2 border-transparent hover:border-gray-600" data-tab="tree">File Tree</button>
410
+ <button class="tab-btn py-4 border-b-2 border-transparent hover:border-gray-600" data-tab="stats">Statistics</button>
411
+ <button class="tab-btn py-4 border-b-2 border-transparent hover:border-gray-600" data-tab="optimize">Optimize</button>
412
+ </nav>
413
+ </div>
414
+ </div>
415
+
416
+ <!-- Tab Contents -->
417
+ <div class="container mx-auto px-6 py-8">
418
+ <!-- Overview Tab -->
419
+ <div id="overview-tab" class="tab-content active">
420
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
421
+ <div class="stat-card bg-gray-800 rounded-lg p-6 border border-gray-700">
422
+ <div class="text-gray-400 text-sm mb-2">Total Commits</div>
423
+ <div id="stat-commits" class="text-3xl font-bold text-blue-400">-</div>
424
+ </div>
425
+ <div class="stat-card bg-gray-800 rounded-lg p-6 border border-gray-700">
426
+ <div class="text-gray-400 text-sm mb-2">Contributors</div>
427
+ <div id="stat-contributors" class="text-3xl font-bold text-green-400">-</div>
428
+ </div>
429
+ <div class="stat-card bg-gray-800 rounded-lg p-6 border border-gray-700">
430
+ <div class="text-gray-400 text-sm mb-2">Branches</div>
431
+ <div id="stat-branches" class="text-3xl font-bold text-purple-400">-</div>
432
+ </div>
433
+ <div class="stat-card bg-gray-800 rounded-lg p-6 border border-gray-700">
434
+ <div class="text-gray-400 text-sm mb-2">Repository Size</div>
435
+ <div id="stat-size" class="text-3xl font-bold text-yellow-400">-</div>
436
+ </div>
437
+ </div>
438
+
439
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
440
+ <!-- Recent Activity -->
441
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
442
+ <h3 class="text-xl font-bold mb-4">๐Ÿ“Š Recent Activity</h3>
443
+ <div class="space-y-3">
444
+ <div class="flex justify-between">
445
+ <span class="text-gray-400">Last 24 hours</span>
446
+ <span id="activity-day" class="font-semibold">-</span>
447
+ </div>
448
+ <div class="flex justify-between">
449
+ <span class="text-gray-400">Last 7 days</span>
450
+ <span id="activity-week" class="font-semibold">-</span>
451
+ </div>
452
+ <div class="flex justify-between">
453
+ <span class="text-gray-400">Last 30 days</span>
454
+ <span id="activity-month" class="font-semibold">-</span>
455
+ </div>
456
+ <div class="flex justify-between">
457
+ <span class="text-gray-400">Avg commits/day</span>
458
+ <span id="activity-avg" class="font-semibold">-</span>
459
+ </div>
460
+ </div>
461
+ </div>
462
+
463
+ <!-- Top Contributors -->
464
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
465
+ <h3 class="text-xl font-bold mb-4">๐Ÿ‘ฅ Top Contributors</h3>
466
+ <div id="contributors-list" class="space-y-3"></div>
467
+ </div>
468
+ </div>
469
+ </div>
470
+
471
+ <!-- Commits Tab -->
472
+ <div id="commits-tab" class="tab-content">
473
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
474
+ <div class="flex justify-between items-center mb-4">
475
+ <h3 class="text-xl font-bold">๐Ÿ“ Commit History</h3>
476
+ <div class="text-sm text-gray-400">Showing last 50 commits</div>
477
+ </div>
478
+ <div id="commits-list" class="space-y-2 max-h-[600px] overflow-y-auto"></div>
479
+ </div>
480
+ </div>
481
+
482
+ <!-- Git Graph Tab -->
483
+ <div id="graph-tab" class="tab-content">
484
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
485
+ <div class="flex justify-between items-center mb-4">
486
+ <h3 class="text-xl font-bold">๐ŸŒณ Git Graph</h3>
487
+ <div class="flex items-center space-x-4">
488
+ <select id="graph-limit" class="bg-gray-700 border border-gray-600 rounded px-3 py-1 text-sm">
489
+ <option value="50">Last 50 commits</option>
490
+ <option value="100" selected>Last 100 commits</option>
491
+ <option value="200">Last 200 commits</option>
492
+ <option value="500">Last 500 commits</option>
493
+ </select>
494
+ <button onclick="loadGitGraph()" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm">Refresh</button>
495
+ </div>
496
+ </div>
497
+ <div id="git-graph" class="bg-gray-900 rounded p-4 font-mono text-sm max-h-[700px] overflow-auto"></div>
498
+ </div>
499
+ </div>
500
+
501
+ <!-- Branches Tab -->
502
+ <div id="branches-tab" class="tab-content">
503
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
504
+ <h3 class="text-xl font-bold mb-4">๐ŸŒฟ Branches</h3>
505
+ <div id="branches-list" class="space-y-2"></div>
506
+ </div>
507
+ </div>
508
+
509
+ <!-- File Tree Tab -->
510
+ <div id="tree-tab" class="tab-content">
511
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
512
+ <h3 class="text-xl font-bold mb-4">๐Ÿ“‚ File Tree</h3>
513
+ <div class="split-pane">
514
+ <div class="split-left bg-gray-900 rounded p-4">
515
+ <div id="file-tree" class="font-mono text-sm"></div>
516
+ </div>
517
+ <div class="split-right">
518
+ <div id="file-info" class="mb-2 text-sm text-gray-400">Select a file to view</div>
519
+ <div id="monaco-editor"></div>
520
+ </div>
521
+ </div>
522
+ </div>
523
+ </div>
524
+
525
+ <!-- Statistics Tab -->
526
+ <div id="stats-tab" class="tab-content">
527
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
528
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
529
+ <h3 class="text-xl font-bold mb-4">๐Ÿ“… Commits by Day of Week</h3>
530
+ <canvas id="day-chart"></canvas>
531
+ </div>
532
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
533
+ <h3 class="text-xl font-bold mb-4">๐Ÿ• Commits by Hour of Day</h3>
534
+ <canvas id="hour-chart"></canvas>
535
+ </div>
536
+ </div>
537
+ </div>
538
+
539
+ <!-- Optimize Tab -->
540
+ <div id="optimize-tab" class="tab-content">
541
+ <div class="bg-gray-800 rounded-lg p-6 border border-gray-700">
542
+ <h3 class="text-xl font-bold mb-6">๐Ÿš€ Repository Optimization</h3>
543
+
544
+ <div class="space-y-6">
545
+ <div id="garbage-info" class="bg-gray-900 rounded-lg p-6 border border-gray-700"></div>
546
+
547
+ <div id="health-details" class="bg-gray-900 rounded-lg p-6 border border-gray-700"></div>
548
+
549
+ <div class="flex space-x-4">
550
+ <button onclick="runGarbageCollection()" class="px-6 py-3 bg-green-600 hover:bg-green-700 rounded-lg transition font-semibold">
551
+ ๐Ÿงน Run Garbage Collection
552
+ </button>
553
+ </div>
554
+ </div>
555
+ </div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+
560
+ <!-- Modal Overlay -->
561
+ <div id="modal-overlay" class="modal-overlay">
562
+ <div class="modal" id="modal">
563
+ <div class="modal-header">
564
+ <span class="modal-icon" id="modal-icon">โ„น๏ธ</span>
565
+ <h3 class="modal-title" id="modal-title">Notice</h3>
566
+ </div>
567
+ <div class="modal-body" id="modal-body">
568
+ This is a message.
569
+ </div>
570
+ <div class="modal-footer" id="modal-footer">
571
+ <button class="modal-btn modal-btn-primary" onclick="closeModal()">OK</button>
572
+ </div>
573
+ </div>
574
+ </div>
575
+
576
+ <!-- Toast Container -->
577
+ <div id="toast-container" class="toast-container"></div>
578
+
579
+ <script>
580
+ let ws;
581
+ let currentData = null;
582
+ let monacoEditor = null;
583
+ let allFiles = [];
584
+ let modalCallback = null;
585
+
586
+ // Modal Functions
587
+ function showModal(options) {
588
+ const {
589
+ title = 'Notice',
590
+ message = '',
591
+ icon = 'โ„น๏ธ',
592
+ type = 'info', // info, success, error, warning, confirm
593
+ confirmText = 'OK',
594
+ cancelText = 'Cancel',
595
+ onConfirm = null,
596
+ onCancel = null
597
+ } = options;
598
+
599
+ const overlay = document.getElementById('modal-overlay');
600
+ const modal = document.getElementById('modal');
601
+ const modalIcon = document.getElementById('modal-icon');
602
+ const modalTitle = document.getElementById('modal-title');
603
+ const modalBody = document.getElementById('modal-body');
604
+ const modalFooter = document.getElementById('modal-footer');
605
+
606
+ // Set content
607
+ modalIcon.textContent = icon;
608
+ modalTitle.textContent = title;
609
+ modalBody.innerHTML = message;
610
+
611
+ // Build footer buttons
612
+ let footerHTML = '';
613
+
614
+ if (type === 'confirm') {
615
+ footerHTML = `
616
+ <button class="modal-btn modal-btn-secondary" onclick="handleModalCancel()">
617
+ ${cancelText}
618
+ </button>
619
+ <button class="modal-btn modal-btn-primary" onclick="handleModalConfirm()">
620
+ ${confirmText}
621
+ </button>
622
+ `;
623
+ } else if (type === 'error') {
624
+ footerHTML = `
625
+ <button class="modal-btn modal-btn-danger" onclick="closeModal()">
626
+ ${confirmText}
627
+ </button>
628
+ `;
629
+ } else if (type === 'success') {
630
+ footerHTML = `
631
+ <button class="modal-btn modal-btn-success" onclick="closeModal()">
632
+ ${confirmText}
633
+ </button>
634
+ `;
635
+ } else {
636
+ footerHTML = `
637
+ <button class="modal-btn modal-btn-primary" onclick="closeModal()">
638
+ ${confirmText}
639
+ </button>
640
+ `;
641
+ }
642
+
643
+ modalFooter.innerHTML = footerHTML;
644
+
645
+ // Store callbacks
646
+ modalCallback = { onConfirm, onCancel };
647
+
648
+ // Show modal
649
+ overlay.classList.add('active');
650
+
651
+ // Close on overlay click
652
+ overlay.onclick = (e) => {
653
+ if (e.target === overlay) {
654
+ closeModal();
655
+ }
656
+ };
657
+ }
658
+
659
+ function closeModal() {
660
+ const overlay = document.getElementById('modal-overlay');
661
+ overlay.classList.remove('active');
662
+ modalCallback = null;
663
+ }
664
+
665
+ function handleModalConfirm() {
666
+ if (modalCallback && modalCallback.onConfirm) {
667
+ modalCallback.onConfirm();
668
+ }
669
+ closeModal();
670
+ }
671
+
672
+ function handleModalCancel() {
673
+ if (modalCallback && modalCallback.onCancel) {
674
+ modalCallback.onCancel();
675
+ }
676
+ closeModal();
677
+ }
678
+
679
+ // Toast Notification Functions
680
+ function showToast(message, type = 'info', duration = 4000) {
681
+ const container = document.getElementById('toast-container');
682
+ const toast = document.createElement('div');
683
+ toast.className = `toast ${type}`;
684
+
685
+ const icons = {
686
+ success: 'โœ“',
687
+ error: 'โœ—',
688
+ warning: 'โš ',
689
+ info: 'โ„น'
690
+ };
691
+
692
+ const icon = icons[type] || icons.info;
693
+
694
+ toast.innerHTML = `
695
+ <span class="toast-icon">${icon}</span>
696
+ <div class="toast-content">${message}</div>
697
+ <span class="toast-close" onclick="this.parentElement.remove()">ร—</span>
698
+ `;
699
+
700
+ container.appendChild(toast);
701
+
702
+ // Auto remove after duration
703
+ setTimeout(() => {
704
+ toast.style.animation = 'slideInRight 0.3s ease-out reverse';
705
+ setTimeout(() => toast.remove(), 300);
706
+ }, duration);
707
+ }
708
+
709
+ // Initialize
710
+ document.addEventListener('DOMContentLoaded', () => {
711
+ initWebSocket();
712
+ loadData();
713
+ setupTabs();
714
+ initMonaco();
715
+ });
716
+
717
+ function initMonaco() {
718
+ require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
719
+
720
+ require(['vs/editor/editor.main'], function() {
721
+ monacoEditor = monaco.editor.create(document.getElementById('monaco-editor'), {
722
+ value: '// Select a file from the tree to view its contents',
723
+ language: 'javascript',
724
+ theme: 'vs-dark',
725
+ automaticLayout: true,
726
+ readOnly: true,
727
+ minimap: { enabled: true },
728
+ fontSize: 14,
729
+ lineNumbers: 'on',
730
+ scrollBeyondLastLine: false,
731
+ wordWrap: 'on'
732
+ });
733
+ });
734
+ }
735
+
736
+ async function loadFileContent(filepath) {
737
+ try {
738
+ showToast(`Loading ${filepath.split('/').pop()}...`, 'info', 1500);
739
+
740
+ const ref = currentData.branches.current;
741
+ const response = await fetch(`/api/file/${ref}/${filepath}`);
742
+ const data = await response.json();
743
+
744
+ if (data.content) {
745
+ // Detect language from file extension
746
+ const ext = filepath.split('.').pop().toLowerCase();
747
+ const languageMap = {
748
+ js: 'javascript', ts: 'typescript', jsx: 'javascript', tsx: 'typescript',
749
+ py: 'python', java: 'java', cpp: 'cpp', c: 'c', cs: 'csharp',
750
+ html: 'html', css: 'css', scss: 'scss', sass: 'sass',
751
+ json: 'json', xml: 'xml', yaml: 'yaml', yml: 'yaml',
752
+ md: 'markdown', sh: 'shell', bash: 'shell',
753
+ sql: 'sql', php: 'php', rb: 'ruby', go: 'go',
754
+ rs: 'rust', swift: 'swift', kt: 'kotlin',
755
+ vue: 'html', svelte: 'html'
756
+ };
757
+
758
+ const language = languageMap[ext] || 'plaintext';
759
+
760
+ monaco.editor.setModelLanguage(monacoEditor.getModel(), language);
761
+ monacoEditor.setValue(data.content);
762
+
763
+ document.getElementById('file-info').innerHTML = `
764
+ <div class="flex items-center justify-between">
765
+ <span class="text-blue-400">๐Ÿ“„ ${escapeHtml(filepath)}</span>
766
+ <span class="text-gray-500">${language}</span>
767
+ </div>
768
+ `;
769
+
770
+ showToast('File loaded successfully', 'success', 2000);
771
+ }
772
+ } catch (error) {
773
+ monacoEditor.setValue(`// Error loading file: ${error.message}`);
774
+ document.getElementById('file-info').innerHTML = `
775
+ <span class="text-red-400">โŒ Error loading ${escapeHtml(filepath)}</span>
776
+ `;
777
+ showToast(`Failed to load ${filepath.split('/').pop()}`, 'error', 3000);
778
+ }
779
+ }
780
+
781
+ function initWebSocket() {
782
+ ws = new WebSocket(`ws://${window.location.host}`);
783
+
784
+ ws.onopen = () => {
785
+ updateConnectionStatus(true);
786
+ };
787
+
788
+ ws.onclose = () => {
789
+ updateConnectionStatus(false);
790
+ setTimeout(initWebSocket, 3000);
791
+ };
792
+
793
+ ws.onmessage = (event) => {
794
+ const data = JSON.parse(event.data);
795
+ if (data.type === 'update') {
796
+ currentData = data.data;
797
+ renderData();
798
+ showToast('Repository data updated', 'success', 2000);
799
+ }
800
+ };
801
+ }
802
+
803
+ function updateConnectionStatus(connected) {
804
+ const status = document.getElementById('connection-status');
805
+ if (connected) {
806
+ status.innerHTML = `
807
+ <div class="w-2 h-2 bg-green-500 rounded-full"></div>
808
+ <span class="text-sm text-gray-400">Connected</span>
809
+ `;
810
+ } else {
811
+ status.innerHTML = `
812
+ <div class="w-2 h-2 bg-red-500 rounded-full"></div>
813
+ <span class="text-sm text-gray-400">Disconnected</span>
814
+ `;
815
+ }
816
+ }
817
+
818
+ async function loadData() {
819
+ try {
820
+ const response = await fetch('/api/analyze');
821
+ currentData = await response.json();
822
+ renderData();
823
+ document.getElementById('loading').classList.add('hidden');
824
+ document.getElementById('content').classList.remove('hidden');
825
+
826
+ // Show welcome toast
827
+ showToast('โœจ Repository loaded successfully!', 'success', 3000);
828
+ } catch (error) {
829
+ console.error('Error loading data:', error);
830
+ showModal({
831
+ type: 'error',
832
+ title: 'Error',
833
+ icon: 'โŒ',
834
+ message: `Failed to load repository data: ${error.message}`,
835
+ confirmText: 'Close'
836
+ });
837
+ }
838
+ }
839
+
840
+ function refreshData() {
841
+ showToast('Refreshing repository data...', 'info', 2000);
842
+
843
+ if (ws && ws.readyState === WebSocket.OPEN) {
844
+ ws.send(JSON.stringify({ type: 'refresh' }));
845
+ } else {
846
+ loadData();
847
+ }
848
+ }
849
+
850
+ function renderData() {
851
+ if (!currentData) return;
852
+
853
+ // Repository info
854
+ document.getElementById('repo-path').textContent = currentData.path;
855
+ document.getElementById('current-branch').textContent = currentData.branches.current;
856
+
857
+ // Status
858
+ const status = currentData.status;
859
+ let statusHTML = '';
860
+ if (status.isClean) {
861
+ statusHTML = '<span class="text-green-400">โœ“ Clean</span>';
862
+ } else {
863
+ const changes = [];
864
+ if (status.modified.length) changes.push(`${status.modified.length} modified`);
865
+ if (status.created.length) changes.push(`${status.created.length} created`);
866
+ if (status.deleted.length) changes.push(`${status.deleted.length} deleted`);
867
+ statusHTML = `<span class="text-yellow-400">โš  ${changes.join(', ')}</span>`;
868
+ }
869
+ document.getElementById('repo-status').innerHTML = statusHTML;
870
+
871
+ // Health banner
872
+ renderHealthBanner();
873
+
874
+ // Stats
875
+ document.getElementById('stat-commits').textContent = currentData.commitCount.toLocaleString();
876
+ document.getElementById('stat-contributors').textContent = currentData.contributors.length;
877
+ document.getElementById('stat-branches').textContent = currentData.branches.all.length;
878
+ document.getElementById('stat-size').textContent = currentData.repoSize.mb + ' MB';
879
+
880
+ // Activity
881
+ document.getElementById('activity-day').textContent = currentData.stats.commitsLastDay + ' commits';
882
+ document.getElementById('activity-week').textContent = currentData.stats.commitsLastWeek + ' commits';
883
+ document.getElementById('activity-month').textContent = currentData.stats.commitsLastMonth + ' commits';
884
+ document.getElementById('activity-avg').textContent = currentData.stats.avgCommitsPerDay;
885
+
886
+ // Contributors
887
+ renderContributors();
888
+ renderCommits();
889
+ renderBranches();
890
+ renderFileTree();
891
+ renderCharts();
892
+ renderOptimization();
893
+ loadGitGraph();
894
+ }
895
+
896
+ async function loadGitGraph() {
897
+ try {
898
+ const limit = document.getElementById('graph-limit')?.value || 100;
899
+ const response = await fetch(`/api/graph?limit=${limit}`);
900
+ const graph = await response.json();
901
+
902
+ const graphElement = document.getElementById('git-graph');
903
+ if (!graphElement) return;
904
+
905
+ graphElement.innerHTML = graph.map(commit => {
906
+ const graphPart = commit.graph.replace(/\*/g, '<span class="text-yellow-400">โ—</span>')
907
+ .replace(/\|/g, '<span class="text-blue-400">โ”‚</span>')
908
+ .replace(/\//g, '<span class="text-green-400">โ•ฑ</span>')
909
+ .replace(/\\/g, '<span class="text-purple-400">โ•ฒ</span>');
910
+
911
+ return `
912
+ <div class="commit-node hover:bg-gray-800 py-1 px-2 rounded">
913
+ <span class="text-gray-400">${graphPart}</span>
914
+ <span class="text-yellow-500 mx-2">${commit.hash.substring(0, 7)}</span>
915
+ <span class="text-gray-300">${escapeHtml(commit.subject)}</span>
916
+ <span class="text-blue-400 ml-2">(${escapeHtml(commit.author)})</span>
917
+ <span class="text-gray-500 ml-2">${escapeHtml(commit.date)}</span>
918
+ </div>
919
+ `;
920
+ }).join('');
921
+ } catch (error) {
922
+ console.error('Error loading git graph:', error);
923
+ const graphElement = document.getElementById('git-graph');
924
+ if (graphElement) {
925
+ graphElement.innerHTML = '<div class="text-red-400">Error loading git graph</div>';
926
+ }
927
+ }
928
+ }
929
+
930
+ function renderData() {
931
+ if (!currentData) return;
932
+
933
+ // Repository info
934
+ document.getElementById('repo-path').textContent = currentData.path;
935
+ document.getElementById('current-branch').textContent = currentData.branches.current;
936
+
937
+ // Status
938
+ const status = currentData.status;
939
+ let statusHTML = '';
940
+ if (status.isClean) {
941
+ statusHTML = '<span class="text-green-400">โœ“ Clean</span>';
942
+ } else {
943
+ const changes = [];
944
+ if (status.modified.length) changes.push(`${status.modified.length} modified`);
945
+ if (status.created.length) changes.push(`${status.created.length} created`);
946
+ if (status.deleted.length) changes.push(`${status.deleted.length} deleted`);
947
+ statusHTML = `<span class="text-yellow-400">โš  ${changes.join(', ')}</span>`;
948
+ }
949
+ document.getElementById('repo-status').innerHTML = statusHTML;
950
+
951
+ // Health banner
952
+ renderHealthBanner();
953
+
954
+ // Stats
955
+ document.getElementById('stat-commits').textContent = currentData.commitCount.toLocaleString();
956
+ document.getElementById('stat-contributors').textContent = currentData.contributors.length;
957
+ document.getElementById('stat-branches').textContent = currentData.branches.all.length;
958
+ document.getElementById('stat-size').textContent = currentData.repoSize.mb + ' MB';
959
+
960
+ // Activity
961
+ document.getElementById('activity-day').textContent = currentData.stats.commitsLastDay + ' commits';
962
+ document.getElementById('activity-week').textContent = currentData.stats.commitsLastWeek + ' commits';
963
+ document.getElementById('activity-month').textContent = currentData.stats.commitsLastMonth + ' commits';
964
+ document.getElementById('activity-avg').textContent = currentData.stats.avgCommitsPerDay;
965
+
966
+ // Contributors
967
+ renderContributors();
968
+ renderCommits();
969
+ renderBranches();
970
+ renderFileTree();
971
+ renderCharts();
972
+ renderOptimization();
973
+ }
974
+
975
+ function renderHealthBanner() {
976
+ const health = currentData.health;
977
+ const colors = {
978
+ excellent: 'bg-green-900 border-green-700 text-green-300',
979
+ good: 'bg-blue-900 border-blue-700 text-blue-300',
980
+ 'needs attention': 'bg-yellow-900 border-yellow-700 text-yellow-300',
981
+ poor: 'bg-red-900 border-red-700 text-red-300'
982
+ };
983
+
984
+ const colorClass = colors[health.status] || colors.good;
985
+
986
+ document.getElementById('health-banner').innerHTML = `
987
+ <div class="rounded-lg p-4 border ${colorClass}">
988
+ <div class="flex items-center justify-between">
989
+ <div class="flex items-center space-x-4">
990
+ <div class="text-3xl font-bold">${health.score}</div>
991
+ <div>
992
+ <div class="font-semibold text-lg">Repository Health: ${health.status.toUpperCase()}</div>
993
+ ${health.issues.length ? `<div class="text-sm opacity-80">${health.issues.join(', ')}</div>` : ''}
994
+ </div>
995
+ </div>
996
+ ${health.suggestions.length ? `
997
+ <button onclick="switchTab('optimize')" class="px-4 py-2 bg-white bg-opacity-20 hover:bg-opacity-30 rounded transition">
998
+ View Suggestions
999
+ </button>
1000
+ ` : ''}
1001
+ </div>
1002
+ </div>
1003
+ `;
1004
+ }
1005
+
1006
+ function renderContributors() {
1007
+ const list = document.getElementById('contributors-list');
1008
+ const top = currentData.contributors.slice(0, 5);
1009
+
1010
+ list.innerHTML = top.map(c => `
1011
+ <div class="flex justify-between items-center">
1012
+ <span class="text-gray-300">${escapeHtml(c.name)}</span>
1013
+ <span class="text-blue-400 font-semibold">${c.commits} commits</span>
1014
+ </div>
1015
+ `).join('');
1016
+ }
1017
+
1018
+ function renderCommits() {
1019
+ const list = document.getElementById('commits-list');
1020
+
1021
+ list.innerHTML = currentData.recentCommits.map(commit => `
1022
+ <div class="bg-gray-900 rounded p-4 border border-gray-700 hover:border-gray-600 transition">
1023
+ <div class="flex items-start justify-between mb-2">
1024
+ <div class="font-semibold text-blue-400">${escapeHtml(commit.message)}</div>
1025
+ <div class="text-xs text-gray-500 font-mono">${commit.hash.substring(0, 7)}</div>
1026
+ </div>
1027
+ <div class="text-sm text-gray-400">
1028
+ <span>${escapeHtml(commit.author_name)}</span>
1029
+ <span class="mx-2">โ€ข</span>
1030
+ <span>${new Date(commit.date).toLocaleString()}</span>
1031
+ </div>
1032
+ </div>
1033
+ `).join('');
1034
+ }
1035
+
1036
+ function renderBranches() {
1037
+ const list = document.getElementById('branches-list');
1038
+
1039
+ list.innerHTML = currentData.branches.all.filter(b => !b.remote).map(branch => `
1040
+ <div class="bg-gray-900 rounded p-4 border ${branch.current ? 'border-green-500' : 'border-gray-700'} hover:border-gray-600 transition">
1041
+ <div class="flex items-center justify-between">
1042
+ <div class="flex items-center space-x-3">
1043
+ ${branch.current ? '<span class="text-green-400">โ˜…</span>' : '<span class="text-gray-600">โ—‹</span>'}
1044
+ <span class="font-semibold ${branch.current ? 'text-green-400' : 'text-gray-300'}">${escapeHtml(branch.name)}</span>
1045
+ </div>
1046
+ <div class="text-xs text-gray-500 font-mono">${branch.commit.substring(0, 7)}</div>
1047
+ </div>
1048
+ </div>
1049
+ `).join('');
1050
+ }
1051
+
1052
+ async function renderFileTree() {
1053
+ try {
1054
+ const response = await fetch('/api/tree');
1055
+ const tree = await response.json();
1056
+
1057
+ allFiles = [];
1058
+ const treeElement = document.getElementById('file-tree');
1059
+ treeElement.innerHTML = renderTreeNode(tree, '', 0);
1060
+ } catch (error) {
1061
+ console.error('Error loading file tree:', error);
1062
+ }
1063
+ }
1064
+
1065
+ function renderTreeNode(node, path, indent = 0) {
1066
+ let html = '';
1067
+ const spacing = ' '.repeat(indent);
1068
+ const entries = Object.entries(node).filter(([key]) => key !== '_files');
1069
+
1070
+ // Render directories
1071
+ entries.forEach(([key, value]) => {
1072
+ const folderId = `folder-${path}${key}`.replace(/[^a-zA-Z0-9]/g, '-');
1073
+ html += `
1074
+ <div class="folder-item py-1 px-2 rounded" style="padding-left: ${indent * 20}px" onclick="toggleFolder('${folderId}')">
1075
+ <span class="text-blue-400">
1076
+ <span id="${folderId}-icon">โ–ถ</span> ๐Ÿ“ ${escapeHtml(key)}/
1077
+ </span>
1078
+ </div>
1079
+ <div id="${folderId}" class="folder-content">
1080
+ ${renderTreeNode(value, path + key + '/', indent + 1)}
1081
+ </div>
1082
+ `;
1083
+ });
1084
+
1085
+ // Render files
1086
+ if (node._files) {
1087
+ node._files.forEach(file => {
1088
+ const filepath = path + file;
1089
+ allFiles.push(filepath);
1090
+ const fileId = `file-${filepath}`.replace(/[^a-zA-Z0-9]/g, '-');
1091
+ html += `
1092
+ <div id="${fileId}" class="file-item py-1 px-2 rounded" style="padding-left: ${(indent + 1) * 20}px"
1093
+ onclick="selectFile('${escapeHtml(filepath)}', '${fileId}')">
1094
+ <span class="text-gray-400">๐Ÿ“„ ${escapeHtml(file)}</span>
1095
+ </div>
1096
+ `;
1097
+ });
1098
+ }
1099
+
1100
+ return html;
1101
+ }
1102
+
1103
+ function toggleFolder(folderId) {
1104
+ const folder = document.getElementById(folderId);
1105
+ const icon = document.getElementById(folderId + '-icon');
1106
+
1107
+ if (folder.classList.contains('expanded')) {
1108
+ folder.classList.remove('expanded');
1109
+ icon.textContent = 'โ–ถ';
1110
+ } else {
1111
+ folder.classList.add('expanded');
1112
+ icon.textContent = 'โ–ผ';
1113
+ }
1114
+ }
1115
+
1116
+ function selectFile(filepath, fileId) {
1117
+ // Remove selection from all files
1118
+ document.querySelectorAll('.file-item').forEach(el => {
1119
+ el.classList.remove('selected');
1120
+ });
1121
+
1122
+ // Add selection to clicked file
1123
+ document.getElementById(fileId).classList.add('selected');
1124
+
1125
+ // Load file content in Monaco Editor
1126
+ loadFileContent(filepath);
1127
+ }
1128
+
1129
+
1130
+ function renderCharts() {
1131
+ // Day of week chart
1132
+ const dayCtx = document.getElementById('day-chart').getContext('2d');
1133
+ new Chart(dayCtx, {
1134
+ type: 'bar',
1135
+ data: {
1136
+ labels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
1137
+ datasets: [{
1138
+ label: 'Commits',
1139
+ data: currentData.stats.dayOfWeek,
1140
+ backgroundColor: 'rgba(59, 130, 246, 0.8)',
1141
+ borderColor: 'rgba(59, 130, 246, 1)',
1142
+ borderWidth: 1
1143
+ }]
1144
+ },
1145
+ options: {
1146
+ responsive: true,
1147
+ maintainAspectRatio: true,
1148
+ plugins: {
1149
+ legend: { display: false }
1150
+ },
1151
+ scales: {
1152
+ y: {
1153
+ beginAtZero: true,
1154
+ ticks: { color: '#9ca3af' },
1155
+ grid: { color: '#374151' }
1156
+ },
1157
+ x: {
1158
+ ticks: { color: '#9ca3af' },
1159
+ grid: { color: '#374151' }
1160
+ }
1161
+ }
1162
+ }
1163
+ });
1164
+
1165
+ // Hour of day chart
1166
+ const hourCtx = document.getElementById('hour-chart').getContext('2d');
1167
+ new Chart(hourCtx, {
1168
+ type: 'line',
1169
+ data: {
1170
+ labels: Array.from({ length: 24 }, (_, i) => i),
1171
+ datasets: [{
1172
+ label: 'Commits',
1173
+ data: currentData.stats.hourOfDay,
1174
+ backgroundColor: 'rgba(16, 185, 129, 0.2)',
1175
+ borderColor: 'rgba(16, 185, 129, 1)',
1176
+ borderWidth: 2,
1177
+ fill: true,
1178
+ tension: 0.4
1179
+ }]
1180
+ },
1181
+ options: {
1182
+ responsive: true,
1183
+ maintainAspectRatio: true,
1184
+ plugins: {
1185
+ legend: { display: false }
1186
+ },
1187
+ scales: {
1188
+ y: {
1189
+ beginAtZero: true,
1190
+ ticks: { color: '#9ca3af' },
1191
+ grid: { color: '#374151' }
1192
+ },
1193
+ x: {
1194
+ ticks: { color: '#9ca3af' },
1195
+ grid: { color: '#374151' }
1196
+ }
1197
+ }
1198
+ }
1199
+ });
1200
+ }
1201
+
1202
+ function renderOptimization() {
1203
+ const garbage = currentData.garbageInfo;
1204
+ const health = currentData.health;
1205
+
1206
+ document.getElementById('garbage-info').innerHTML = `
1207
+ <h4 class="text-lg font-semibold mb-4">๐Ÿ“ฆ Repository Storage</h4>
1208
+ <div class="space-y-3">
1209
+ <div class="flex justify-between">
1210
+ <span class="text-gray-400">Loose Objects</span>
1211
+ <span class="font-semibold">${garbage.looseObjects.toLocaleString()}</span>
1212
+ </div>
1213
+ <div class="flex justify-between">
1214
+ <span class="text-gray-400">Loose Size</span>
1215
+ <span class="font-semibold">${garbage.looseSize} KB</span>
1216
+ </div>
1217
+ <div class="flex justify-between">
1218
+ <span class="text-gray-400">Pack Files</span>
1219
+ <span class="font-semibold">${garbage.packCount}</span>
1220
+ </div>
1221
+ <div class="flex justify-between">
1222
+ <span class="text-gray-400">Pack Size</span>
1223
+ <span class="font-semibold">${garbage.packSize} KB</span>
1224
+ </div>
1225
+ <div class="pt-4 border-t border-gray-700">
1226
+ <div class="flex items-start space-x-2">
1227
+ <span class="${garbage.shouldGC ? 'text-yellow-400' : 'text-green-400'}">${garbage.shouldGC ? 'โš ' : 'โœ“'}</span>
1228
+ <span class="${garbage.shouldGC ? 'text-yellow-400' : 'text-green-400'}">${garbage.recommendation}</span>
1229
+ </div>
1230
+ </div>
1231
+ </div>
1232
+ `;
1233
+
1234
+ document.getElementById('health-details').innerHTML = `
1235
+ <h4 class="text-lg font-semibold mb-4">๐Ÿ’Š Health Report</h4>
1236
+ ${health.issues.length ? `
1237
+ <div class="mb-4">
1238
+ <div class="text-sm text-gray-400 mb-2">Issues Found:</div>
1239
+ <ul class="space-y-2">
1240
+ ${health.issues.map(issue => `
1241
+ <li class="flex items-start space-x-2">
1242
+ <span class="text-yellow-400">โš </span>
1243
+ <span class="text-gray-300">${escapeHtml(issue)}</span>
1244
+ </li>
1245
+ `).join('')}
1246
+ </ul>
1247
+ </div>
1248
+ ` : '<div class="text-green-400">โœ“ No issues found</div>'}
1249
+
1250
+ ${health.suggestions.length ? `
1251
+ <div class="mt-4">
1252
+ <div class="text-sm text-gray-400 mb-2">Suggestions:</div>
1253
+ <ul class="space-y-2">
1254
+ ${health.suggestions.map(suggestion => `
1255
+ <li class="flex items-start space-x-2">
1256
+ <span class="text-blue-400">๐Ÿ’ก</span>
1257
+ <span class="text-gray-300">${escapeHtml(suggestion)}</span>
1258
+ </li>
1259
+ `).join('')}
1260
+ </ul>
1261
+ </div>
1262
+ ` : ''}
1263
+ `;
1264
+ }
1265
+
1266
+ async function runGarbageCollection() {
1267
+ showModal({
1268
+ type: 'confirm',
1269
+ title: 'Run Garbage Collection',
1270
+ icon: '๐Ÿงน',
1271
+ message: 'This will run <code>git gc --aggressive</code> to optimize your repository. This may take a few minutes depending on repository size.<br><br>Continue?',
1272
+ confirmText: 'Run GC',
1273
+ cancelText: 'Cancel',
1274
+ onConfirm: async () => {
1275
+ showToast('Running garbage collection...', 'info', 2000);
1276
+
1277
+ try {
1278
+ const response = await fetch('/api/gc', { method: 'POST' });
1279
+ const result = await response.json();
1280
+
1281
+ if (result.success) {
1282
+ showModal({
1283
+ type: 'success',
1284
+ title: 'Success',
1285
+ icon: 'โœ“',
1286
+ message: 'Garbage collection completed successfully! Your repository has been optimized.',
1287
+ confirmText: 'Great!',
1288
+ onConfirm: () => {
1289
+ refreshData();
1290
+ }
1291
+ });
1292
+ } else {
1293
+ showModal({
1294
+ type: 'error',
1295
+ title: 'Error',
1296
+ icon: 'โŒ',
1297
+ message: `Garbage collection failed: ${result.error}`,
1298
+ confirmText: 'Close'
1299
+ });
1300
+ }
1301
+ } catch (error) {
1302
+ showModal({
1303
+ type: 'error',
1304
+ title: 'Error',
1305
+ icon: 'โŒ',
1306
+ message: `Failed to run garbage collection: ${error.message}`,
1307
+ confirmText: 'Close'
1308
+ });
1309
+ }
1310
+ }
1311
+ });
1312
+ }
1313
+
1314
+ function setupTabs() {
1315
+ const tabs = document.querySelectorAll('.tab-btn');
1316
+ tabs.forEach(tab => {
1317
+ tab.addEventListener('click', () => {
1318
+ const tabName = tab.dataset.tab;
1319
+ switchTab(tabName);
1320
+ });
1321
+ });
1322
+
1323
+ // Setup graph limit dropdown
1324
+ const graphLimit = document.getElementById('graph-limit');
1325
+ if (graphLimit) {
1326
+ graphLimit.addEventListener('change', loadGitGraph);
1327
+ }
1328
+ }
1329
+
1330
+ function switchTab(tabName) {
1331
+ // Update tab buttons
1332
+ document.querySelectorAll('.tab-btn').forEach(btn => {
1333
+ if (btn.dataset.tab === tabName) {
1334
+ btn.classList.add('border-blue-500', 'text-blue-400');
1335
+ btn.classList.remove('border-transparent');
1336
+ } else {
1337
+ btn.classList.remove('border-blue-500', 'text-blue-400');
1338
+ btn.classList.add('border-transparent');
1339
+ }
1340
+ });
1341
+
1342
+ // Update tab content
1343
+ document.querySelectorAll('.tab-content').forEach(content => {
1344
+ content.classList.remove('active');
1345
+ });
1346
+ document.getElementById(tabName + '-tab').classList.add('active');
1347
+ }
1348
+
1349
+ function escapeHtml(text) {
1350
+ const map = {
1351
+ '&': '&amp;',
1352
+ '<': '&lt;',
1353
+ '>': '&gt;',
1354
+ '"': '&quot;',
1355
+ "'": '&#039;'
1356
+ };
1357
+ return text.replace(/[&<>"']/g, m => map[m]);
1358
+ }
1359
+ </script>
1360
+ </body>
1361
+ </html>