git2report 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git2report/__init__.py +2 -0
- git2report/__main__.py +4 -0
- git2report/branches.py +34 -0
- git2report/cache.py +78 -0
- git2report/charts.py +155 -0
- git2report/chunker.py +47 -0
- git2report/cli.py +173 -0
- git2report/clustering.py +56 -0
- git2report/config.py +28 -0
- git2report/eval_tools.py +123 -0
- git2report/extractor.py +37 -0
- git2report/filtering.py +34 -0
- git2report/metrics.py +111 -0
- git2report/models.py +45 -0
- git2report/pipeline.py +188 -0
- git2report/report.py +884 -0
- git2report/rules.py +71 -0
- git2report/summarizer.py +359 -0
- git2report/timeutil.py +19 -0
- git2report/web.py +1207 -0
- git2report-0.1.0.dist-info/METADATA +142 -0
- git2report-0.1.0.dist-info/RECORD +26 -0
- git2report-0.1.0.dist-info/WHEEL +5 -0
- git2report-0.1.0.dist-info/entry_points.txt +4 -0
- git2report-0.1.0.dist-info/licenses/LICENSE +21 -0
- git2report-0.1.0.dist-info/top_level.txt +1 -0
git2report/web.py
ADDED
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
"""Interactive Terminal-Style Web GUI for Git2Report.
|
|
2
|
+
|
|
3
|
+
Provides an authentic CLI / Hacker terminal experience in the browser to configure,
|
|
4
|
+
execute, stream live logs, preview, and download engineering progress reports.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import mimetypes
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import threading
|
|
14
|
+
import urllib.parse
|
|
15
|
+
import webbrowser
|
|
16
|
+
from http import HTTPStatus
|
|
17
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from git2report.cache import default_cache
|
|
22
|
+
from git2report.models import GitReportError
|
|
23
|
+
from git2report.pipeline import Options, run_pipeline
|
|
24
|
+
from git2report.report import render_html, render_markdown, render_pdf
|
|
25
|
+
from git2report.summarizer import get_summarizer
|
|
26
|
+
from git2report.timeutil import parse_since
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("git2report.web")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def discover_local_repos() -> list[dict[str, str]]:
|
|
32
|
+
"""Scan current folder and cloned_repos/ for existing Git repositories."""
|
|
33
|
+
repos = []
|
|
34
|
+
seen = set()
|
|
35
|
+
|
|
36
|
+
def check(p: Path, label: str):
|
|
37
|
+
resolved = str(p.resolve())
|
|
38
|
+
if resolved not in seen and (p / ".git").exists():
|
|
39
|
+
seen.add(resolved)
|
|
40
|
+
repos.append({"path": str(p).replace("\\", "/"), "name": label})
|
|
41
|
+
|
|
42
|
+
# Current directory
|
|
43
|
+
check(Path("."), "Current Repo (.)")
|
|
44
|
+
|
|
45
|
+
# Cloned repos
|
|
46
|
+
cloned_dir = Path("cloned_repos")
|
|
47
|
+
if cloned_dir.exists():
|
|
48
|
+
for sub in cloned_dir.iterdir():
|
|
49
|
+
if sub.is_dir():
|
|
50
|
+
check(sub, f"cloned_repos/{sub.name}")
|
|
51
|
+
|
|
52
|
+
# Parent directory check
|
|
53
|
+
parent = Path("..")
|
|
54
|
+
if parent.exists():
|
|
55
|
+
for sub in parent.iterdir():
|
|
56
|
+
if sub.is_dir() and sub.name != "git2report":
|
|
57
|
+
check(sub, f"../{sub.name}")
|
|
58
|
+
|
|
59
|
+
return repos
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
TERMINAL_HTML = """<!DOCTYPE html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="UTF-8">
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
67
|
+
<title>git2report@terminal: ~</title>
|
|
68
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
69
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
70
|
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&display=swap" rel="stylesheet">
|
|
71
|
+
<style>
|
|
72
|
+
:root {
|
|
73
|
+
--term-bg: #0B0E14;
|
|
74
|
+
--term-card: #0F131D;
|
|
75
|
+
--term-border: #1E2638;
|
|
76
|
+
--term-border-glow: #00ff8833;
|
|
77
|
+
--text-main: #E2E8F0;
|
|
78
|
+
--text-muted: #64748B;
|
|
79
|
+
--prompt-user: #00FF88;
|
|
80
|
+
--prompt-host: #38BDF8;
|
|
81
|
+
--prompt-path: #FACC15;
|
|
82
|
+
--prompt-symbol: #EC4899;
|
|
83
|
+
--accent-green: #00FF88;
|
|
84
|
+
--accent-cyan: #38BDF8;
|
|
85
|
+
--accent-yellow: #FACC15;
|
|
86
|
+
--accent-red: #EF4444;
|
|
87
|
+
--accent-purple: #A855F7;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
91
|
+
|
|
92
|
+
body {
|
|
93
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
94
|
+
background-color: #05070A;
|
|
95
|
+
background-image:
|
|
96
|
+
radial-gradient(ellipse at 50% 0%, rgba(14, 165, 233, 0.08) 0%, transparent 60%),
|
|
97
|
+
linear-gradient(rgba(255, 255, 255, 0.015) 1px, transparent 1px),
|
|
98
|
+
linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px);
|
|
99
|
+
background-size: 100% 100%, 32px 32px, 32px 32px;
|
|
100
|
+
color: var(--text-main);
|
|
101
|
+
min-height: 100vh;
|
|
102
|
+
padding: 16px;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
align-items: center;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.term-window {
|
|
109
|
+
width: 100%;
|
|
110
|
+
max-width: 1360px;
|
|
111
|
+
background: var(--term-bg);
|
|
112
|
+
border: 1px solid var(--term-border);
|
|
113
|
+
border-radius: 12px;
|
|
114
|
+
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.8), 0 0 1px 1px var(--term-border);
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Top Title Bar */
|
|
121
|
+
.title-bar {
|
|
122
|
+
background: #090C12;
|
|
123
|
+
border-bottom: 1px solid var(--term-border);
|
|
124
|
+
padding: 10px 16px;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: space-between;
|
|
128
|
+
user-select: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.window-controls {
|
|
132
|
+
display: flex;
|
|
133
|
+
gap: 8px;
|
|
134
|
+
align-items: center;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.win-dot {
|
|
138
|
+
width: 12px;
|
|
139
|
+
height: 12px;
|
|
140
|
+
border-radius: 50%;
|
|
141
|
+
display: inline-block;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.dot-close { background: #FF5F56; border: 1px solid #E0443E; }
|
|
145
|
+
.dot-min { background: #FFBD2E; border: 1px solid #DEA123; }
|
|
146
|
+
.dot-max { background: #27C93F; border: 1px solid #1AAB29; }
|
|
147
|
+
|
|
148
|
+
.window-title {
|
|
149
|
+
font-size: 0.8rem;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
color: var(--text-muted);
|
|
152
|
+
letter-spacing: 0.05em;
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 8px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.session-badge {
|
|
159
|
+
font-size: 0.7rem;
|
|
160
|
+
background: rgba(0, 255, 136, 0.1);
|
|
161
|
+
border: 1px solid rgba(0, 255, 136, 0.3);
|
|
162
|
+
color: var(--accent-green);
|
|
163
|
+
padding: 2px 8px;
|
|
164
|
+
border-radius: 4px;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Terminal Body Content */
|
|
169
|
+
.term-content {
|
|
170
|
+
padding: 20px 24px;
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: column;
|
|
173
|
+
gap: 18px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ASCII Banner */
|
|
177
|
+
.ascii-banner {
|
|
178
|
+
color: var(--accent-green);
|
|
179
|
+
font-size: 0.7rem;
|
|
180
|
+
line-height: 1.15;
|
|
181
|
+
white-space: pre;
|
|
182
|
+
user-select: none;
|
|
183
|
+
overflow-x: auto;
|
|
184
|
+
text-shadow: 0 0 10px rgba(0, 255, 136, 0.3);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Prompt line */
|
|
188
|
+
.cli-bar {
|
|
189
|
+
background: var(--term-card);
|
|
190
|
+
border: 1px solid var(--term-border);
|
|
191
|
+
border-radius: 8px;
|
|
192
|
+
padding: 12px 16px;
|
|
193
|
+
font-size: 0.88rem;
|
|
194
|
+
display: flex;
|
|
195
|
+
flex-wrap: wrap;
|
|
196
|
+
align-items: center;
|
|
197
|
+
gap: 8px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.prompt-label {
|
|
201
|
+
display: flex;
|
|
202
|
+
align-items: center;
|
|
203
|
+
gap: 2px;
|
|
204
|
+
font-weight: 700;
|
|
205
|
+
}
|
|
206
|
+
.p-user { color: var(--prompt-user); }
|
|
207
|
+
.p-at { color: var(--text-muted); }
|
|
208
|
+
.p-host { color: var(--prompt-host); }
|
|
209
|
+
.p-colon { color: var(--text-muted); }
|
|
210
|
+
.p-path { color: var(--prompt-path); }
|
|
211
|
+
.p-dollar { color: var(--prompt-symbol); margin-left: 4px; }
|
|
212
|
+
|
|
213
|
+
.command-display {
|
|
214
|
+
color: #FFFFFF;
|
|
215
|
+
font-weight: 600;
|
|
216
|
+
flex: 1;
|
|
217
|
+
min-width: 250px;
|
|
218
|
+
word-break: break-all;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.cursor-blink {
|
|
222
|
+
display: inline-block;
|
|
223
|
+
width: 8px;
|
|
224
|
+
height: 15px;
|
|
225
|
+
background: var(--accent-green);
|
|
226
|
+
animation: blink 1s step-start infinite;
|
|
227
|
+
vertical-align: middle;
|
|
228
|
+
margin-left: 2px;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@keyframes blink {
|
|
232
|
+
50% { opacity: 0; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Quick Action Shortcuts */
|
|
236
|
+
.quick-actions {
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-wrap: wrap;
|
|
239
|
+
align-items: center;
|
|
240
|
+
gap: 8px;
|
|
241
|
+
font-size: 0.78rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.qa-title {
|
|
245
|
+
color: var(--text-muted);
|
|
246
|
+
font-size: 0.75rem;
|
|
247
|
+
text-transform: uppercase;
|
|
248
|
+
letter-spacing: 0.05em;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.btn-qa {
|
|
252
|
+
background: rgba(255, 255, 255, 0.04);
|
|
253
|
+
border: 1px solid var(--term-border);
|
|
254
|
+
color: var(--accent-cyan);
|
|
255
|
+
padding: 5px 10px;
|
|
256
|
+
border-radius: 6px;
|
|
257
|
+
font-family: inherit;
|
|
258
|
+
font-size: 0.75rem;
|
|
259
|
+
cursor: pointer;
|
|
260
|
+
transition: all 0.15s;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.btn-qa:hover {
|
|
264
|
+
background: rgba(56, 189, 248, 0.15);
|
|
265
|
+
border-color: var(--accent-cyan);
|
|
266
|
+
box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.btn-qa.danger {
|
|
270
|
+
color: var(--accent-red);
|
|
271
|
+
}
|
|
272
|
+
.btn-qa.danger:hover {
|
|
273
|
+
background: rgba(239, 68, 68, 0.15);
|
|
274
|
+
border-color: var(--accent-red);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* Split Workspace Grid */
|
|
278
|
+
.workspace-grid {
|
|
279
|
+
display: grid;
|
|
280
|
+
grid-template-columns: 460px 1fr;
|
|
281
|
+
gap: 20px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@media (max-width: 1024px) {
|
|
285
|
+
.workspace-grid { grid-template-columns: 1fr; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.terminal-pane {
|
|
289
|
+
background: var(--term-card);
|
|
290
|
+
border: 1px solid var(--term-border);
|
|
291
|
+
border-radius: 8px;
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
overflow: hidden;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.pane-header {
|
|
298
|
+
background: #090C12;
|
|
299
|
+
padding: 10px 14px;
|
|
300
|
+
border-bottom: 1px solid var(--term-border);
|
|
301
|
+
font-size: 0.78rem;
|
|
302
|
+
font-weight: 700;
|
|
303
|
+
color: var(--accent-cyan);
|
|
304
|
+
display: flex;
|
|
305
|
+
align-items: center;
|
|
306
|
+
justify-content: space-between;
|
|
307
|
+
letter-spacing: 0.05em;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.pane-body {
|
|
311
|
+
padding: 16px;
|
|
312
|
+
display: flex;
|
|
313
|
+
flex-direction: column;
|
|
314
|
+
gap: 16px;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/* Terminal Fields */
|
|
318
|
+
.arg-group {
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
gap: 6px;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.arg-label {
|
|
325
|
+
font-size: 0.78rem;
|
|
326
|
+
color: var(--accent-yellow);
|
|
327
|
+
font-weight: 600;
|
|
328
|
+
display: flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
gap: 6px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.arg-desc {
|
|
334
|
+
font-size: 0.7rem;
|
|
335
|
+
color: var(--text-muted);
|
|
336
|
+
margin-left: 4px;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.term-input, .term-select {
|
|
340
|
+
background: #06090F;
|
|
341
|
+
border: 1px solid var(--term-border);
|
|
342
|
+
color: #FFFFFF;
|
|
343
|
+
padding: 8px 12px;
|
|
344
|
+
border-radius: 6px;
|
|
345
|
+
font-family: inherit;
|
|
346
|
+
font-size: 0.85rem;
|
|
347
|
+
outline: none;
|
|
348
|
+
transition: all 0.2s;
|
|
349
|
+
width: 100%;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.term-input:focus, .term-select:focus {
|
|
353
|
+
border-color: var(--accent-green);
|
|
354
|
+
box-shadow: 0 0 0 1px var(--accent-green), 0 0 10px rgba(0, 255, 136, 0.15);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.preset-pills {
|
|
358
|
+
display: flex;
|
|
359
|
+
flex-wrap: wrap;
|
|
360
|
+
gap: 6px;
|
|
361
|
+
margin-top: 2px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.p-pill {
|
|
365
|
+
background: rgba(255, 255, 255, 0.03);
|
|
366
|
+
border: 1px solid var(--term-border);
|
|
367
|
+
color: var(--text-muted);
|
|
368
|
+
padding: 3px 8px;
|
|
369
|
+
border-radius: 4px;
|
|
370
|
+
font-size: 0.72rem;
|
|
371
|
+
cursor: pointer;
|
|
372
|
+
transition: all 0.15s;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.p-pill:hover, .p-pill.active {
|
|
376
|
+
background: rgba(0, 255, 136, 0.15);
|
|
377
|
+
border-color: var(--accent-green);
|
|
378
|
+
color: var(--accent-green);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.checkbox-row {
|
|
382
|
+
display: flex;
|
|
383
|
+
gap: 16px;
|
|
384
|
+
background: #06090F;
|
|
385
|
+
padding: 8px 12px;
|
|
386
|
+
border-radius: 6px;
|
|
387
|
+
border: 1px solid var(--term-border);
|
|
388
|
+
font-size: 0.8rem;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.checkbox-item {
|
|
392
|
+
display: flex;
|
|
393
|
+
align-items: center;
|
|
394
|
+
gap: 6px;
|
|
395
|
+
cursor: pointer;
|
|
396
|
+
color: var(--text-main);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.btn-exec {
|
|
400
|
+
background: linear-gradient(135deg, #00FF88 0%, #10B981 100%);
|
|
401
|
+
color: #05070A;
|
|
402
|
+
border: none;
|
|
403
|
+
padding: 12px;
|
|
404
|
+
border-radius: 6px;
|
|
405
|
+
font-family: inherit;
|
|
406
|
+
font-size: 0.88rem;
|
|
407
|
+
font-weight: 800;
|
|
408
|
+
letter-spacing: 0.05em;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
justify-content: center;
|
|
413
|
+
gap: 8px;
|
|
414
|
+
box-shadow: 0 4px 16px rgba(0, 255, 136, 0.3);
|
|
415
|
+
transition: all 0.2s;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.btn-exec:hover:not(:disabled) {
|
|
419
|
+
transform: translateY(-1px);
|
|
420
|
+
box-shadow: 0 6px 24px rgba(0, 255, 136, 0.5);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.btn-exec:disabled {
|
|
424
|
+
opacity: 0.5;
|
|
425
|
+
cursor: not-allowed;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* Output Stream Section */
|
|
429
|
+
.stream-box {
|
|
430
|
+
background: #06090F;
|
|
431
|
+
border: 1px solid var(--term-border);
|
|
432
|
+
border-radius: 6px;
|
|
433
|
+
padding: 14px;
|
|
434
|
+
font-size: 0.8rem;
|
|
435
|
+
height: 220px;
|
|
436
|
+
overflow-y: auto;
|
|
437
|
+
white-space: pre-wrap;
|
|
438
|
+
color: #94A3B8;
|
|
439
|
+
line-height: 1.5;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.stream-line {
|
|
443
|
+
display: block;
|
|
444
|
+
margin-bottom: 2px;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.stream-line.info { color: var(--accent-cyan); }
|
|
448
|
+
.stream-line.success { color: var(--accent-green); font-weight: 700; }
|
|
449
|
+
.stream-line.error { color: var(--accent-red); font-weight: 700; }
|
|
450
|
+
|
|
451
|
+
/* ASCII KPI Table */
|
|
452
|
+
.ascii-kpi-card {
|
|
453
|
+
background: #06090F;
|
|
454
|
+
border: 1px solid var(--term-border);
|
|
455
|
+
border-radius: 6px;
|
|
456
|
+
padding: 12px 14px;
|
|
457
|
+
font-size: 0.8rem;
|
|
458
|
+
display: grid;
|
|
459
|
+
grid-template-columns: repeat(4, 1fr);
|
|
460
|
+
gap: 12px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
@media (max-width: 768px) {
|
|
464
|
+
.ascii-kpi-card { grid-template-columns: repeat(2, 1fr); }
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
.kpi-unit-title {
|
|
468
|
+
font-size: 0.68rem;
|
|
469
|
+
color: var(--text-muted);
|
|
470
|
+
text-transform: uppercase;
|
|
471
|
+
letter-spacing: 0.06em;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.kpi-unit-val {
|
|
475
|
+
font-size: 1.2rem;
|
|
476
|
+
font-weight: 700;
|
|
477
|
+
color: #FFFFFF;
|
|
478
|
+
margin-top: 2px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* Downloads Bar */
|
|
482
|
+
.term-export-bar {
|
|
483
|
+
display: flex;
|
|
484
|
+
flex-wrap: wrap;
|
|
485
|
+
align-items: center;
|
|
486
|
+
justify-content: space-between;
|
|
487
|
+
gap: 10px;
|
|
488
|
+
background: #06090F;
|
|
489
|
+
border: 1px solid var(--term-border);
|
|
490
|
+
border-radius: 6px;
|
|
491
|
+
padding: 10px 14px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.export-btns {
|
|
495
|
+
display: flex;
|
|
496
|
+
gap: 8px;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.btn-export {
|
|
500
|
+
background: rgba(255, 255, 255, 0.05);
|
|
501
|
+
border: 1px solid var(--term-border);
|
|
502
|
+
color: var(--accent-green);
|
|
503
|
+
padding: 6px 12px;
|
|
504
|
+
border-radius: 5px;
|
|
505
|
+
font-family: inherit;
|
|
506
|
+
font-size: 0.78rem;
|
|
507
|
+
font-weight: 700;
|
|
508
|
+
cursor: pointer;
|
|
509
|
+
text-decoration: none;
|
|
510
|
+
display: inline-flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
gap: 6px;
|
|
513
|
+
transition: all 0.15s;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.btn-export:hover {
|
|
517
|
+
background: rgba(0, 255, 136, 0.15);
|
|
518
|
+
border-color: var(--accent-green);
|
|
519
|
+
box-shadow: 0 0 10px rgba(0, 255, 136, 0.2);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.btn-export.secondary {
|
|
523
|
+
color: var(--accent-cyan);
|
|
524
|
+
}
|
|
525
|
+
.btn-export.secondary:hover {
|
|
526
|
+
background: rgba(56, 189, 248, 0.15);
|
|
527
|
+
border-color: var(--accent-cyan);
|
|
528
|
+
box-shadow: 0 0 10px rgba(56, 189, 248, 0.2);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* Preview Frame */
|
|
532
|
+
.preview-container {
|
|
533
|
+
width: 100%;
|
|
534
|
+
height: 520px;
|
|
535
|
+
background: white;
|
|
536
|
+
border: 1px solid var(--term-border);
|
|
537
|
+
border-radius: 6px;
|
|
538
|
+
overflow: hidden;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.preview-frame {
|
|
542
|
+
width: 100%;
|
|
543
|
+
height: 100%;
|
|
544
|
+
border: none;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.tab-bar {
|
|
548
|
+
display: flex;
|
|
549
|
+
gap: 6px;
|
|
550
|
+
border-bottom: 1px solid var(--term-border);
|
|
551
|
+
padding: 0 12px;
|
|
552
|
+
background: #090C12;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.t-tab {
|
|
556
|
+
background: transparent;
|
|
557
|
+
border: none;
|
|
558
|
+
border-bottom: 2px solid transparent;
|
|
559
|
+
color: var(--text-muted);
|
|
560
|
+
padding: 8px 12px;
|
|
561
|
+
font-family: inherit;
|
|
562
|
+
font-size: 0.78rem;
|
|
563
|
+
font-weight: 600;
|
|
564
|
+
cursor: pointer;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.t-tab.active {
|
|
568
|
+
color: var(--accent-green);
|
|
569
|
+
border-bottom-color: var(--accent-green);
|
|
570
|
+
}
|
|
571
|
+
</style>
|
|
572
|
+
</head>
|
|
573
|
+
<body>
|
|
574
|
+
|
|
575
|
+
<div class="term-window">
|
|
576
|
+
<!-- Top macOS / Linux Window Bar -->
|
|
577
|
+
<div class="title-bar">
|
|
578
|
+
<div class="window-controls">
|
|
579
|
+
<span class="win-dot dot-close"></span>
|
|
580
|
+
<span class="win-dot dot-min"></span>
|
|
581
|
+
<span class="win-dot dot-max"></span>
|
|
582
|
+
</div>
|
|
583
|
+
<div class="window-title">
|
|
584
|
+
<span>terminal@git2report: ~ (pts/0 - zsh)</span>
|
|
585
|
+
</div>
|
|
586
|
+
<div class="session-badge">
|
|
587
|
+
<span>● DAEMON ACTIVE</span>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
|
|
591
|
+
<div class="term-content">
|
|
592
|
+
<!-- ASCII Header Art -->
|
|
593
|
+
<pre class="ascii-banner">
|
|
594
|
+
██████╗ ██╗████████╗██████╗ ██████╗ ███████╗██████╗ ██████╗ ██████╗ ████████╗
|
|
595
|
+
██╔════╝ ██║╚══██╔══╝╚════██╗██╔══██╗██╔════╝██╔══██╗██╔═══██╗██╔══██╗╚══██╔══╝
|
|
596
|
+
██║ ███╗██║ ██║ █████╔╝██████╔╝█████╗ ██████╔╝██║ ██║██████╔╝ ██║
|
|
597
|
+
██║ ██║██║ ██║ ██╔═══╝ ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██╔══██╗ ██║
|
|
598
|
+
╚██████╔╝██║ ██║ ███████╗██║ ██║███████╗██║ ╚██████╔╝██║ ██║ ██║
|
|
599
|
+
╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
|
|
600
|
+
[ v0.1.0 :: AUTONOMOUS EVIDENCE-LINKED REPOSITORY REPORTING SYSTEM ]
|
|
601
|
+
</pre>
|
|
602
|
+
|
|
603
|
+
<!-- Dynamic CLI Command Line -->
|
|
604
|
+
<div class="cli-bar">
|
|
605
|
+
<div class="prompt-label">
|
|
606
|
+
<span class="p-user">root</span><span class="p-at">@</span><span class="p-host">git2report</span><span class="p-colon">:</span><span class="p-path">~</span><span class="p-dollar">$</span>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="command-display" id="commandPreview">
|
|
609
|
+
git2report --repo cloned_repos/CampusThrift --since 2025-01-01 --audience guide --llm gemini --all
|
|
610
|
+
</div>
|
|
611
|
+
<span class="cursor-blink"></span>
|
|
612
|
+
</div>
|
|
613
|
+
|
|
614
|
+
<!-- Quick Command Shortcuts -->
|
|
615
|
+
<div class="quick-actions">
|
|
616
|
+
<span class="qa-title">Presets:</span>
|
|
617
|
+
<button type="button" class="btn-qa" onclick="loadPreset('campusthrift')">[$ CAMPUS THRIFT (FULL)]</button>
|
|
618
|
+
<button type="button" class="btn-qa" onclick="loadPreset('current')">[$ CURRENT REPO (.)]</button>
|
|
619
|
+
<button type="button" class="btn-qa" onclick="loadPreset('offline')">[$ FAST OFFLINE MODE]</button>
|
|
620
|
+
<button type="button" class="btn-qa danger" onclick="clearCache()">[$ CLEAR CACHE]</button>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<!-- Main Workspace Grid -->
|
|
624
|
+
<div class="workspace-grid">
|
|
625
|
+
<!-- LEFT PANE: CONFIGURATION & COMMAND PROMPT -->
|
|
626
|
+
<div class="terminal-pane">
|
|
627
|
+
<div class="pane-header">
|
|
628
|
+
<span>[1] CLI FLAGS & PARAMETERS</span>
|
|
629
|
+
<span style="color: var(--text-muted);">STDIN</span>
|
|
630
|
+
</div>
|
|
631
|
+
|
|
632
|
+
<form id="termForm" class="pane-body" onsubmit="executeRun(event)">
|
|
633
|
+
<!-- Target Repo -->
|
|
634
|
+
<div class="arg-group">
|
|
635
|
+
<label class="arg-label" for="repoInput">
|
|
636
|
+
<span>--repo, -r</span>
|
|
637
|
+
<span class="arg-desc"><path or github url></span>
|
|
638
|
+
</label>
|
|
639
|
+
<input type="text" id="repoInput" class="term-input" value="cloned_repos/CampusThrift" oninput="updateCliPreview()" required>
|
|
640
|
+
<div class="preset-pills" id="discoveredReposPills">
|
|
641
|
+
<!-- Dynamically populated -->
|
|
642
|
+
</div>
|
|
643
|
+
</div>
|
|
644
|
+
|
|
645
|
+
<!-- Time Window -->
|
|
646
|
+
<div class="arg-group">
|
|
647
|
+
<label class="arg-label" for="sinceInput">
|
|
648
|
+
<span>--since, -s</span>
|
|
649
|
+
<span class="arg-desc"><7d | 14d | 30d | YYYY-MM-DD></span>
|
|
650
|
+
</label>
|
|
651
|
+
<input type="text" id="sinceInput" class="term-input" value="2025-01-01" oninput="updateCliPreview()" required>
|
|
652
|
+
<div class="preset-pills">
|
|
653
|
+
<span class="p-pill" onclick="setSinceVal('7d')">7d</span>
|
|
654
|
+
<span class="p-pill" onclick="setSinceVal('14d')">14d</span>
|
|
655
|
+
<span class="p-pill" onclick="setSinceVal('30d')">30d</span>
|
|
656
|
+
<span class="p-pill active" onclick="setSinceVal('2025-01-01')">2025-01-01 (All)</span>
|
|
657
|
+
</div>
|
|
658
|
+
</div>
|
|
659
|
+
|
|
660
|
+
<!-- Audience Persona -->
|
|
661
|
+
<div class="arg-group">
|
|
662
|
+
<label class="arg-label" for="audienceSelect">
|
|
663
|
+
<span>--audience, -a</span>
|
|
664
|
+
<span class="arg-desc"><guide | client | team></span>
|
|
665
|
+
</label>
|
|
666
|
+
<select id="audienceSelect" class="term-select" onchange="updateCliPreview()">
|
|
667
|
+
<option value="guide" selected>guide (Academic & Architecture)</option>
|
|
668
|
+
<option value="client">client (Business & User-Facing)</option>
|
|
669
|
+
<option value="team">team (Release-Notes Technical)</option>
|
|
670
|
+
</select>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<!-- AI Engine -->
|
|
674
|
+
<div class="arg-group">
|
|
675
|
+
<label class="arg-label" for="llmSelect">
|
|
676
|
+
<span>--llm</span>
|
|
677
|
+
<span class="arg-desc"><gemini | ollama | offline></span>
|
|
678
|
+
</label>
|
|
679
|
+
<select id="llmSelect" class="term-select" onchange="onLlmChange()">
|
|
680
|
+
<option value="gemini" selected>gemini (Google Gemini + Smart Cache)</option>
|
|
681
|
+
<option value="ollama">ollama (Local Private AI / Offline Model)</option>
|
|
682
|
+
<option value="offline">offline (Deterministic Heuristic / Zero AI)</option>
|
|
683
|
+
</select>
|
|
684
|
+
</div>
|
|
685
|
+
|
|
686
|
+
<!-- Model Selection (Shows for Ollama & Gemini) -->
|
|
687
|
+
<div class="arg-group" id="modelGroup" style="display: none;">
|
|
688
|
+
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
689
|
+
<label class="arg-label" for="modelInput">
|
|
690
|
+
<span>--model</span>
|
|
691
|
+
<span class="arg-desc" id="modelLabelDesc"><model name></span>
|
|
692
|
+
</label>
|
|
693
|
+
<span id="ollamaStatusBadge" style="font-size: 0.68rem; color: var(--text-muted);"></span>
|
|
694
|
+
</div>
|
|
695
|
+
<input type="text" id="modelInput" class="term-input" value="llama3:8b" placeholder="e.g. llama3:8b, mistral, deepseek-r1:8b" oninput="updateCliPreview()">
|
|
696
|
+
<div class="preset-pills" id="modelPills">
|
|
697
|
+
<span class="p-pill active" onclick="setModelVal('llama3:8b')">llama3:8b</span>
|
|
698
|
+
<span class="p-pill" onclick="setModelVal('llama3.2')">llama3.2</span>
|
|
699
|
+
<span class="p-pill" onclick="setModelVal('mistral')">mistral</span>
|
|
700
|
+
<span class="p-pill" onclick="setModelVal('deepseek-r1:8b')">deepseek-r1:8b</span>
|
|
701
|
+
<span class="p-pill" onclick="setModelVal('phi4')">phi4</span>
|
|
702
|
+
<span class="p-pill" onclick="setModelVal('qwen2.5')">qwen2.5</span>
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
|
|
706
|
+
<!-- Planned Goals -->
|
|
707
|
+
<div class="arg-group">
|
|
708
|
+
<label class="arg-label" for="nextGoalInput">
|
|
709
|
+
<span>--next, -n</span>
|
|
710
|
+
<span class="arg-desc"><optional sprint goal></span>
|
|
711
|
+
</label>
|
|
712
|
+
<input type="text" id="nextGoalInput" class="term-input" placeholder="e.g. Implement realtime chat" oninput="updateCliPreview()">
|
|
713
|
+
</div>
|
|
714
|
+
|
|
715
|
+
<!-- Format Switches -->
|
|
716
|
+
<div class="arg-group">
|
|
717
|
+
<label class="arg-label">
|
|
718
|
+
<span>--formats</span>
|
|
719
|
+
<span class="arg-desc"><emit outputs></span>
|
|
720
|
+
</label>
|
|
721
|
+
<div class="checkbox-row">
|
|
722
|
+
<label class="checkbox-item"><input type="checkbox" id="fmtPdf" checked> .pdf</label>
|
|
723
|
+
<label class="checkbox-item"><input type="checkbox" id="fmtHtml" checked> .html</label>
|
|
724
|
+
<label class="checkbox-item"><input type="checkbox" id="fmtMd" checked> .md</label>
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
|
|
728
|
+
<button type="submit" id="btnExecute" class="btn-exec">
|
|
729
|
+
<span>▶ RUN PIPELINE (ENTER)</span>
|
|
730
|
+
</button>
|
|
731
|
+
</form>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
<!-- RIGHT PANE: LIVE TERMINAL LOGS & RESULTS -->
|
|
735
|
+
<div class="terminal-pane">
|
|
736
|
+
<div class="pane-header">
|
|
737
|
+
<span>[2] EXECUTION CONSOLE & PREVIEW</span>
|
|
738
|
+
<span id="execStatus" style="color: var(--accent-green);">STDOUT: IDLE</span>
|
|
739
|
+
</div>
|
|
740
|
+
|
|
741
|
+
<div class="tab-bar">
|
|
742
|
+
<button class="t-tab active" id="tabStreamBtn" onclick="switchRightTab('stream')">💻 STDOUT STREAM</button>
|
|
743
|
+
<button class="t-tab" id="tabPreviewBtn" onclick="switchRightTab('preview')">👁 SPLIT PREVIEW</button>
|
|
744
|
+
</div>
|
|
745
|
+
|
|
746
|
+
<div class="pane-body">
|
|
747
|
+
<!-- Terminal Output Stream -->
|
|
748
|
+
<div id="viewStream">
|
|
749
|
+
<div class="stream-box" id="termStream">
|
|
750
|
+
<span class="stream-line info">[+] Git2Report System Initialized.</span>
|
|
751
|
+
<span class="stream-line">[*] Ready for execution. Press [▶ RUN PIPELINE] or hit ENTER.</span>
|
|
752
|
+
</div>
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<!-- Live HTML Report Preview -->
|
|
756
|
+
<div id="viewPreview" style="display: none;">
|
|
757
|
+
<div class="preview-container">
|
|
758
|
+
<iframe id="reportPreviewFrame" class="preview-frame"></iframe>
|
|
759
|
+
</div>
|
|
760
|
+
</div>
|
|
761
|
+
|
|
762
|
+
<!-- ASCII KPI Box -->
|
|
763
|
+
<div class="ascii-kpi-card" id="kpiBox" style="display: none;">
|
|
764
|
+
<div>
|
|
765
|
+
<div class="kpi-unit-title">Commits</div>
|
|
766
|
+
<div class="kpi-unit-val" id="kpiCommits" style="color: var(--accent-cyan);">0</div>
|
|
767
|
+
</div>
|
|
768
|
+
<div>
|
|
769
|
+
<div class="kpi-unit-title">Code Churn</div>
|
|
770
|
+
<div class="kpi-unit-val" id="kpiChurn" style="color: var(--accent-green);">0</div>
|
|
771
|
+
</div>
|
|
772
|
+
<div>
|
|
773
|
+
<div class="kpi-unit-title">Top Focus</div>
|
|
774
|
+
<div class="kpi-unit-val" id="kpiTop" style="color: var(--accent-yellow); font-size: 1rem;">-</div>
|
|
775
|
+
</div>
|
|
776
|
+
<div>
|
|
777
|
+
<div class="kpi-unit-title">Test Ratio</div>
|
|
778
|
+
<div class="kpi-unit-val" id="kpiTest" style="color: var(--accent-purple);">0.0</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
|
|
782
|
+
<!-- Terminal Downloads Toolbar -->
|
|
783
|
+
<div class="term-export-bar" id="exportBar" style="display: none;">
|
|
784
|
+
<div style="font-size: 0.78rem; color: var(--accent-green); font-weight: 700;">
|
|
785
|
+
[✓] PIPELINE COMPLETE :: ARTIFACTS READY
|
|
786
|
+
</div>
|
|
787
|
+
<div class="export-btns">
|
|
788
|
+
<a id="btnPdf" href="#" class="btn-export" download>⤓ report.pdf</a>
|
|
789
|
+
<a id="btnHtml" href="#" class="btn-export secondary" download>⤓ report.html</a>
|
|
790
|
+
<a id="btnMd" href="#" class="btn-export secondary" download>⤓ report.md</a>
|
|
791
|
+
</div>
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
</div>
|
|
799
|
+
|
|
800
|
+
<script>
|
|
801
|
+
// Discover local repositories
|
|
802
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
803
|
+
try {
|
|
804
|
+
const res = await fetch('/api/repos');
|
|
805
|
+
const data = await res.json();
|
|
806
|
+
const container = document.getElementById('discoveredReposPills');
|
|
807
|
+
data.repos.forEach(r => {
|
|
808
|
+
const pill = document.createElement('span');
|
|
809
|
+
pill.className = 'p-pill';
|
|
810
|
+
pill.innerText = r.name;
|
|
811
|
+
pill.onclick = () => {
|
|
812
|
+
document.getElementById('repoInput').value = r.path;
|
|
813
|
+
updateCliPreview();
|
|
814
|
+
};
|
|
815
|
+
container.appendChild(pill);
|
|
816
|
+
});
|
|
817
|
+
} catch (e) {
|
|
818
|
+
console.error(e);
|
|
819
|
+
}
|
|
820
|
+
updateCliPreview();
|
|
821
|
+
checkOllamaStatus();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
async function checkOllamaStatus() {
|
|
825
|
+
const badge = document.getElementById('ollamaStatusBadge');
|
|
826
|
+
try {
|
|
827
|
+
const res = await fetch('/api/ollama/status');
|
|
828
|
+
const data = await res.json();
|
|
829
|
+
if (data.online) {
|
|
830
|
+
badge.innerHTML = `<span style="color: var(--accent-green);">● OLLAMA ONLINE</span>`;
|
|
831
|
+
if (data.models && data.models.length > 0) {
|
|
832
|
+
const container = document.getElementById('modelPills');
|
|
833
|
+
container.innerHTML = '';
|
|
834
|
+
data.models.forEach((m, idx) => {
|
|
835
|
+
const pill = document.createElement('span');
|
|
836
|
+
pill.className = 'p-pill' + (idx === 0 ? ' active' : '');
|
|
837
|
+
pill.innerText = m;
|
|
838
|
+
pill.onclick = () => setModelVal(m);
|
|
839
|
+
container.appendChild(pill);
|
|
840
|
+
});
|
|
841
|
+
document.getElementById('modelInput').value = data.models[0];
|
|
842
|
+
updateCliPreview();
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
badge.innerHTML = `<span style="color: var(--text-muted);">○ OLLAMA OFFLINE (run 'ollama serve')</span>`;
|
|
846
|
+
}
|
|
847
|
+
} catch (e) {
|
|
848
|
+
badge.innerHTML = `<span style="color: var(--text-muted);">○ OFFLINE</span>`;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function onLlmChange() {
|
|
853
|
+
const llm = document.getElementById('llmSelect').value;
|
|
854
|
+
const modelGroup = document.getElementById('modelGroup');
|
|
855
|
+
if (llm === 'ollama') {
|
|
856
|
+
modelGroup.style.display = 'flex';
|
|
857
|
+
document.getElementById('modelLabelDesc').innerText = '<ollama model, e.g. llama3:8b>';
|
|
858
|
+
checkOllamaStatus();
|
|
859
|
+
} else {
|
|
860
|
+
modelGroup.style.display = 'none';
|
|
861
|
+
}
|
|
862
|
+
updateCliPreview();
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function setModelVal(val) {
|
|
866
|
+
document.getElementById('modelInput').value = val;
|
|
867
|
+
document.querySelectorAll('#modelPills .p-pill').forEach(p => p.classList.remove('active'));
|
|
868
|
+
event.target.classList.add('active');
|
|
869
|
+
updateCliPreview();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function setSinceVal(val) {
|
|
873
|
+
document.getElementById('sinceInput').value = val;
|
|
874
|
+
document.querySelectorAll('.preset-pills .p-pill').forEach(p => p.classList.remove('active'));
|
|
875
|
+
event.target.classList.add('active');
|
|
876
|
+
updateCliPreview();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function updateCliPreview() {
|
|
880
|
+
const repo = document.getElementById('repoInput').value.trim() || '.';
|
|
881
|
+
const since = document.getElementById('sinceInput').value.trim() || '7d';
|
|
882
|
+
const audience = document.getElementById('audienceSelect').value;
|
|
883
|
+
const llm = document.getElementById('llmSelect').value;
|
|
884
|
+
const model = document.getElementById('modelInput').value.trim();
|
|
885
|
+
const next = document.getElementById('nextGoalInput').value.trim();
|
|
886
|
+
|
|
887
|
+
let cmd = `git2report --repo ${repo} --since ${since} --audience ${audience} --llm ${llm}`;
|
|
888
|
+
if (llm === 'ollama' && model) {
|
|
889
|
+
cmd += ` --model ${model}`;
|
|
890
|
+
}
|
|
891
|
+
if (next) {
|
|
892
|
+
cmd += ` --next "${next}"`;
|
|
893
|
+
}
|
|
894
|
+
cmd += ' --all';
|
|
895
|
+
document.getElementById('commandPreview').innerText = cmd;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function loadPreset(type) {
|
|
899
|
+
if (type === 'campusthrift') {
|
|
900
|
+
document.getElementById('repoInput').value = 'cloned_repos/CampusThrift';
|
|
901
|
+
document.getElementById('sinceInput').value = '2025-01-01';
|
|
902
|
+
document.getElementById('audienceSelect').value = 'guide';
|
|
903
|
+
document.getElementById('llmSelect').value = 'gemini';
|
|
904
|
+
} else if (type === 'current') {
|
|
905
|
+
document.getElementById('repoInput').value = '.';
|
|
906
|
+
document.getElementById('sinceInput').value = '7d';
|
|
907
|
+
document.getElementById('audienceSelect').value = 'team';
|
|
908
|
+
document.getElementById('llmSelect').value = 'gemini';
|
|
909
|
+
} else if (type === 'offline') {
|
|
910
|
+
document.getElementById('llmSelect').value = 'offline';
|
|
911
|
+
}
|
|
912
|
+
onLlmChange();
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
async function clearCache() {
|
|
916
|
+
if (!confirm('Clear smart summary cache (.git2report_cache.json)?')) return;
|
|
917
|
+
try {
|
|
918
|
+
const res = await fetch('/api/cache/clear', { method: 'POST' });
|
|
919
|
+
const data = await res.json();
|
|
920
|
+
appendStreamLine(`[✓] Cache cleared: ${data.cleared || 0} entries removed.`, 'success');
|
|
921
|
+
} catch (e) {
|
|
922
|
+
appendStreamLine(`[!] Failed to clear cache: ${e.message}`, 'error');
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function switchRightTab(name) {
|
|
927
|
+
document.getElementById('tabStreamBtn').classList.toggle('active', name === 'stream');
|
|
928
|
+
document.getElementById('tabPreviewBtn').classList.toggle('active', name === 'preview');
|
|
929
|
+
document.getElementById('viewStream').style.display = (name === 'stream') ? 'block' : 'none';
|
|
930
|
+
document.getElementById('viewPreview').style.display = (name === 'preview') ? 'block' : 'none';
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function appendStreamLine(text, type = '') {
|
|
934
|
+
const box = document.getElementById('termStream');
|
|
935
|
+
const line = document.createElement('span');
|
|
936
|
+
line.className = 'stream-line ' + type;
|
|
937
|
+
line.innerText = text;
|
|
938
|
+
box.appendChild(line);
|
|
939
|
+
box.scrollTop = box.scrollHeight;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function executeRun(e) {
|
|
943
|
+
e.preventDefault();
|
|
944
|
+
const btn = document.getElementById('btnExecute');
|
|
945
|
+
btn.disabled = true;
|
|
946
|
+
btn.innerHTML = '<span>⚡ EXECUTING PIPELINE...</span>';
|
|
947
|
+
document.getElementById('execStatus').innerText = 'STDOUT: RUNNING';
|
|
948
|
+
document.getElementById('execStatus').style.color = 'var(--accent-yellow)';
|
|
949
|
+
|
|
950
|
+
switchRightTab('stream');
|
|
951
|
+
const stream = document.getElementById('termStream');
|
|
952
|
+
stream.innerHTML = '';
|
|
953
|
+
appendStreamLine(`$ ${document.getElementById('commandPreview').innerText}`, 'info');
|
|
954
|
+
|
|
955
|
+
const repo = document.getElementById('repoInput').value.trim();
|
|
956
|
+
const since = document.getElementById('sinceInput').value.trim();
|
|
957
|
+
const audience = document.getElementById('audienceSelect').value;
|
|
958
|
+
const llm = document.getElementById('llmSelect').value;
|
|
959
|
+
const model = document.getElementById('modelInput').value.trim();
|
|
960
|
+
const nextGoal = document.getElementById('nextGoalInput').value.trim();
|
|
961
|
+
|
|
962
|
+
const payload = {
|
|
963
|
+
repo,
|
|
964
|
+
since,
|
|
965
|
+
audience,
|
|
966
|
+
llm,
|
|
967
|
+
model: (llm === 'ollama') ? model : '',
|
|
968
|
+
next_goals: nextGoal ? [nextGoal] : [],
|
|
969
|
+
formats: {
|
|
970
|
+
pdf: document.getElementById('fmtPdf').checked,
|
|
971
|
+
html: document.getElementById('fmtHtml').checked,
|
|
972
|
+
md: document.getElementById('fmtMd').checked,
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
try {
|
|
977
|
+
const res = await fetch('/api/generate', {
|
|
978
|
+
method: 'POST',
|
|
979
|
+
headers: { 'Content-Type': 'application/json' },
|
|
980
|
+
body: JSON.stringify(payload)
|
|
981
|
+
});
|
|
982
|
+
const result = await res.json();
|
|
983
|
+
|
|
984
|
+
if (result.error) {
|
|
985
|
+
appendStreamLine(`[ERROR] ${result.error}`, 'error');
|
|
986
|
+
document.getElementById('execStatus').innerText = 'STDOUT: FAILED';
|
|
987
|
+
document.getElementById('execStatus').style.color = 'var(--accent-red)';
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
(result.logs || []).forEach(l => appendStreamLine(`[+] ${l}`, 'info'));
|
|
992
|
+
appendStreamLine(`[✓] SUCCESS: Generated PDF, HTML & MD in reports/`, 'success');
|
|
993
|
+
|
|
994
|
+
// Update KPI
|
|
995
|
+
document.getElementById('kpiBox').style.display = 'grid';
|
|
996
|
+
document.getElementById('kpiCommits').innerText = result.metrics.commits || '0';
|
|
997
|
+
document.getElementById('kpiChurn').innerText = `+${result.metrics.lines_added || 0} / -${result.metrics.lines_deleted || 0}`;
|
|
998
|
+
document.getElementById('kpiTop').innerText = result.metrics.top_module || '-';
|
|
999
|
+
document.getElementById('kpiTest').innerText = result.metrics.test_to_code_ratio || '0.0';
|
|
1000
|
+
|
|
1001
|
+
// Update Downloads
|
|
1002
|
+
document.getElementById('exportBar').style.display = 'flex';
|
|
1003
|
+
document.getElementById('btnPdf').href = '/api/download?type=pdf&token=' + Date.now();
|
|
1004
|
+
document.getElementById('btnHtml').href = '/api/download?type=html&token=' + Date.now();
|
|
1005
|
+
document.getElementById('btnMd').href = '/api/download?type=md&token=' + Date.now();
|
|
1006
|
+
|
|
1007
|
+
// Update Live Frame
|
|
1008
|
+
document.getElementById('reportPreviewFrame').src = '/api/preview?token=' + Date.now();
|
|
1009
|
+
|
|
1010
|
+
document.getElementById('execStatus').innerText = 'STDOUT: COMPLETED';
|
|
1011
|
+
document.getElementById('execStatus').style.color = 'var(--accent-green)';
|
|
1012
|
+
} catch (err) {
|
|
1013
|
+
appendStreamLine(`[CRITICAL] Server connection failed: ${err.message}`, 'error');
|
|
1014
|
+
document.getElementById('execStatus').innerText = 'STDOUT: ERROR';
|
|
1015
|
+
document.getElementById('execStatus').style.color = 'var(--accent-red)';
|
|
1016
|
+
} finally {
|
|
1017
|
+
btn.disabled = false;
|
|
1018
|
+
btn.innerHTML = '<span>▶ RUN PIPELINE (ENTER)</span>';
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
</script>
|
|
1022
|
+
</body>
|
|
1023
|
+
</html>
|
|
1024
|
+
"""
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
class WebStudioHandler(BaseHTTPRequestHandler):
|
|
1028
|
+
last_generated_files: dict[str, Path] = {}
|
|
1029
|
+
|
|
1030
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
1031
|
+
"""Suppress standard access logs to keep terminal output clean."""
|
|
1032
|
+
pass
|
|
1033
|
+
|
|
1034
|
+
def do_GET(self) -> None:
|
|
1035
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
1036
|
+
path = parsed.path
|
|
1037
|
+
query = urllib.parse.parse_qs(parsed.query)
|
|
1038
|
+
|
|
1039
|
+
if path == "/" or path == "/index.html":
|
|
1040
|
+
self.send_response(HTTPStatus.OK)
|
|
1041
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
1042
|
+
self.end_headers()
|
|
1043
|
+
self.wfile.write(TERMINAL_HTML.encode("utf-8"))
|
|
1044
|
+
return
|
|
1045
|
+
|
|
1046
|
+
if path == "/api/repos":
|
|
1047
|
+
repos = discover_local_repos()
|
|
1048
|
+
self.send_response(HTTPStatus.OK)
|
|
1049
|
+
self.send_header("Content-Type", "application/json")
|
|
1050
|
+
self.end_headers()
|
|
1051
|
+
self.wfile.write(json.dumps({"repos": repos}).encode("utf-8"))
|
|
1052
|
+
return
|
|
1053
|
+
|
|
1054
|
+
if path == "/api/ollama/status":
|
|
1055
|
+
from git2report.summarizer import list_local_ollama_models
|
|
1056
|
+
models = list_local_ollama_models()
|
|
1057
|
+
self.send_response(HTTPStatus.OK)
|
|
1058
|
+
self.send_header("Content-Type", "application/json")
|
|
1059
|
+
self.end_headers()
|
|
1060
|
+
self.wfile.write(json.dumps({"online": len(models) > 0, "models": models}).encode("utf-8"))
|
|
1061
|
+
return
|
|
1062
|
+
|
|
1063
|
+
if path == "/api/preview":
|
|
1064
|
+
html_path = self.last_generated_files.get("html")
|
|
1065
|
+
if html_path and html_path.exists():
|
|
1066
|
+
content = html_path.read_bytes()
|
|
1067
|
+
self.send_response(HTTPStatus.OK)
|
|
1068
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
1069
|
+
self.end_headers()
|
|
1070
|
+
self.wfile.write(content)
|
|
1071
|
+
return
|
|
1072
|
+
self.send_error(HTTPStatus.NOT_FOUND, "No HTML report generated yet.")
|
|
1073
|
+
return
|
|
1074
|
+
|
|
1075
|
+
if path == "/api/download":
|
|
1076
|
+
ftype = query.get("type", ["pdf"])[0].lower()
|
|
1077
|
+
target_path = self.last_generated_files.get(ftype)
|
|
1078
|
+
if target_path and target_path.exists():
|
|
1079
|
+
mime, _ = mimetypes.guess_type(str(target_path))
|
|
1080
|
+
mime = mime or "application/octet-stream"
|
|
1081
|
+
self.send_response(HTTPStatus.OK)
|
|
1082
|
+
self.send_header("Content-Type", mime)
|
|
1083
|
+
self.send_header("Content-Disposition", f'attachment; filename="{target_path.name}"')
|
|
1084
|
+
self.send_header("Content-Length", str(target_path.stat().st_size))
|
|
1085
|
+
self.end_headers()
|
|
1086
|
+
self.wfile.write(target_path.read_bytes())
|
|
1087
|
+
return
|
|
1088
|
+
self.send_error(HTTPStatus.NOT_FOUND, f"File for {ftype} not found.")
|
|
1089
|
+
return
|
|
1090
|
+
|
|
1091
|
+
self.send_error(HTTPStatus.NOT_FOUND, "Not found")
|
|
1092
|
+
|
|
1093
|
+
def do_POST(self) -> None:
|
|
1094
|
+
parsed = urllib.parse.urlparse(self.path)
|
|
1095
|
+
if parsed.path == "/api/cache/clear":
|
|
1096
|
+
cleared = default_cache.clear()
|
|
1097
|
+
self.send_response(HTTPStatus.OK)
|
|
1098
|
+
self.send_header("Content-Type", "application/json")
|
|
1099
|
+
self.end_headers()
|
|
1100
|
+
self.wfile.write(json.dumps({"cleared": cleared}).encode("utf-8"))
|
|
1101
|
+
return
|
|
1102
|
+
|
|
1103
|
+
if parsed.path == "/api/generate":
|
|
1104
|
+
content_len = int(self.headers.get("Content-Length", 0))
|
|
1105
|
+
body = self.rfile.read(content_len).decode("utf-8")
|
|
1106
|
+
try:
|
|
1107
|
+
data = json.loads(body)
|
|
1108
|
+
repo_str = data.get("repo", ".")
|
|
1109
|
+
since_str = data.get("since", "7d")
|
|
1110
|
+
audience = data.get("audience", "guide")
|
|
1111
|
+
llm = data.get("llm", "gemini")
|
|
1112
|
+
model = data.get("model", "")
|
|
1113
|
+
planned = data.get("next_goals", [])
|
|
1114
|
+
|
|
1115
|
+
repo_path = Path(repo_str)
|
|
1116
|
+
if repo_str.startswith(("http://", "https://", "git@")):
|
|
1117
|
+
repo_name = repo_str.rstrip("/").split("/")[-1]
|
|
1118
|
+
if repo_name.endswith(".git"):
|
|
1119
|
+
repo_name = repo_name[:-4]
|
|
1120
|
+
local_dir = Path("cloned_repos") / repo_name
|
|
1121
|
+
local_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
1122
|
+
if not (local_dir / ".git").exists():
|
|
1123
|
+
subprocess.run(["git", "clone", repo_str, str(local_dir)], check=True)
|
|
1124
|
+
repo_path = local_dir
|
|
1125
|
+
|
|
1126
|
+
logs: list[str] = []
|
|
1127
|
+
log_fn = lambda m: logs.append(str(m))
|
|
1128
|
+
|
|
1129
|
+
opts = Options(
|
|
1130
|
+
repo=repo_path,
|
|
1131
|
+
since=parse_since(since_str),
|
|
1132
|
+
audience=audience,
|
|
1133
|
+
planned=planned,
|
|
1134
|
+
include_branches=True
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
chosen_model = model or ("gemini-2.5-flash" if llm == "gemini" else ("llama3:8b" if llm == "ollama" else ""))
|
|
1138
|
+
summarizer = get_summarizer(llm, chosen_model)
|
|
1139
|
+
report_data = run_pipeline(opts, summarizer, log=log_fn)
|
|
1140
|
+
|
|
1141
|
+
reports_dir = Path("reports")
|
|
1142
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
1143
|
+
clean_name = repo_path.resolve().name.lower()
|
|
1144
|
+
base_out = reports_dir / f"{clean_name}_report"
|
|
1145
|
+
|
|
1146
|
+
# Write files
|
|
1147
|
+
html_p = base_out.with_suffix(".html")
|
|
1148
|
+
html_p.write_text(render_html(report_data), encoding="utf-8")
|
|
1149
|
+
self.last_generated_files["html"] = html_p
|
|
1150
|
+
|
|
1151
|
+
pdf_p = base_out.with_suffix(".pdf")
|
|
1152
|
+
render_pdf(report_data, pdf_p)
|
|
1153
|
+
self.last_generated_files["pdf"] = pdf_p
|
|
1154
|
+
|
|
1155
|
+
md_p = base_out.with_suffix(".md")
|
|
1156
|
+
md_p.write_text(render_markdown(report_data), encoding="utf-8")
|
|
1157
|
+
self.last_generated_files["md"] = md_p
|
|
1158
|
+
|
|
1159
|
+
resp_payload = {
|
|
1160
|
+
"status": "success",
|
|
1161
|
+
"metrics": report_data.metrics,
|
|
1162
|
+
"logs": logs,
|
|
1163
|
+
"files": {
|
|
1164
|
+
"html": str(html_p),
|
|
1165
|
+
"pdf": str(pdf_p),
|
|
1166
|
+
"md": str(md_p)
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
self.send_response(HTTPStatus.OK)
|
|
1170
|
+
self.send_header("Content-Type", "application/json")
|
|
1171
|
+
self.end_headers()
|
|
1172
|
+
self.wfile.write(json.dumps(resp_payload).encode("utf-8"))
|
|
1173
|
+
except Exception as exc:
|
|
1174
|
+
self.send_response(HTTPStatus.BAD_REQUEST)
|
|
1175
|
+
self.send_header("Content-Type", "application/json")
|
|
1176
|
+
self.end_headers()
|
|
1177
|
+
self.wfile.write(json.dumps({"error": str(exc)}).encode("utf-8"))
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
self.send_error(HTTPStatus.NOT_FOUND, "Not found")
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def launch_web_server(port: int = 5000, open_browser: bool = True) -> None:
|
|
1184
|
+
"""Launch the Web Terminal server and open the browser."""
|
|
1185
|
+
for p in range(port, port + 20):
|
|
1186
|
+
try:
|
|
1187
|
+
httpd = ThreadingHTTPServer(("127.0.0.1", p), WebStudioHandler)
|
|
1188
|
+
url = f"http://localhost:{p}"
|
|
1189
|
+
print(f"\n=======================================================")
|
|
1190
|
+
print(f" 📟 GIT2REPORT TERMINAL STUDIO RUNNING AT: {url}")
|
|
1191
|
+
print(f"=======================================================")
|
|
1192
|
+
print(" Press Ctrl+C in this terminal to stop the server.\n")
|
|
1193
|
+
if open_browser:
|
|
1194
|
+
threading.Timer(0.8, lambda: webbrowser.open(url)).start()
|
|
1195
|
+
httpd.serve_forever()
|
|
1196
|
+
return
|
|
1197
|
+
except OSError:
|
|
1198
|
+
continue
|
|
1199
|
+
raise RuntimeError("Could not find an available port to start the web server.")
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
def main():
|
|
1203
|
+
launch_web_server()
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
if __name__ == "__main__":
|
|
1207
|
+
main()
|