python-checkup 0.0.1__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.
- python_checkup/__init__.py +9 -0
- python_checkup/__main__.py +3 -0
- python_checkup/analysis_request.py +35 -0
- python_checkup/analyzer_catalog.py +100 -0
- python_checkup/analyzers/__init__.py +54 -0
- python_checkup/analyzers/bandit.py +158 -0
- python_checkup/analyzers/basedpyright.py +103 -0
- python_checkup/analyzers/cached.py +106 -0
- python_checkup/analyzers/dependency_vulns.py +298 -0
- python_checkup/analyzers/deptry.py +142 -0
- python_checkup/analyzers/detect_secrets.py +101 -0
- python_checkup/analyzers/mypy.py +217 -0
- python_checkup/analyzers/radon.py +150 -0
- python_checkup/analyzers/registry.py +69 -0
- python_checkup/analyzers/ruff.py +256 -0
- python_checkup/analyzers/typos.py +80 -0
- python_checkup/analyzers/vulture.py +151 -0
- python_checkup/cache.py +244 -0
- python_checkup/cli.py +763 -0
- python_checkup/config.py +87 -0
- python_checkup/dedup.py +119 -0
- python_checkup/dependencies/discovery.py +192 -0
- python_checkup/detection.py +298 -0
- python_checkup/diff.py +130 -0
- python_checkup/discovery.py +180 -0
- python_checkup/formatters/__init__.py +0 -0
- python_checkup/formatters/badge.py +38 -0
- python_checkup/formatters/json_fmt.py +22 -0
- python_checkup/formatters/terminal.py +396 -0
- python_checkup/mcp/__init__.py +3 -0
- python_checkup/mcp/installer.py +119 -0
- python_checkup/mcp/server.py +411 -0
- python_checkup/models.py +114 -0
- python_checkup/plan.py +109 -0
- python_checkup/progress.py +95 -0
- python_checkup/runner.py +438 -0
- python_checkup/scoring/__init__.py +0 -0
- python_checkup/scoring/engine.py +397 -0
- python_checkup/skills/SKILL.md +416 -0
- python_checkup/skills/__init__.py +0 -0
- python_checkup/skills/agents.py +98 -0
- python_checkup/skills/installer.py +248 -0
- python_checkup/skills/rule_db.py +806 -0
- python_checkup/web/__init__.py +0 -0
- python_checkup/web/server.py +285 -0
- python_checkup/web/static/__init__.py +0 -0
- python_checkup/web/static/index.html +959 -0
- python_checkup/web/template.py +26 -0
- python_checkup-0.0.1.dist-info/METADATA +250 -0
- python_checkup-0.0.1.dist-info/RECORD +53 -0
- python_checkup-0.0.1.dist-info/WHEEL +4 -0
- python_checkup-0.0.1.dist-info/entry_points.txt +14 -0
- python_checkup-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,959 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>python-checkup — Health Report</title>
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<link
|
|
11
|
+
href="https://fonts.googleapis.com/css2?family=DM+Sans:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
12
|
+
rel="stylesheet">
|
|
13
|
+
<script>
|
|
14
|
+
tailwind.config = {
|
|
15
|
+
darkMode: 'class',
|
|
16
|
+
theme: {
|
|
17
|
+
extend: {
|
|
18
|
+
fontFamily: {
|
|
19
|
+
sans: ['DM Sans', 'system-ui', 'sans-serif'],
|
|
20
|
+
mono: ['JetBrains Mono', 'Consolas', 'monospace'],
|
|
21
|
+
},
|
|
22
|
+
colors: {
|
|
23
|
+
surface: { 950: '#0a0e17', 900: '#0f1420', 850: '#141a28', 800: '#1a2133', 700: '#243049', 600: '#2e3f5e' },
|
|
24
|
+
accent: { 400: '#2dd4bf', 500: '#14b8a6', 600: '#0d9488', 700: '#0f766e', 900: '#0c3b36' },
|
|
25
|
+
},
|
|
26
|
+
fontSize: { '2xs': '0.65rem' },
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
<style>
|
|
32
|
+
.no-scrollbar::-webkit-scrollbar {
|
|
33
|
+
display: none
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.no-scrollbar {
|
|
37
|
+
-ms-overflow-style: none;
|
|
38
|
+
scrollbar-width: none
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@keyframes enter {
|
|
42
|
+
from {
|
|
43
|
+
opacity: 0;
|
|
44
|
+
transform: translateY(6px)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
to {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
transform: translateY(0)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@keyframes fadeIn {
|
|
54
|
+
from {
|
|
55
|
+
opacity: 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
to {
|
|
59
|
+
opacity: 1
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@keyframes scaleIn {
|
|
64
|
+
from {
|
|
65
|
+
opacity: 0;
|
|
66
|
+
transform: scale(.96) translateY(8px)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
to {
|
|
70
|
+
opacity: 1;
|
|
71
|
+
transform: scale(1) translateY(0)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.animate-enter {
|
|
76
|
+
animation: enter .25s ease-out both
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.stagger-1 {
|
|
80
|
+
animation-delay: 30ms
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.stagger-2 {
|
|
84
|
+
animation-delay: 60ms
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.stagger-3 {
|
|
88
|
+
animation-delay: 90ms
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.stagger-4 {
|
|
92
|
+
animation-delay: 120ms
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.stagger-5 {
|
|
96
|
+
animation-delay: 150ms
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.stagger-6 {
|
|
100
|
+
animation-delay: 180ms
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.custom-scroll::-webkit-scrollbar {
|
|
104
|
+
width: 6px
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.custom-scroll::-webkit-scrollbar-track {
|
|
108
|
+
background: transparent
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.custom-scroll::-webkit-scrollbar-thumb {
|
|
112
|
+
background: #243049;
|
|
113
|
+
border-radius: 3px
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.custom-scroll::-webkit-scrollbar-thumb:hover {
|
|
117
|
+
background: #2e3f5e
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
*:focus-visible {
|
|
121
|
+
outline: 2px solid #2dd4bf;
|
|
122
|
+
outline-offset: 2px;
|
|
123
|
+
border-radius: 4px
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
select {
|
|
127
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23475569' d='M3 5l3 3 3-3'/%3E%3C/svg%3E");
|
|
128
|
+
background-repeat: no-repeat;
|
|
129
|
+
background-position: right 8px center;
|
|
130
|
+
-webkit-appearance: none;
|
|
131
|
+
appearance: none;
|
|
132
|
+
padding-right: 24px
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.score-ring {
|
|
136
|
+
position: relative;
|
|
137
|
+
width: 140px;
|
|
138
|
+
height: 140px
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.score-ring svg {
|
|
142
|
+
transform: rotate(-90deg)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.score-ring .score-text {
|
|
146
|
+
position: absolute;
|
|
147
|
+
inset: 0;
|
|
148
|
+
display: flex;
|
|
149
|
+
flex-direction: column;
|
|
150
|
+
align-items: center;
|
|
151
|
+
justify-content: center
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.row-hover:hover {
|
|
155
|
+
background: rgba(45, 212, 191, .04)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.toast {
|
|
159
|
+
position: fixed;
|
|
160
|
+
bottom: 24px;
|
|
161
|
+
right: 24px;
|
|
162
|
+
z-index: 200;
|
|
163
|
+
animation: enter .2s ease-out both
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.category-bar {
|
|
167
|
+
height: 6px;
|
|
168
|
+
border-radius: 3px;
|
|
169
|
+
background: #1a2133;
|
|
170
|
+
overflow: hidden
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.category-bar-fill {
|
|
174
|
+
height: 100%;
|
|
175
|
+
border-radius: 3px;
|
|
176
|
+
transition: width .6s ease-out
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.modal-backdrop {
|
|
180
|
+
animation: fadeIn .15s ease-out both
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.modal-content {
|
|
184
|
+
animation: scaleIn .2s ease-out both
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@keyframes spin {
|
|
188
|
+
to {
|
|
189
|
+
transform: rotate(360deg)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.spinner {
|
|
194
|
+
width: 16px;
|
|
195
|
+
height: 16px;
|
|
196
|
+
border: 2px solid #0f766e;
|
|
197
|
+
border-top-color: #2dd4bf;
|
|
198
|
+
border-radius: 50%;
|
|
199
|
+
animation: spin .6s linear infinite;
|
|
200
|
+
display: inline-block
|
|
201
|
+
}
|
|
202
|
+
</style>
|
|
203
|
+
</head>
|
|
204
|
+
|
|
205
|
+
<body class="h-full bg-surface-950 text-gray-300 text-[13px] font-sans dark antialiased">
|
|
206
|
+
|
|
207
|
+
<div id="app" class="h-full flex flex-col">
|
|
208
|
+
<!-- Header -->
|
|
209
|
+
<header class="flex-shrink-0 border-b border-surface-800 bg-surface-900/80 backdrop-blur-sm px-6 py-3">
|
|
210
|
+
<div class="max-w-[1400px] mx-auto flex items-center justify-between">
|
|
211
|
+
<div class="flex items-center gap-3">
|
|
212
|
+
<h1 class="text-[15px] font-semibold text-white tracking-tight">python-checkup</h1>
|
|
213
|
+
<span class="text-2xs text-gray-500 font-mono" id="headerLabel"></span>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="flex items-center gap-3">
|
|
216
|
+
<span class="text-2xs text-gray-500" id="headerMeta"></span>
|
|
217
|
+
<button id="rerunBtn" onclick="openRerunModal()"
|
|
218
|
+
class="hidden px-3 py-1.5 text-2xs font-medium rounded-lg bg-accent-900 border border-accent-700 text-accent-400 hover:bg-accent-900/80 hover:border-accent-600 transition"
|
|
219
|
+
title="Re-run analysis with selected analyzers">
|
|
220
|
+
Re-run
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</header>
|
|
225
|
+
|
|
226
|
+
<!-- Main content -->
|
|
227
|
+
<main class="flex-1 overflow-y-auto custom-scroll">
|
|
228
|
+
<div class="max-w-[1400px] mx-auto px-6 py-6 space-y-6">
|
|
229
|
+
|
|
230
|
+
<!-- Score + Project info row -->
|
|
231
|
+
<div class="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-6 animate-enter">
|
|
232
|
+
<!-- Score ring -->
|
|
233
|
+
<div class="flex items-center gap-6 bg-surface-900 border border-surface-800 rounded-2xl px-8 py-6">
|
|
234
|
+
<div class="score-ring">
|
|
235
|
+
<svg width="140" height="140" viewBox="0 0 140 140">
|
|
236
|
+
<circle cx="70" cy="70" r="60" fill="none" stroke="#1a2133" stroke-width="10" />
|
|
237
|
+
<circle id="scoreArc" cx="70" cy="70" r="60" fill="none" stroke-width="10" stroke-linecap="round"
|
|
238
|
+
stroke-dasharray="377" stroke-dashoffset="377" />
|
|
239
|
+
</svg>
|
|
240
|
+
<div class="score-text">
|
|
241
|
+
<span id="scoreValue" class="text-3xl font-semibold text-white tabular-nums">--</span>
|
|
242
|
+
<span id="scoreLabel" class="text-2xs text-gray-500 mt-0.5"></span>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
<div class="space-y-2">
|
|
246
|
+
<div class="text-xs text-gray-500">Overall Health Score</div>
|
|
247
|
+
<div id="scoreSummary" class="text-xs text-gray-400 leading-relaxed max-w-[200px]"></div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<!-- Project info cards -->
|
|
252
|
+
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
253
|
+
<div class="bg-surface-900 border border-surface-800 rounded-xl px-4 py-3">
|
|
254
|
+
<div class="text-2xs text-gray-500 mb-1">Python</div>
|
|
255
|
+
<div id="infoPython" class="text-sm font-medium text-white">--</div>
|
|
256
|
+
</div>
|
|
257
|
+
<div class="bg-surface-900 border border-surface-800 rounded-xl px-4 py-3">
|
|
258
|
+
<div class="text-2xs text-gray-500 mb-1">Framework</div>
|
|
259
|
+
<div id="infoFramework" class="text-sm font-medium text-white">--</div>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="bg-surface-900 border border-surface-800 rounded-xl px-4 py-3">
|
|
262
|
+
<div class="text-2xs text-gray-500 mb-1">Files</div>
|
|
263
|
+
<div id="infoFiles" class="text-sm font-medium text-white tabular-nums">--</div>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="bg-surface-900 border border-surface-800 rounded-xl px-4 py-3">
|
|
266
|
+
<div class="text-2xs text-gray-500 mb-1">Lines</div>
|
|
267
|
+
<div id="infoLines" class="text-sm font-medium text-white tabular-nums">--</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<!-- Category breakdown -->
|
|
273
|
+
<div class="animate-enter stagger-1">
|
|
274
|
+
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider mb-3">Category Breakdown</h2>
|
|
275
|
+
<div id="categoryCards" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3"></div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- Diagnostics -->
|
|
279
|
+
<div class="animate-enter stagger-2">
|
|
280
|
+
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-3">
|
|
281
|
+
<h2 class="text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
282
|
+
Diagnostics <span id="diagCount" class="text-gray-600"></span>
|
|
283
|
+
</h2>
|
|
284
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
285
|
+
<input id="searchInput" type="text" placeholder="Search files, rules, messages..."
|
|
286
|
+
class="px-3 py-1.5 text-2xs rounded-lg bg-surface-850 border border-surface-700 text-gray-300 placeholder:text-gray-600 w-56 focus:border-accent-700 transition">
|
|
287
|
+
<!-- Category multi-select dropdown -->
|
|
288
|
+
<div class="relative" id="categoryDropdown">
|
|
289
|
+
<button type="button" onclick="toggleCategoryDropdown()" id="categoryDropdownBtn"
|
|
290
|
+
class="px-3 py-1.5 text-2xs rounded-lg bg-surface-850 border border-surface-700 text-gray-300 cursor-pointer hover:border-surface-600 transition flex items-center gap-1.5 min-w-[130px]">
|
|
291
|
+
<span id="categoryDropdownLabel">All categories</span>
|
|
292
|
+
<svg class="w-3 h-3 text-gray-500 ml-auto flex-shrink-0" fill="none" stroke="currentColor"
|
|
293
|
+
stroke-width="2" viewBox="0 0 24 24">
|
|
294
|
+
<path stroke-linecap="round" d="m19 9-7 7-7-7" />
|
|
295
|
+
</svg>
|
|
296
|
+
</button>
|
|
297
|
+
<div id="categoryDropdownMenu"
|
|
298
|
+
class="hidden absolute top-full left-0 mt-1 w-52 bg-surface-900 border border-surface-700 rounded-xl shadow-xl shadow-black/40 z-30 py-1 max-h-64 overflow-y-auto custom-scroll">
|
|
299
|
+
<label
|
|
300
|
+
class="flex items-center gap-2 px-3 py-1.5 hover:bg-surface-800 cursor-pointer transition text-2xs border-b border-surface-800 mb-0.5">
|
|
301
|
+
<input type="checkbox" id="catCheckAll" checked onchange="toggleAllCategories(this.checked)"
|
|
302
|
+
class="accent-accent-500 rounded">
|
|
303
|
+
<span class="text-gray-300 font-medium">All categories</span>
|
|
304
|
+
</label>
|
|
305
|
+
<div id="categoryCheckboxes"></div>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
<select id="filterSeverity"
|
|
309
|
+
class="px-3 py-1.5 text-2xs rounded-lg bg-surface-850 border border-surface-700 text-gray-300 cursor-pointer">
|
|
310
|
+
<option value="">All severities</option>
|
|
311
|
+
<option value="error">Error</option>
|
|
312
|
+
<option value="warning">Warning</option>
|
|
313
|
+
<option value="info">Info</option>
|
|
314
|
+
</select>
|
|
315
|
+
<select id="filterTool"
|
|
316
|
+
class="px-3 py-1.5 text-2xs rounded-lg bg-surface-850 border border-surface-700 text-gray-300 cursor-pointer">
|
|
317
|
+
<option value="">All tools</option>
|
|
318
|
+
</select>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div id="diagList" class="space-y-2"></div>
|
|
323
|
+
|
|
324
|
+
<!-- Pagination -->
|
|
325
|
+
<div id="pagination" class="hidden flex items-center justify-between mt-4 pt-4 border-t border-surface-800">
|
|
326
|
+
<span id="pageInfo" class="text-2xs text-gray-500"></span>
|
|
327
|
+
<div class="flex items-center gap-2">
|
|
328
|
+
<button id="prevPage" onclick="changePage(-1)"
|
|
329
|
+
class="px-3 py-1.5 text-2xs rounded-lg bg-surface-850 border border-surface-700 text-gray-400 hover:text-white hover:border-surface-600 transition disabled:opacity-30 disabled:cursor-not-allowed">Prev</button>
|
|
330
|
+
<button id="nextPage" onclick="changePage(1)"
|
|
331
|
+
class="px-3 py-1.5 text-2xs rounded-lg bg-surface-850 border border-surface-700 text-gray-400 hover:text-white hover:border-surface-600 transition disabled:opacity-30 disabled:cursor-not-allowed">Next</button>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<!-- Footer -->
|
|
337
|
+
<div class="text-center text-2xs text-gray-600 py-4 border-t border-surface-800">
|
|
338
|
+
<span id="footerInfo"></span>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</main>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<!-- Re-run modal -->
|
|
345
|
+
<div id="rerunModal" class="hidden fixed inset-0 z-50">
|
|
346
|
+
<div class="modal-backdrop absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeRerunModal()"></div>
|
|
347
|
+
<div class="absolute inset-0 flex items-start justify-center pt-[10vh] px-4 pointer-events-none">
|
|
348
|
+
<div
|
|
349
|
+
class="modal-content pointer-events-auto w-full max-w-lg bg-surface-900 border border-surface-700 rounded-2xl shadow-2xl shadow-black/50 overflow-hidden">
|
|
350
|
+
<!-- Modal header -->
|
|
351
|
+
<div class="flex items-center justify-between px-5 py-4 border-b border-surface-800">
|
|
352
|
+
<div>
|
|
353
|
+
<h3 class="text-sm font-semibold text-white">Re-run analysis</h3>
|
|
354
|
+
<p class="text-2xs text-gray-500 mt-0.5">Select which analyzers to include</p>
|
|
355
|
+
</div>
|
|
356
|
+
<button onclick="closeRerunModal()"
|
|
357
|
+
class="p-1.5 rounded-lg text-gray-500 hover:text-white hover:bg-surface-800 transition" title="Close">
|
|
358
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
359
|
+
<path stroke-linecap="round" d="M6 18 18 6M6 6l12 12" />
|
|
360
|
+
</svg>
|
|
361
|
+
</button>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<!-- Analyzer list -->
|
|
365
|
+
<div class="px-5 py-4 max-h-[50vh] overflow-y-auto custom-scroll">
|
|
366
|
+
<div id="rerunAnalyzerList" class="space-y-1"></div>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<!-- Modal footer -->
|
|
370
|
+
<div class="flex items-center justify-between px-5 py-3 border-t border-surface-800 bg-surface-850/50">
|
|
371
|
+
<div class="flex items-center gap-3">
|
|
372
|
+
<button onclick="selectAllRerunAnalyzers(true)"
|
|
373
|
+
class="text-2xs text-accent-400 hover:text-accent-300 transition">Select all</button>
|
|
374
|
+
<span class="text-2xs text-surface-700">|</span>
|
|
375
|
+
<button onclick="selectAllRerunAnalyzers(false)"
|
|
376
|
+
class="text-2xs text-accent-400 hover:text-accent-300 transition">Clear</button>
|
|
377
|
+
<span id="rerunCount" class="text-2xs text-gray-600 ml-1"></span>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="flex items-center gap-2">
|
|
380
|
+
<div id="rerunStatus" class="hidden text-2xs text-gray-400 mr-2 flex items-center gap-2">
|
|
381
|
+
<span class="spinner"></span>
|
|
382
|
+
<span id="rerunStatusText">Running...</span>
|
|
383
|
+
</div>
|
|
384
|
+
<button onclick="closeRerunModal()"
|
|
385
|
+
class="px-3 py-1.5 text-2xs font-medium rounded-lg text-gray-400 hover:text-white transition">Cancel</button>
|
|
386
|
+
<button id="rerunExecBtn" onclick="executeRerun()"
|
|
387
|
+
class="px-4 py-1.5 text-2xs font-medium rounded-lg bg-accent-600 text-white hover:bg-accent-500 transition disabled:opacity-40 disabled:cursor-not-allowed">
|
|
388
|
+
Run analysis
|
|
389
|
+
</button>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
|
|
396
|
+
<!-- Toast notification -->
|
|
397
|
+
<div id="toast" class="toast hidden">
|
|
398
|
+
<div
|
|
399
|
+
class="px-4 py-2.5 rounded-xl bg-accent-900 border border-accent-700 text-accent-400 text-xs font-medium shadow-lg shadow-black/30">
|
|
400
|
+
Copied to clipboard
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<script>
|
|
405
|
+
const REPORT = {{REPORT_DATA}};
|
|
406
|
+
const SESSION_TOKEN = "{{SESSION_TOKEN}}";
|
|
407
|
+
const ANALYZER_CATALOG = {{ANALYZER_CATALOG}};
|
|
408
|
+
const CAN_RERUN = {{CAN_RERUN}};
|
|
409
|
+
const PAGE_SIZE = 50;
|
|
410
|
+
|
|
411
|
+
let currentPage = 0;
|
|
412
|
+
let filteredDiags = [];
|
|
413
|
+
let currentReport = REPORT;
|
|
414
|
+
|
|
415
|
+
// ── Category display names ──
|
|
416
|
+
const CATEGORY_LABELS = {
|
|
417
|
+
quality: 'Code Quality',
|
|
418
|
+
type_safety: 'Type Safety',
|
|
419
|
+
security: 'Security',
|
|
420
|
+
complexity: 'Complexity',
|
|
421
|
+
dead_code: 'Dead Code',
|
|
422
|
+
dependencies: 'Dependencies',
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const CATEGORY_ICONS = {
|
|
426
|
+
quality: `<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 0-6.23.693L5 14.5m14.8.8 1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0 1 12 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"/></svg>`,
|
|
427
|
+
type_safety: `<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75 22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3-4.5 16.5"/></svg>`,
|
|
428
|
+
security: `<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"/></svg>`,
|
|
429
|
+
complexity: `<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25a2.25 2.25 0 0 1-2.25-2.25v-2.25Z"/></svg>`,
|
|
430
|
+
dead_code: `<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"/></svg>`,
|
|
431
|
+
dependencies: `<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="m21 7.5-9-5.25L3 7.5m18 0-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9"/></svg>`,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const SEVERITY_COLORS = {
|
|
435
|
+
error: { bg: 'bg-red-500/10', text: 'text-red-400', border: 'border-red-500/20', badge: 'ERR' },
|
|
436
|
+
warning: { bg: 'bg-yellow-500/10', text: 'text-yellow-400', border: 'border-yellow-500/20', badge: 'WRN' },
|
|
437
|
+
info: { bg: 'bg-blue-500/10', text: 'text-blue-400', border: 'border-blue-500/20', badge: 'INF' },
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// ── Helpers ──
|
|
441
|
+
function scoreColor(score) {
|
|
442
|
+
if (score >= 80) return '#22c55e';
|
|
443
|
+
if (score >= 60) return '#eab308';
|
|
444
|
+
if (score >= 40) return '#f97316';
|
|
445
|
+
return '#ef4444';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function scoreTailwind(score) {
|
|
449
|
+
if (score >= 80) return 'text-green-400';
|
|
450
|
+
if (score >= 60) return 'text-yellow-400';
|
|
451
|
+
if (score >= 40) return 'text-orange-400';
|
|
452
|
+
return 'text-red-400';
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function escapeHtml(text) {
|
|
456
|
+
const d = document.createElement('div');
|
|
457
|
+
d.textContent = text;
|
|
458
|
+
return d.innerHTML;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function showToast(msg) {
|
|
462
|
+
const t = document.getElementById('toast');
|
|
463
|
+
t.querySelector('div').textContent = msg || 'Copied to clipboard';
|
|
464
|
+
t.classList.remove('hidden');
|
|
465
|
+
clearTimeout(t._timer);
|
|
466
|
+
t._timer = setTimeout(() => t.classList.add('hidden'), 2000);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function shortPath(p) {
|
|
470
|
+
const parts = p.split('/');
|
|
471
|
+
if (parts.length <= 3) return p;
|
|
472
|
+
return '.../' + parts.slice(-3).join('/');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Initialize ──
|
|
476
|
+
function init() {
|
|
477
|
+
document.getElementById('searchInput').addEventListener('input', applyFilters);
|
|
478
|
+
document.getElementById('filterSeverity').addEventListener('change', applyFilters);
|
|
479
|
+
document.getElementById('filterTool').addEventListener('change', applyFilters);
|
|
480
|
+
|
|
481
|
+
document.addEventListener('click', (e) => {
|
|
482
|
+
const dropdown = document.getElementById('categoryDropdown');
|
|
483
|
+
if (!dropdown.contains(e.target)) {
|
|
484
|
+
document.getElementById('categoryDropdownMenu').classList.add('hidden');
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// Close modal on Escape
|
|
489
|
+
document.addEventListener('keydown', (e) => {
|
|
490
|
+
if (e.key === 'Escape') closeRerunModal();
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (CAN_RERUN) {
|
|
494
|
+
document.getElementById('rerunBtn').classList.remove('hidden');
|
|
495
|
+
buildRerunAnalyzerList();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
renderReport(currentReport);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderReport(r) {
|
|
502
|
+
const score = r.score;
|
|
503
|
+
const color = scoreColor(score);
|
|
504
|
+
const arc = document.getElementById('scoreArc');
|
|
505
|
+
const circumference = 2 * Math.PI * 60;
|
|
506
|
+
arc.style.stroke = color;
|
|
507
|
+
arc.style.transition = 'stroke-dashoffset 1s ease-out';
|
|
508
|
+
arc.style.strokeDashoffset = circumference * (1 - score / 100);
|
|
509
|
+
|
|
510
|
+
document.getElementById('scoreValue').textContent = score;
|
|
511
|
+
document.getElementById('scoreValue').className = `text-3xl font-semibold tabular-nums ${scoreTailwind(score)}`;
|
|
512
|
+
document.getElementById('scoreLabel').textContent = r.label;
|
|
513
|
+
document.getElementById('headerLabel').textContent = `${score}/100 — ${r.label}`;
|
|
514
|
+
|
|
515
|
+
const totalIssues = r.diagnostics.length;
|
|
516
|
+
const errors = r.diagnostics.filter(d => d.severity === 'error').length;
|
|
517
|
+
const warnings = r.diagnostics.filter(d => d.severity === 'warning').length;
|
|
518
|
+
document.getElementById('scoreSummary').textContent =
|
|
519
|
+
`${totalIssues} issues found (${errors} errors, ${warnings} warnings) across ${r.analyzers_used.length} analyzers in ${(r.duration_ms / 1000).toFixed(1)}s.`;
|
|
520
|
+
|
|
521
|
+
document.getElementById('infoPython').textContent = r.project.python_version || 'Unknown';
|
|
522
|
+
document.getElementById('infoFramework').textContent = r.project.framework || 'None';
|
|
523
|
+
document.getElementById('infoFiles').textContent = r.project.total_files.toLocaleString();
|
|
524
|
+
document.getElementById('infoLines').textContent = r.project.total_lines.toLocaleString();
|
|
525
|
+
|
|
526
|
+
document.getElementById('headerMeta').textContent =
|
|
527
|
+
`${r.analyzers_used.length} analyzers | ${(r.duration_ms / 1000).toFixed(1)}s`;
|
|
528
|
+
|
|
529
|
+
document.getElementById('footerInfo').textContent =
|
|
530
|
+
`Analyzed with python-checkup | Analyzers: ${r.analyzers_used.join(', ')}` +
|
|
531
|
+
(r.analyzers_skipped.length ? ` | Skipped: ${r.analyzers_skipped.join(', ')}` : '');
|
|
532
|
+
|
|
533
|
+
renderCategories(r.category_scores);
|
|
534
|
+
populateFilters(r);
|
|
535
|
+
|
|
536
|
+
filteredDiags = r.diagnostics.slice();
|
|
537
|
+
currentPage = 0;
|
|
538
|
+
renderDiagnostics();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function populateFilters(r) {
|
|
542
|
+
const cats = [...new Set(r.diagnostics.map(d => d.category))].sort();
|
|
543
|
+
const container = document.getElementById('categoryCheckboxes');
|
|
544
|
+
container.innerHTML = cats.map(c => {
|
|
545
|
+
const label = CATEGORY_LABELS[c] || c;
|
|
546
|
+
const count = r.diagnostics.filter(d => d.category === c).length;
|
|
547
|
+
return `
|
|
548
|
+
<label class="flex items-center gap-2 px-3 py-1.5 hover:bg-surface-800 cursor-pointer transition text-2xs">
|
|
549
|
+
<input type="checkbox" value="${c}" checked onchange="onCategoryCheckChange()" class="accent-accent-500 rounded cat-check">
|
|
550
|
+
<span class="text-gray-300 flex-1">${escapeHtml(label)}</span>
|
|
551
|
+
<span class="text-gray-600 tabular-nums">${count}</span>
|
|
552
|
+
</label>`;
|
|
553
|
+
}).join('');
|
|
554
|
+
|
|
555
|
+
document.getElementById('catCheckAll').checked = true;
|
|
556
|
+
updateCategoryLabel();
|
|
557
|
+
|
|
558
|
+
const tools = new Set(r.diagnostics.map(d => d.tool));
|
|
559
|
+
const toolSelect = document.getElementById('filterTool');
|
|
560
|
+
while (toolSelect.options.length > 1) toolSelect.remove(1);
|
|
561
|
+
toolSelect.value = '';
|
|
562
|
+
[...tools].sort().forEach(t => {
|
|
563
|
+
const opt = document.createElement('option');
|
|
564
|
+
opt.value = t;
|
|
565
|
+
opt.textContent = t;
|
|
566
|
+
toolSelect.appendChild(opt);
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
document.getElementById('searchInput').value = '';
|
|
570
|
+
document.getElementById('filterSeverity').value = '';
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function renderCategories(categories) {
|
|
574
|
+
const container = document.getElementById('categoryCards');
|
|
575
|
+
const sorted = [...categories].sort((a, b) => {
|
|
576
|
+
const aIsComplexity = a.category === 'complexity' ? 1 : 0;
|
|
577
|
+
const bIsComplexity = b.category === 'complexity' ? 1 : 0;
|
|
578
|
+
if (aIsComplexity !== bIsComplexity) return aIsComplexity - bIsComplexity;
|
|
579
|
+
return a.score - b.score;
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
container.innerHTML = sorted.map((cs, i) => {
|
|
583
|
+
const label = CATEGORY_LABELS[cs.category] || cs.category;
|
|
584
|
+
const color = scoreColor(cs.score);
|
|
585
|
+
const icon = CATEGORY_ICONS[cs.category] || '';
|
|
586
|
+
return `
|
|
587
|
+
<div class="bg-surface-900 border border-surface-800 rounded-xl p-4 animate-enter stagger-${i + 1} hover:border-surface-700 transition cursor-pointer"
|
|
588
|
+
onclick="filterByCategory('${cs.category}')">
|
|
589
|
+
<div class="flex items-center justify-between mb-3">
|
|
590
|
+
<div class="flex items-center gap-2 text-gray-400">
|
|
591
|
+
${icon}
|
|
592
|
+
<span class="text-xs font-medium">${escapeHtml(label)}</span>
|
|
593
|
+
</div>
|
|
594
|
+
<span class="text-lg font-semibold tabular-nums" style="color:${color}">${cs.score}</span>
|
|
595
|
+
</div>
|
|
596
|
+
<div class="category-bar mb-2">
|
|
597
|
+
<div class="category-bar-fill" style="width:${cs.score}%;background:${color}"></div>
|
|
598
|
+
</div>
|
|
599
|
+
<div class="flex items-center justify-between text-2xs text-gray-500">
|
|
600
|
+
<span>${cs.issue_count} issue${cs.issue_count !== 1 ? 's' : ''}</span>
|
|
601
|
+
<span>weight: ${cs.weight}%</span>
|
|
602
|
+
</div>
|
|
603
|
+
${cs.details ? `<div class="text-2xs text-gray-600 mt-1 truncate">${escapeHtml(cs.details)}</div>` : ''}
|
|
604
|
+
</div>
|
|
605
|
+
`;
|
|
606
|
+
}).join('');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function filterByCategory(cat) {
|
|
610
|
+
const checks = document.querySelectorAll('.cat-check');
|
|
611
|
+
checks.forEach(cb => { cb.checked = (cb.value === cat); });
|
|
612
|
+
document.getElementById('catCheckAll').checked = false;
|
|
613
|
+
updateCategoryLabel();
|
|
614
|
+
applyFilters();
|
|
615
|
+
document.getElementById('diagList').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function toggleCategoryDropdown() {
|
|
619
|
+
document.getElementById('categoryDropdownMenu').classList.toggle('hidden');
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function toggleAllCategories(checked) {
|
|
623
|
+
document.querySelectorAll('.cat-check').forEach(cb => { cb.checked = checked; });
|
|
624
|
+
updateCategoryLabel();
|
|
625
|
+
applyFilters();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function onCategoryCheckChange() {
|
|
629
|
+
const checks = document.querySelectorAll('.cat-check');
|
|
630
|
+
const allChecked = [...checks].every(cb => cb.checked);
|
|
631
|
+
const noneChecked = [...checks].every(cb => !cb.checked);
|
|
632
|
+
document.getElementById('catCheckAll').checked = allChecked;
|
|
633
|
+
if (noneChecked) {
|
|
634
|
+
document.getElementById('catCheckAll').checked = true;
|
|
635
|
+
checks.forEach(cb => { cb.checked = true; });
|
|
636
|
+
}
|
|
637
|
+
updateCategoryLabel();
|
|
638
|
+
applyFilters();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function getSelectedCategories() {
|
|
642
|
+
const checks = document.querySelectorAll('.cat-check');
|
|
643
|
+
const selected = [...checks].filter(cb => cb.checked).map(cb => cb.value);
|
|
644
|
+
if (selected.length === 0 || selected.length === checks.length) return null;
|
|
645
|
+
return new Set(selected);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function updateCategoryLabel() {
|
|
649
|
+
const selected = getSelectedCategories();
|
|
650
|
+
const label = document.getElementById('categoryDropdownLabel');
|
|
651
|
+
if (!selected) {
|
|
652
|
+
label.textContent = 'All categories';
|
|
653
|
+
} else if (selected.size === 1) {
|
|
654
|
+
const cat = [...selected][0];
|
|
655
|
+
label.textContent = CATEGORY_LABELS[cat] || cat;
|
|
656
|
+
} else {
|
|
657
|
+
label.textContent = `${selected.size} categories`;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function applyFilters() {
|
|
662
|
+
const search = document.getElementById('searchInput').value.toLowerCase();
|
|
663
|
+
const selectedCats = getSelectedCategories();
|
|
664
|
+
const sev = document.getElementById('filterSeverity').value;
|
|
665
|
+
const tool = document.getElementById('filterTool').value;
|
|
666
|
+
|
|
667
|
+
filteredDiags = currentReport.diagnostics.filter(d => {
|
|
668
|
+
if (selectedCats && !selectedCats.has(d.category)) return false;
|
|
669
|
+
if (sev && d.severity !== sev) return false;
|
|
670
|
+
if (tool && d.tool !== tool) return false;
|
|
671
|
+
if (search) {
|
|
672
|
+
const haystack = `${d.file_path} ${d.rule_id} ${d.message} ${d.tool}`.toLowerCase();
|
|
673
|
+
if (!haystack.includes(search)) return false;
|
|
674
|
+
}
|
|
675
|
+
return true;
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
currentPage = 0;
|
|
679
|
+
renderDiagnostics();
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function renderDiagnostics() {
|
|
683
|
+
const container = document.getElementById('diagList');
|
|
684
|
+
const total = filteredDiags.length;
|
|
685
|
+
|
|
686
|
+
document.getElementById('diagCount').textContent = `(${total})`;
|
|
687
|
+
|
|
688
|
+
if (total === 0) {
|
|
689
|
+
container.innerHTML = `
|
|
690
|
+
<div class="text-center py-12 text-gray-500">
|
|
691
|
+
<div class="text-lg mb-2">No issues found</div>
|
|
692
|
+
<div class="text-2xs">Try adjusting your filters</div>
|
|
693
|
+
</div>`;
|
|
694
|
+
document.getElementById('pagination').classList.add('hidden');
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const start = currentPage * PAGE_SIZE;
|
|
699
|
+
const pageItems = filteredDiags.slice(start, start + PAGE_SIZE);
|
|
700
|
+
|
|
701
|
+
const byFile = new Map();
|
|
702
|
+
for (const d of pageItems) {
|
|
703
|
+
if (!byFile.has(d.file_path)) byFile.set(d.file_path, []);
|
|
704
|
+
byFile.get(d.file_path).push(d);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let html = '';
|
|
708
|
+
for (const [filePath, diags] of byFile) {
|
|
709
|
+
const fileId = filePath.replace(/[^a-zA-Z0-9]/g, '_');
|
|
710
|
+
const errorCount = diags.filter(d => d.severity === 'error').length;
|
|
711
|
+
const warnCount = diags.filter(d => d.severity === 'warning').length;
|
|
712
|
+
|
|
713
|
+
html += `
|
|
714
|
+
<div class="bg-surface-900 border border-surface-800 rounded-xl overflow-hidden">
|
|
715
|
+
<div class="flex items-center justify-between px-4 py-2.5 bg-surface-850 cursor-pointer select-none"
|
|
716
|
+
onclick="toggleFileGroup('${fileId}')">
|
|
717
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
718
|
+
<svg class="w-3.5 h-3.5 text-gray-500 transition-transform" id="chevron_${fileId}" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" d="m19 9-7 7-7-7"/></svg>
|
|
719
|
+
<span class="text-xs font-mono text-gray-300 truncate" title="${escapeHtml(filePath)}">${escapeHtml(shortPath(filePath))}</span>
|
|
720
|
+
<span class="text-2xs text-gray-500">(${diags.length})</span>
|
|
721
|
+
</div>
|
|
722
|
+
<div class="flex items-center gap-2 flex-shrink-0">
|
|
723
|
+
${errorCount ? `<span class="text-2xs text-red-400">${errorCount} err</span>` : ''}
|
|
724
|
+
${warnCount ? `<span class="text-2xs text-yellow-400">${warnCount} wrn</span>` : ''}
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
<div id="group_${fileId}" class="divide-y divide-surface-800">
|
|
728
|
+
${diags.map(d => renderDiagRow(d)).join('')}
|
|
729
|
+
</div>
|
|
730
|
+
</div>`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
container.innerHTML = html;
|
|
734
|
+
|
|
735
|
+
const totalPages = Math.ceil(total / PAGE_SIZE);
|
|
736
|
+
if (totalPages > 1) {
|
|
737
|
+
document.getElementById('pagination').classList.remove('hidden');
|
|
738
|
+
document.getElementById('pageInfo').textContent = `Page ${currentPage + 1} of ${totalPages} (${total} issues)`;
|
|
739
|
+
document.getElementById('prevPage').disabled = currentPage === 0;
|
|
740
|
+
document.getElementById('nextPage').disabled = currentPage >= totalPages - 1;
|
|
741
|
+
} else {
|
|
742
|
+
document.getElementById('pagination').classList.add('hidden');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function buildCopyPrompt(d) {
|
|
747
|
+
const parts = [
|
|
748
|
+
`Fix the following ${d.severity} in my codebase:`,
|
|
749
|
+
``,
|
|
750
|
+
`File: ${d.file_path}`,
|
|
751
|
+
`Line: ${d.line}`,
|
|
752
|
+
`Rule: ${d.rule_id} (${d.tool})`,
|
|
753
|
+
`Issue: ${d.message}`,
|
|
754
|
+
];
|
|
755
|
+
if (d.fix) parts.push(`Suggested fix: ${d.fix}`);
|
|
756
|
+
if (d.help_url) parts.push(`Reference: ${d.help_url}`);
|
|
757
|
+
parts.push(
|
|
758
|
+
``,
|
|
759
|
+
`Read the file, understand the context around line ${d.line}, and apply the minimal change needed to resolve this. Do not change unrelated code.`
|
|
760
|
+
);
|
|
761
|
+
return parts.join('\n');
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function copyDiagPrompt(btn, d) {
|
|
765
|
+
const prompt = buildCopyPrompt(d);
|
|
766
|
+
navigator.clipboard.writeText(prompt).then(() => {
|
|
767
|
+
// Brief visual feedback on the button
|
|
768
|
+
btn.classList.add('text-accent-400');
|
|
769
|
+
setTimeout(() => btn.classList.remove('text-accent-400'), 1200);
|
|
770
|
+
showToast('Copied fix prompt to clipboard');
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function renderDiagRow(d) {
|
|
775
|
+
const sev = SEVERITY_COLORS[d.severity] || SEVERITY_COLORS.info;
|
|
776
|
+
const catLabel = CATEGORY_LABELS[d.category] || d.category;
|
|
777
|
+
const fixHtml = d.fix ? `<div class="text-2xs text-accent-400 mt-1">Fix: ${escapeHtml(d.fix)}</div>` : '';
|
|
778
|
+
const helpHtml = d.help_url ? `<a href="${escapeHtml(d.help_url)}" target="_blank" rel="noopener" class="text-2xs text-accent-400/60 hover:text-accent-400 ml-2">docs</a>` : '';
|
|
779
|
+
|
|
780
|
+
// Escape diagnostic data for inline onclick
|
|
781
|
+
const diagJson = JSON.stringify(d).replace(/'/g, ''').replace(/"/g, '"');
|
|
782
|
+
|
|
783
|
+
return `
|
|
784
|
+
<div class="group/row px-4 py-2.5 row-hover flex items-start gap-3 relative">
|
|
785
|
+
<span class="flex-shrink-0 px-1.5 py-0.5 text-2xs font-mono font-medium rounded ${sev.bg} ${sev.text} border ${sev.border}">${sev.badge}</span>
|
|
786
|
+
<div class="min-w-0 flex-1">
|
|
787
|
+
<div class="flex items-baseline gap-2 flex-wrap">
|
|
788
|
+
<span class="text-xs font-mono text-gray-400">${d.line}:${d.column || 0}</span>
|
|
789
|
+
<span class="text-xs font-mono text-accent-400/80">${escapeHtml(d.rule_id)}</span>
|
|
790
|
+
<span class="text-2xs text-gray-600">${escapeHtml(d.tool)}</span>
|
|
791
|
+
<span class="text-2xs text-gray-600">${escapeHtml(catLabel)}</span>
|
|
792
|
+
${helpHtml}
|
|
793
|
+
</div>
|
|
794
|
+
<div class="text-xs text-gray-300 mt-0.5">${escapeHtml(d.message)}</div>
|
|
795
|
+
${fixHtml}
|
|
796
|
+
</div>
|
|
797
|
+
<button onclick="copyDiagPrompt(this, ${diagJson})"
|
|
798
|
+
class="flex-shrink-0 p-1.5 rounded-lg text-gray-600 hover:text-accent-400 hover:bg-surface-800 transition-all"
|
|
799
|
+
title="Copy fix prompt for AI">
|
|
800
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
|
|
801
|
+
</button>
|
|
802
|
+
</div>`;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function toggleFileGroup(fileId) {
|
|
806
|
+
const group = document.getElementById('group_' + fileId);
|
|
807
|
+
const chevron = document.getElementById('chevron_' + fileId);
|
|
808
|
+
if (group.style.display === 'none') {
|
|
809
|
+
group.style.display = '';
|
|
810
|
+
chevron.style.transform = '';
|
|
811
|
+
} else {
|
|
812
|
+
group.style.display = 'none';
|
|
813
|
+
chevron.style.transform = 'rotate(-90deg)';
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function changePage(delta) {
|
|
818
|
+
const totalPages = Math.ceil(filteredDiags.length / PAGE_SIZE);
|
|
819
|
+
currentPage = Math.max(0, Math.min(totalPages - 1, currentPage + delta));
|
|
820
|
+
renderDiagnostics();
|
|
821
|
+
document.getElementById('diagList').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ── Re-run modal ──
|
|
825
|
+
function buildRerunAnalyzerList() {
|
|
826
|
+
const container = document.getElementById('rerunAnalyzerList');
|
|
827
|
+
|
|
828
|
+
// Group analyzers by category
|
|
829
|
+
const byCat = {};
|
|
830
|
+
for (const [name, info] of Object.entries(ANALYZER_CATALOG)) {
|
|
831
|
+
for (const cat of info.categories) {
|
|
832
|
+
if (!byCat[cat]) byCat[cat] = [];
|
|
833
|
+
byCat[cat].push({ name, info });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Sort categories by label
|
|
838
|
+
const catOrder = ['quality', 'type_safety', 'security', 'complexity', 'dead_code', 'dependencies'];
|
|
839
|
+
const sortedCats = catOrder.filter(c => byCat[c]);
|
|
840
|
+
|
|
841
|
+
let html = '';
|
|
842
|
+
for (const cat of sortedCats) {
|
|
843
|
+
const label = CATEGORY_LABELS[cat] || cat;
|
|
844
|
+
const icon = CATEGORY_ICONS[cat] || '';
|
|
845
|
+
const analyzers = byCat[cat];
|
|
846
|
+
// Deduplicate (an analyzer can appear in multiple categories)
|
|
847
|
+
const seen = new Set();
|
|
848
|
+
|
|
849
|
+
html += `
|
|
850
|
+
<div class="mb-3 last:mb-0">
|
|
851
|
+
<div class="flex items-center gap-1.5 text-gray-500 mb-1.5 px-1">
|
|
852
|
+
${icon}
|
|
853
|
+
<span class="text-2xs font-medium uppercase tracking-wider">${escapeHtml(label)}</span>
|
|
854
|
+
</div>
|
|
855
|
+
<div class="space-y-0.5">`;
|
|
856
|
+
|
|
857
|
+
for (const { name, info } of analyzers) {
|
|
858
|
+
if (seen.has(name)) continue;
|
|
859
|
+
seen.add(name);
|
|
860
|
+
const optBadge = info.optional
|
|
861
|
+
? '<span class="ml-auto text-2xs text-gray-600 bg-surface-800 px-1.5 py-0.5 rounded">optional</span>'
|
|
862
|
+
: '';
|
|
863
|
+
|
|
864
|
+
html += `
|
|
865
|
+
<label class="flex items-center gap-2.5 px-3 py-2 rounded-lg hover:bg-surface-800 cursor-pointer transition group">
|
|
866
|
+
<input type="checkbox" value="${name}" checked onchange="updateRerunCount()" class="accent-accent-500 rounded rerun-check">
|
|
867
|
+
<span class="text-xs text-gray-300 group-hover:text-white transition font-mono">${name}</span>
|
|
868
|
+
${optBadge}
|
|
869
|
+
</label>`;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
html += `
|
|
873
|
+
</div>
|
|
874
|
+
</div>`;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
container.innerHTML = html;
|
|
878
|
+
updateRerunCount();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function updateRerunCount() {
|
|
882
|
+
const total = document.querySelectorAll('.rerun-check').length;
|
|
883
|
+
const checked = document.querySelectorAll('.rerun-check:checked').length;
|
|
884
|
+
document.getElementById('rerunCount').textContent = `${checked}/${total} selected`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function selectAllRerunAnalyzers(checked) {
|
|
888
|
+
document.querySelectorAll('.rerun-check').forEach(cb => { cb.checked = checked; });
|
|
889
|
+
updateRerunCount();
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function openRerunModal() {
|
|
893
|
+
document.getElementById('rerunModal').classList.remove('hidden');
|
|
894
|
+
document.body.style.overflow = 'hidden';
|
|
895
|
+
// Reset status
|
|
896
|
+
const status = document.getElementById('rerunStatus');
|
|
897
|
+
status.classList.add('hidden');
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function closeRerunModal() {
|
|
901
|
+
document.getElementById('rerunModal').classList.add('hidden');
|
|
902
|
+
document.body.style.overflow = '';
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
async function executeRerun() {
|
|
906
|
+
const selected = [...document.querySelectorAll('.rerun-check:checked')].map(cb => cb.value);
|
|
907
|
+
|
|
908
|
+
if (selected.length === 0) {
|
|
909
|
+
showToast('Select at least one analyzer');
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const btn = document.getElementById('rerunExecBtn');
|
|
914
|
+
const status = document.getElementById('rerunStatus');
|
|
915
|
+
const statusText = document.getElementById('rerunStatusText');
|
|
916
|
+
|
|
917
|
+
btn.disabled = true;
|
|
918
|
+
btn.textContent = 'Running...';
|
|
919
|
+
status.classList.remove('hidden');
|
|
920
|
+
statusText.textContent = `Running ${selected.length} analyzer${selected.length > 1 ? 's' : ''}...`;
|
|
921
|
+
statusText.className = 'text-gray-400';
|
|
922
|
+
|
|
923
|
+
try {
|
|
924
|
+
const resp = await fetch('/api/rerun', {
|
|
925
|
+
method: 'POST',
|
|
926
|
+
headers: {
|
|
927
|
+
'Content-Type': 'application/json',
|
|
928
|
+
'X-Session-Token': SESSION_TOKEN,
|
|
929
|
+
},
|
|
930
|
+
body: JSON.stringify({ analyzers: selected }),
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const data = await resp.json();
|
|
934
|
+
|
|
935
|
+
if (!resp.ok) {
|
|
936
|
+
statusText.textContent = data.error || 'Unknown error';
|
|
937
|
+
statusText.className = 'text-red-400';
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
currentReport = data.report;
|
|
942
|
+
renderReport(currentReport);
|
|
943
|
+
closeRerunModal();
|
|
944
|
+
showToast(`Analysis complete — score: ${currentReport.score}/100`);
|
|
945
|
+
} catch (e) {
|
|
946
|
+
statusText.textContent = `Network error: ${e.message}`;
|
|
947
|
+
statusText.className = 'text-red-400';
|
|
948
|
+
} finally {
|
|
949
|
+
btn.disabled = false;
|
|
950
|
+
btn.textContent = 'Run analysis';
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ── Boot ──
|
|
955
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
956
|
+
</script>
|
|
957
|
+
</body>
|
|
958
|
+
|
|
959
|
+
</html>
|