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.
- package/README.md +256 -0
- package/cli.js +57 -0
- package/package.json +39 -0
- package/public/index.html +1361 -0
- package/server.js +107 -0
|
@@ -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
|
+
'&': '&',
|
|
1352
|
+
'<': '<',
|
|
1353
|
+
'>': '>',
|
|
1354
|
+
'"': '"',
|
|
1355
|
+
"'": '''
|
|
1356
|
+
};
|
|
1357
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
1358
|
+
}
|
|
1359
|
+
</script>
|
|
1360
|
+
</body>
|
|
1361
|
+
</html>
|