tech-debt-visualizer 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/cli.js +8 -6
- package/dist/llm.d.ts +1 -1
- package/dist/llm.js +17 -8
- package/dist/reports/assets/report.css +320 -121
- package/dist/reports/assets/report.js +44 -20
- package/dist/reports/html.js +103 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ So: **static metrics + git → score & tier → optional LLM explanations and co
|
|
|
68
68
|
| **Global (after publish)** | `npm install -g tech-debt-visualizer` then `tech-debt analyze [path]` |
|
|
69
69
|
| **No install (after publish)** | `npx tech-debt-visualizer analyze [path]` |
|
|
70
70
|
|
|
71
|
+
Use **`tech-debt-visualizer`** in the command (not `tech-debt`); `npx tech-debt` runs a different npm package. From this repo you can also run `npm run analyze` or `node dist/cli.js analyze .`.
|
|
72
|
+
|
|
71
73
|
Requires **Node 18+**.
|
|
72
74
|
|
|
73
75
|
---
|
package/dist/cli.js
CHANGED
|
@@ -30,9 +30,10 @@ program
|
|
|
30
30
|
.option("-f, --format <type>", "Output format: cli | html | json | markdown", "cli")
|
|
31
31
|
.option("--no-llm", "Skip LLM-powered insights")
|
|
32
32
|
.option("--llm", "Enable LLM (default). Use with --llm-key and/or --llm-model")
|
|
33
|
-
.option("--llm-key <key>", "API key
|
|
34
|
-
.option("--llm-
|
|
35
|
-
.option("--llm-
|
|
33
|
+
.option("--llm-key <key>", "API key (overrides env: GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY)")
|
|
34
|
+
.option("--llm-endpoint <url>", "OpenAI-compatible API base URL (e.g. https://api.openai.com/v1 or proxy)")
|
|
35
|
+
.option("--llm-model <model>", "Model name (e.g. gpt-4o-mini, gemini-2.5-flash)")
|
|
36
|
+
.option("--llm-max-tokens <n>", "Max tokens per response", (v) => parseInt(v, 10))
|
|
36
37
|
.option("--ci", "CI mode: minimal output, exit with non-zero if debt score is high")
|
|
37
38
|
.action(async (path, opts) => {
|
|
38
39
|
const repoPath = join(process.cwd(), path);
|
|
@@ -41,6 +42,7 @@ program
|
|
|
41
42
|
const outputPath = opts.output ?? (format === "html" ? "tech-debt-report.html" : undefined);
|
|
42
43
|
const llmConfigOverrides = {
|
|
43
44
|
apiKey: opts.llmKey,
|
|
45
|
+
baseURL: opts.llmEndpoint,
|
|
44
46
|
model: opts.llmModel,
|
|
45
47
|
...(opts.llmMaxTokens != null && opts.llmMaxTokens > 0 ? { maxTokens: opts.llmMaxTokens } : {}),
|
|
46
48
|
};
|
|
@@ -82,9 +84,9 @@ program
|
|
|
82
84
|
if (!llmConfig) {
|
|
83
85
|
progress.update(totalSteps, { task: "Skipping LLM (no key)" });
|
|
84
86
|
progress.stop();
|
|
85
|
-
process.stderr.write(chalk.yellow(" No LLM API key found.
|
|
86
|
-
"
|
|
87
|
-
"
|
|
87
|
+
process.stderr.write(chalk.yellow(" No LLM API key found. Use --llm-key <key> or set one of:\n" +
|
|
88
|
+
" GEMINI_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY (or .env).\n" +
|
|
89
|
+
" For a custom endpoint: --llm-endpoint <url> --llm-key <key>\n" +
|
|
88
90
|
" Skipping AI insights for this run.\n\n"));
|
|
89
91
|
}
|
|
90
92
|
else {
|
package/dist/llm.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface LLMConfig {
|
|
|
18
18
|
maxTokens?: number;
|
|
19
19
|
}
|
|
20
20
|
export type LLMProvider = "openai" | "openrouter" | "gemini";
|
|
21
|
-
/** Resolve provider and auth from config + env.
|
|
21
|
+
/** Resolve provider and auth from config + env. Explicit baseURL = OpenAI-compatible; else key format or env picks provider. */
|
|
22
22
|
export declare function resolveLLMConfig(config?: LLMConfig): {
|
|
23
23
|
provider: LLMProvider;
|
|
24
24
|
apiKey: string;
|
package/dist/llm.js
CHANGED
|
@@ -68,13 +68,20 @@ function parseFileAssessmentResponse(raw) {
|
|
|
68
68
|
code: code || undefined,
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
|
-
/** Resolve provider and auth from config + env.
|
|
71
|
+
/** Resolve provider and auth from config + env. Explicit baseURL = OpenAI-compatible; else key format or env picks provider. */
|
|
72
72
|
export function resolveLLMConfig(config = {}) {
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const openaiKey = cliKey ?? process.env.OPENAI_API_KEY ?? process.env.ANTHROPIC_API_KEY;
|
|
77
|
-
|
|
73
|
+
const trim = (s) => (typeof s === "string" ? s.trim() : undefined) || undefined;
|
|
74
|
+
const explicitBase = (config.baseURL ?? process.env.OPENAI_BASE_URL)?.replace(/\/$/, "");
|
|
75
|
+
const cliKey = trim(config.apiKey);
|
|
76
|
+
const openaiKey = trim(cliKey ?? process.env.OPENAI_API_KEY ?? process.env.ANTHROPIC_API_KEY);
|
|
77
|
+
if (explicitBase && openaiKey) {
|
|
78
|
+
return {
|
|
79
|
+
provider: "openai",
|
|
80
|
+
apiKey: openaiKey,
|
|
81
|
+
baseURL: explicitBase,
|
|
82
|
+
model: config.model ?? process.env.OPENAI_MODEL ?? OPENAI_DEFAULT_MODEL,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
78
85
|
if (cliKey) {
|
|
79
86
|
if (cliKey.startsWith("AIza")) {
|
|
80
87
|
return {
|
|
@@ -88,11 +95,12 @@ export function resolveLLMConfig(config = {}) {
|
|
|
88
95
|
return {
|
|
89
96
|
provider: "openai",
|
|
90
97
|
apiKey: cliKey,
|
|
91
|
-
baseURL: config.baseURL ?? process.env.OPENAI_BASE_URL ?? "",
|
|
98
|
+
baseURL: (config.baseURL ?? process.env.OPENAI_BASE_URL ?? "").replace(/\/$/, ""),
|
|
92
99
|
model: config.model ?? process.env.OPENAI_MODEL ?? OPENAI_DEFAULT_MODEL,
|
|
93
100
|
};
|
|
94
101
|
}
|
|
95
102
|
}
|
|
103
|
+
const openRouterKey = trim(cliKey ?? process.env.OPENROUTER_API_KEY);
|
|
96
104
|
if (openRouterKey) {
|
|
97
105
|
return {
|
|
98
106
|
provider: "openrouter",
|
|
@@ -101,6 +109,7 @@ export function resolveLLMConfig(config = {}) {
|
|
|
101
109
|
model: config.model ?? process.env.OPENROUTER_MODEL ?? OPENROUTER_DEFAULT_MODEL,
|
|
102
110
|
};
|
|
103
111
|
}
|
|
112
|
+
const geminiKey = trim(cliKey ?? process.env.GEMINI_API_KEY ?? process.env.GOOGLE_GENAI_API_KEY);
|
|
104
113
|
if (geminiKey) {
|
|
105
114
|
return {
|
|
106
115
|
provider: "gemini",
|
|
@@ -113,7 +122,7 @@ export function resolveLLMConfig(config = {}) {
|
|
|
113
122
|
return {
|
|
114
123
|
provider: "openai",
|
|
115
124
|
apiKey: openaiKey,
|
|
116
|
-
baseURL: config.baseURL ?? process.env.OPENAI_BASE_URL ?? "",
|
|
125
|
+
baseURL: (config.baseURL ?? process.env.OPENAI_BASE_URL ?? "").replace(/\/$/, ""),
|
|
117
126
|
model: config.model ?? process.env.OPENAI_MODEL ?? OPENAI_DEFAULT_MODEL,
|
|
118
127
|
};
|
|
119
128
|
}
|
|
@@ -1,141 +1,297 @@
|
|
|
1
|
-
/* Technical Debt Report -
|
|
1
|
+
/* Technical Debt Report — Grafana-style dashboard */
|
|
2
2
|
:root {
|
|
3
|
-
--bg: #
|
|
4
|
-
--
|
|
5
|
-
--
|
|
6
|
-
--
|
|
7
|
-
--
|
|
8
|
-
--
|
|
3
|
+
--bg: #0b0c0e;
|
|
4
|
+
--bg-elevated: #111214;
|
|
5
|
+
--surface: #181b1f;
|
|
6
|
+
--surface-hover: #1e2127;
|
|
7
|
+
--border: #2c3239;
|
|
8
|
+
--border-subtle: #252a30;
|
|
9
|
+
--text: #e8e9ea;
|
|
10
|
+
--text-muted: #8b9199;
|
|
11
|
+
--link: #5794f2;
|
|
12
|
+
--radius: 4px;
|
|
13
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
[data-theme="
|
|
12
|
-
--bg: #
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
--
|
|
16
|
+
[data-theme="light"] {
|
|
17
|
+
--bg: #f4f5f5;
|
|
18
|
+
--bg-elevated: #fff;
|
|
19
|
+
--surface: #fff;
|
|
20
|
+
--surface-hover: #f9fafb;
|
|
21
|
+
--border: #d8d9da;
|
|
22
|
+
--border-subtle: #e8e9ea;
|
|
23
|
+
--text: #1e2022;
|
|
24
|
+
--text-muted: #6b7077;
|
|
25
|
+
--link: #3274d9;
|
|
26
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
* { box-sizing: border-box; }
|
|
21
30
|
|
|
22
31
|
body {
|
|
23
32
|
margin: 0;
|
|
24
|
-
font-family: system
|
|
33
|
+
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
25
34
|
background: var(--bg);
|
|
26
35
|
color: var(--text);
|
|
27
36
|
min-height: 100vh;
|
|
28
|
-
font-size:
|
|
37
|
+
font-size: 13px;
|
|
29
38
|
line-height: 1.5;
|
|
39
|
+
-webkit-font-smoothing: antialiased;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
margin: 0 auto;
|
|
35
|
-
padding: 1.5rem 1.25rem;
|
|
42
|
+
body.dashboard-page {
|
|
43
|
+
padding-bottom: 2rem;
|
|
36
44
|
}
|
|
37
45
|
|
|
38
|
-
/*
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
/* ——— Dashboard header (Grafana-style top bar) ——— */
|
|
47
|
+
.dashboard-header {
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
flex-wrap: wrap;
|
|
52
|
+
gap: 0.75rem;
|
|
53
|
+
padding: 0.75rem 1.5rem;
|
|
54
|
+
background: var(--bg-elevated);
|
|
42
55
|
border-bottom: 1px solid var(--border);
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
position: sticky;
|
|
57
|
+
top: 0;
|
|
58
|
+
z-index: 10;
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
.
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
.dashboard-header-left {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: baseline;
|
|
64
|
+
gap: 0.75rem;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.dashboard-title {
|
|
69
|
+
margin: 0;
|
|
70
|
+
font-size: 1.125rem;
|
|
71
|
+
font-weight: 600;
|
|
72
|
+
letter-spacing: -0.01em;
|
|
50
73
|
color: var(--text);
|
|
74
|
+
white-space: nowrap;
|
|
75
|
+
overflow: hidden;
|
|
76
|
+
text-overflow: ellipsis;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.dashboard-meta {
|
|
80
|
+
font-size: 12px;
|
|
81
|
+
color: var(--text-muted);
|
|
82
|
+
white-space: nowrap;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
text-overflow: ellipsis;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.dashboard-header-right {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 1rem;
|
|
91
|
+
flex-shrink: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.dashboard-score {
|
|
95
|
+
display: inline-flex;
|
|
96
|
+
align-items: baseline;
|
|
97
|
+
gap: 0.2rem;
|
|
98
|
+
padding: 0.35rem 0.65rem;
|
|
99
|
+
border-radius: var(--radius);
|
|
100
|
+
font-weight: 600;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.dashboard-score-value {
|
|
104
|
+
font-size: 1.25rem;
|
|
105
|
+
line-height: 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.dashboard-score-of {
|
|
109
|
+
font-size: 0.85rem;
|
|
110
|
+
font-weight: 500;
|
|
111
|
+
color: var(--text-muted);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.dashboard-score-label {
|
|
115
|
+
font-size: 11px;
|
|
116
|
+
font-weight: 500;
|
|
117
|
+
text-transform: uppercase;
|
|
118
|
+
letter-spacing: 0.04em;
|
|
119
|
+
margin-left: 0.35rem;
|
|
120
|
+
opacity: 0.95;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.dashboard-score.tier-1 { background: rgba(204, 0, 0, 0.2); color: #f66; }
|
|
124
|
+
.dashboard-score.tier-2 { background: rgba(232, 93, 0, 0.2); color: #f90; }
|
|
125
|
+
.dashboard-score.tier-3 { background: rgba(184, 134, 11, 0.2); color: #db9; }
|
|
126
|
+
.dashboard-score.tier-4 { background: rgba(0, 102, 153, 0.2); color: #6cf; }
|
|
127
|
+
.dashboard-score.tier-5 { background: rgba(10, 107, 10, 0.2); color: #6c6; }
|
|
128
|
+
|
|
129
|
+
.dashboard-date {
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
color: var(--text-muted);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* ——— Main content & grid ——— */
|
|
135
|
+
.dashboard-main {
|
|
136
|
+
max-width: 1200px;
|
|
137
|
+
margin: 0 auto;
|
|
138
|
+
padding: 1.25rem 1.5rem;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.dashboard-grid {
|
|
142
|
+
display: grid;
|
|
143
|
+
gap: 1rem;
|
|
144
|
+
margin-bottom: 1rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.dashboard-grid-stats {
|
|
148
|
+
grid-template-columns: repeat(4, 1fr);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.dashboard-grid-half {
|
|
152
|
+
grid-template-columns: repeat(2, 1fr);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@media (max-width: 900px) {
|
|
156
|
+
.dashboard-grid-stats { grid-template-columns: repeat(2, 1fr); }
|
|
157
|
+
.dashboard-grid-half { grid-template-columns: 1fr; }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@media (max-width: 560px) {
|
|
161
|
+
.dashboard-header { padding: 0.6rem 1rem; }
|
|
162
|
+
.dashboard-main { padding: 1rem; }
|
|
163
|
+
.dashboard-grid-stats { grid-template-columns: 1fr; }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ——— Panels (Grafana-style) ——— */
|
|
167
|
+
.panel {
|
|
168
|
+
background: var(--surface);
|
|
169
|
+
border: 1px solid var(--border);
|
|
170
|
+
border-radius: var(--radius);
|
|
171
|
+
box-shadow: var(--shadow);
|
|
172
|
+
overflow: hidden;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.panel-header {
|
|
176
|
+
padding: 0.75rem 1rem;
|
|
177
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
178
|
+
background: rgba(0, 0, 0, 0.03);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
[data-theme="light"] .panel-header {
|
|
182
|
+
background: rgba(0, 0, 0, 0.02);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.panel-title {
|
|
51
186
|
margin: 0;
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
font-weight: 600;
|
|
189
|
+
color: var(--text);
|
|
190
|
+
letter-spacing: -0.01em;
|
|
52
191
|
}
|
|
53
192
|
|
|
54
|
-
|
|
55
|
-
.
|
|
193
|
+
.panel-desc {
|
|
194
|
+
margin: 0.25rem 0 0;
|
|
56
195
|
font-size: 12px;
|
|
57
196
|
color: var(--text-muted);
|
|
58
|
-
|
|
59
|
-
text-align: center;
|
|
197
|
+
line-height: 1.4;
|
|
60
198
|
}
|
|
61
199
|
|
|
62
|
-
.
|
|
200
|
+
.panel-body {
|
|
201
|
+
padding: 1rem;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.panel-body-center {
|
|
63
205
|
text-align: center;
|
|
64
|
-
margin-bottom: 1.5rem;
|
|
65
206
|
}
|
|
66
207
|
|
|
67
|
-
|
|
208
|
+
/* Stat panels (KPI row) */
|
|
209
|
+
.stat-panel .panel-body {
|
|
210
|
+
padding: 1rem 1.25rem;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.stat-value {
|
|
214
|
+
font-size: 1.75rem;
|
|
215
|
+
font-weight: 700;
|
|
216
|
+
line-height: 1.2;
|
|
217
|
+
color: var(--text);
|
|
218
|
+
letter-spacing: -0.02em;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.stat-label {
|
|
222
|
+
font-size: 11px;
|
|
223
|
+
font-weight: 500;
|
|
224
|
+
text-transform: uppercase;
|
|
225
|
+
letter-spacing: 0.04em;
|
|
226
|
+
color: var(--text-muted);
|
|
227
|
+
margin-top: 0.25rem;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.stat-panel-warn .stat-value { color: #f44; }
|
|
231
|
+
|
|
232
|
+
/* Score panel (cleanliness gauge) */
|
|
233
|
+
.panel-score .panel-body-center {
|
|
234
|
+
padding: 1.25rem 1rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.panel-score .score-badge {
|
|
68
238
|
display: inline-block;
|
|
69
239
|
margin-bottom: 0.75rem;
|
|
70
240
|
line-height: 0;
|
|
71
241
|
}
|
|
72
242
|
|
|
73
|
-
.score-badge-svg {
|
|
243
|
+
.panel-score .score-badge-svg {
|
|
74
244
|
display: block;
|
|
75
|
-
width:
|
|
245
|
+
width: 120px;
|
|
76
246
|
height: auto;
|
|
77
|
-
max-width: min(160px, 40vw);
|
|
78
247
|
}
|
|
79
248
|
|
|
80
|
-
.score-badge-svg .score-badge-num {
|
|
81
|
-
font-
|
|
82
|
-
font-size: 56px;
|
|
249
|
+
.panel-score .score-badge-svg .score-badge-num {
|
|
250
|
+
font-size: 42px;
|
|
83
251
|
font-weight: 800;
|
|
84
252
|
letter-spacing: -0.02em;
|
|
85
253
|
}
|
|
86
254
|
|
|
87
|
-
.score-badge-svg .score-badge-of {
|
|
88
|
-
font-
|
|
89
|
-
font-size: 14px;
|
|
255
|
+
.panel-score .score-badge-svg .score-badge-of {
|
|
256
|
+
font-size: 12px;
|
|
90
257
|
font-weight: 700;
|
|
91
258
|
letter-spacing: 0.08em;
|
|
92
259
|
text-transform: uppercase;
|
|
93
260
|
opacity: 0.95;
|
|
94
261
|
}
|
|
95
262
|
|
|
96
|
-
.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.hero.tier-5 { --tier-bg: #0a6b0a; --tier-num: #fff; }
|
|
105
|
-
|
|
106
|
-
/* Summary cards */
|
|
107
|
-
.summary-cards {
|
|
108
|
-
display: grid;
|
|
109
|
-
grid-template-columns: repeat(4, 1fr);
|
|
110
|
-
gap: 0.75rem;
|
|
111
|
-
margin-bottom: 1.5rem;
|
|
263
|
+
.panel-score .score-desc {
|
|
264
|
+
font-size: 13px;
|
|
265
|
+
color: var(--text-muted);
|
|
266
|
+
margin: 0;
|
|
267
|
+
max-width: 320px;
|
|
268
|
+
margin-left: auto;
|
|
269
|
+
margin-right: auto;
|
|
270
|
+
line-height: 1.45;
|
|
112
271
|
}
|
|
113
272
|
|
|
114
|
-
|
|
115
|
-
|
|
273
|
+
/* LLM panel accent */
|
|
274
|
+
.panel-llm {
|
|
275
|
+
border-left: 3px solid var(--link);
|
|
116
276
|
}
|
|
117
277
|
|
|
118
|
-
|
|
278
|
+
/* ——— Banner ——— */
|
|
279
|
+
.no-llm-banner {
|
|
280
|
+
width: 100%;
|
|
119
281
|
background: var(--surface);
|
|
120
|
-
border: 1px solid var(--border);
|
|
121
|
-
padding: 0.
|
|
282
|
+
border-bottom: 1px solid var(--border);
|
|
283
|
+
padding: 0.5rem 1rem;
|
|
284
|
+
text-align: center;
|
|
122
285
|
}
|
|
123
286
|
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
background: var(--surface);
|
|
130
|
-
border: 1px solid var(--border);
|
|
131
|
-
padding: 1rem 1.25rem;
|
|
132
|
-
margin-bottom: 1rem;
|
|
287
|
+
.no-llm-banner .no-llm-cta {
|
|
288
|
+
font-size: 13px;
|
|
289
|
+
font-weight: normal;
|
|
290
|
+
color: var(--text-muted);
|
|
291
|
+
margin: 0;
|
|
133
292
|
}
|
|
134
293
|
|
|
135
|
-
|
|
136
|
-
.section-desc { font-size: 13px; color: var(--text-muted); margin: -0.35rem 0 0.75rem; line-height: 1.4; }
|
|
137
|
-
|
|
138
|
-
/* Treemap */
|
|
294
|
+
/* ——— Treemap ——— */
|
|
139
295
|
#treemap {
|
|
140
296
|
display: flex;
|
|
141
297
|
flex-wrap: wrap;
|
|
@@ -154,10 +310,14 @@ body {
|
|
|
154
310
|
overflow: hidden;
|
|
155
311
|
text-overflow: ellipsis;
|
|
156
312
|
white-space: nowrap;
|
|
157
|
-
border: 1px solid rgba(0,0,0,0.
|
|
313
|
+
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
314
|
+
border-radius: 2px;
|
|
158
315
|
}
|
|
159
316
|
|
|
160
|
-
.treemap-cell:hover {
|
|
317
|
+
.treemap-cell:hover {
|
|
318
|
+
outline: 2px solid var(--link);
|
|
319
|
+
outline-offset: 1px;
|
|
320
|
+
}
|
|
161
321
|
|
|
162
322
|
.treemap-cell[data-severity="critical"] { background: #c00; color: #fff; border-color: #900; }
|
|
163
323
|
.treemap-cell[data-severity="high"] { background: #e85d00; color: #fff; border-color: #b84a00; }
|
|
@@ -165,37 +325,77 @@ body {
|
|
|
165
325
|
.treemap-cell[data-severity="low"] { background: #0a6b0a; color: #fff; border-color: #064906; }
|
|
166
326
|
.treemap-cell[data-severity="none"] { background: var(--border); color: var(--text-muted); border-color: var(--border); }
|
|
167
327
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
328
|
+
.legend {
|
|
329
|
+
display: flex;
|
|
330
|
+
flex-wrap: wrap;
|
|
331
|
+
gap: 1rem;
|
|
332
|
+
align-items: center;
|
|
333
|
+
margin-bottom: 0.75rem;
|
|
334
|
+
font-size: 12px;
|
|
335
|
+
color: var(--text-muted);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.legend span { display: inline-flex; align-items: center; gap: 0.35rem; }
|
|
339
|
+
.legend .swatch { width: 10px; height: 10px; border-radius: 2px; }
|
|
172
340
|
.legend .swatch-crit { background: #c00; }
|
|
173
341
|
.legend .swatch-high { background: #e85d00; }
|
|
174
342
|
.legend .swatch-med { background: #b8860b; }
|
|
175
343
|
.legend .swatch-low { background: #0a6b0a; }
|
|
176
344
|
.legend .swatch-none { background: var(--border); }
|
|
177
345
|
|
|
178
|
-
/*
|
|
179
|
-
.
|
|
346
|
+
/* ——— Priority lists ——— */
|
|
347
|
+
.priority-list {
|
|
348
|
+
list-style: none;
|
|
349
|
+
padding: 0;
|
|
350
|
+
margin: 0;
|
|
351
|
+
font-size: 12px;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.priority-list li {
|
|
355
|
+
padding: 0.4rem 0;
|
|
356
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
357
|
+
color: var(--text);
|
|
358
|
+
font-family: ui-monospace, monospace;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.priority-list li:last-child { border-bottom: none; }
|
|
362
|
+
|
|
363
|
+
/* ——— Debt list ——— */
|
|
364
|
+
.debt-list {
|
|
365
|
+
list-style: none;
|
|
366
|
+
padding: 0;
|
|
367
|
+
margin: 0;
|
|
368
|
+
}
|
|
180
369
|
|
|
181
370
|
.debt-list li {
|
|
182
|
-
border-bottom: 1px solid var(--border);
|
|
183
|
-
padding: 0.
|
|
371
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
372
|
+
padding: 0.65rem 0;
|
|
184
373
|
cursor: pointer;
|
|
374
|
+
transition: background 0.12s ease;
|
|
185
375
|
}
|
|
186
376
|
|
|
187
377
|
.debt-list li:last-child { border-bottom: none; }
|
|
188
|
-
.debt-list li:hover { background: var(--
|
|
189
|
-
|
|
190
|
-
.debt-list .
|
|
378
|
+
.debt-list li:hover { background: var(--surface-hover); }
|
|
379
|
+
|
|
380
|
+
.debt-list .title { font-weight: 600; margin-bottom: 0.2rem; }
|
|
381
|
+
.debt-list .meta { font-size: 12px; color: var(--text-muted); display: block; margin-top: 0.25rem; font-family: ui-monospace, monospace; }
|
|
191
382
|
.debt-list .insight { font-size: 13px; color: var(--text-muted); margin-top: 0.25rem; line-height: 1.4; white-space: pre-wrap; word-break: break-word; }
|
|
192
383
|
.debt-list-ratings { display: flex; align-items: center; gap: 1rem; margin: 0.25rem 0; flex-wrap: wrap; }
|
|
193
384
|
.debt-list-rating { display: inline-flex; align-items: center; gap: 0.35rem; }
|
|
194
385
|
.debt-list-rating-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
|
|
195
|
-
.debt-list-llm-none { color: var(--text-muted); font-size:
|
|
386
|
+
.debt-list-llm-none { color: var(--text-muted); font-size: 12px; }
|
|
196
387
|
|
|
197
388
|
/* Badges */
|
|
198
|
-
.badge {
|
|
389
|
+
.badge {
|
|
390
|
+
display: inline-block;
|
|
391
|
+
padding: 0.15em 0.45em;
|
|
392
|
+
font-size: 10px;
|
|
393
|
+
font-weight: 600;
|
|
394
|
+
text-transform: uppercase;
|
|
395
|
+
letter-spacing: 0.03em;
|
|
396
|
+
border-radius: 3px;
|
|
397
|
+
}
|
|
398
|
+
|
|
199
399
|
.badge-critical { background: #c00; color: #fff; }
|
|
200
400
|
.badge-high { background: #e85d00; color: #fff; }
|
|
201
401
|
.badge-medium { background: #b8860b; color: #fff; }
|
|
@@ -203,11 +403,11 @@ body {
|
|
|
203
403
|
.badge-none { background: var(--border); color: var(--text-muted); }
|
|
204
404
|
.badge-llm { background: var(--link); color: #fff; }
|
|
205
405
|
|
|
206
|
-
/* Detail modal */
|
|
406
|
+
/* ——— Detail modal ——— */
|
|
207
407
|
#detail {
|
|
208
408
|
position: fixed;
|
|
209
409
|
inset: 0;
|
|
210
|
-
background: rgba(0,0,0,0.
|
|
410
|
+
background: rgba(0, 0, 0, 0.55);
|
|
211
411
|
display: none;
|
|
212
412
|
align-items: center;
|
|
213
413
|
justify-content: center;
|
|
@@ -220,7 +420,9 @@ body {
|
|
|
220
420
|
#detail .panel {
|
|
221
421
|
background: var(--surface);
|
|
222
422
|
border: 1px solid var(--border);
|
|
223
|
-
|
|
423
|
+
border-radius: var(--radius);
|
|
424
|
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
|
|
425
|
+
max-width: 540px;
|
|
224
426
|
width: 100%;
|
|
225
427
|
max-height: 90vh;
|
|
226
428
|
overflow-y: auto;
|
|
@@ -229,7 +431,7 @@ body {
|
|
|
229
431
|
-webkit-overflow-scrolling: touch;
|
|
230
432
|
}
|
|
231
433
|
|
|
232
|
-
#detail .panel h3 { margin: 0 0 0.35rem; font-size:
|
|
434
|
+
#detail .panel h3 { margin: 0 0 0.35rem; font-size: 14px; font-weight: 600; }
|
|
233
435
|
#detail .panel .file { font-family: ui-monospace, monospace; font-size: 13px; color: var(--link); }
|
|
234
436
|
#detail .panel .close-hint { margin-top: 0.75rem; font-size: 12px; color: var(--text-muted); }
|
|
235
437
|
|
|
@@ -250,6 +452,10 @@ body {
|
|
|
250
452
|
#detail .panel .detail-explanation .detail-llm-label { margin-top: 0.5rem; margin-bottom: 0.25rem; }
|
|
251
453
|
#detail .panel .detail-explanation .detail-no-llm { font-style: italic; color: var(--text-muted); }
|
|
252
454
|
#detail .panel .detail-explanation .detail-static-desc { margin-top: 0.5rem; font-size: 12px; color: var(--text-muted); }
|
|
455
|
+
#detail .panel .detail-explanation .detail-issues-list { margin: 0.25rem 0 0 0; padding-left: 1.25rem; list-style: none; }
|
|
456
|
+
#detail .panel .detail-explanation .detail-issues-list > li { margin-bottom: 0.35rem; }
|
|
457
|
+
#detail .panel .detail-explanation .detail-issues-list .detail-issue-desc { margin: 0.15rem 0 0 1rem; font-size: 11px; opacity: 0.9; }
|
|
458
|
+
#detail .panel .detail-explanation .detail-issues-list .detail-line { color: var(--text-muted); font-size: 11px; }
|
|
253
459
|
|
|
254
460
|
#detail .panel .file-assessment {
|
|
255
461
|
margin-top: 0.75rem;
|
|
@@ -265,14 +471,14 @@ body {
|
|
|
265
471
|
#detail .panel .file-assessment strong { color: var(--text); font-size: 12px; }
|
|
266
472
|
#detail .panel .suggested-code { margin-top: 0.75rem; }
|
|
267
473
|
|
|
268
|
-
/* Code blocks
|
|
474
|
+
/* ——— Code blocks ——— */
|
|
269
475
|
.code-block {
|
|
270
476
|
margin: 0.5rem 0;
|
|
271
477
|
padding: 0.75rem 1rem;
|
|
272
478
|
background: var(--bg);
|
|
273
479
|
border: 1px solid var(--border);
|
|
274
|
-
border-radius:
|
|
275
|
-
font-size:
|
|
480
|
+
border-radius: var(--radius);
|
|
481
|
+
font-size: 12px;
|
|
276
482
|
overflow-x: auto;
|
|
277
483
|
overflow-y: auto;
|
|
278
484
|
max-height: 20em;
|
|
@@ -280,43 +486,36 @@ body {
|
|
|
280
486
|
|
|
281
487
|
.code-block .lang-label {
|
|
282
488
|
display: block;
|
|
283
|
-
font-size:
|
|
489
|
+
font-size: 10px;
|
|
284
490
|
font-family: system-ui, sans-serif;
|
|
285
491
|
color: var(--text-muted);
|
|
286
492
|
margin-bottom: 0.5rem;
|
|
287
493
|
text-transform: uppercase;
|
|
288
|
-
letter-spacing: 0.
|
|
494
|
+
letter-spacing: 0.04em;
|
|
289
495
|
}
|
|
290
496
|
|
|
291
497
|
.code-block pre { margin: 0; white-space: pre; overflow-x: auto; min-width: min-content; }
|
|
292
498
|
.code-block code {
|
|
293
|
-
font-family: ui-monospace,
|
|
499
|
+
font-family: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;
|
|
294
500
|
font-size: 12px;
|
|
295
501
|
line-height: 1.5;
|
|
296
502
|
color: var(--text);
|
|
297
503
|
display: block;
|
|
298
504
|
}
|
|
299
505
|
|
|
300
|
-
#detail .panel .code-block {
|
|
301
|
-
max-height: 16em;
|
|
302
|
-
}
|
|
303
|
-
|
|
506
|
+
#detail .panel .code-block { max-height: 16em; }
|
|
304
507
|
#detail .panel .suggested-code .code-block { margin: 0.35rem 0 0; background: var(--surface); }
|
|
305
508
|
#detail .panel .file-assessment .code-block { margin: 0.5rem 0 0; background: var(--surface); }
|
|
306
509
|
|
|
307
|
-
/* LLM
|
|
308
|
-
.llm-
|
|
309
|
-
|
|
310
|
-
|
|
510
|
+
/* ——— LLM output ——— */
|
|
511
|
+
.llm-output .llm-prose {
|
|
512
|
+
margin: 0 0 0.75rem;
|
|
513
|
+
line-height: 1.55;
|
|
514
|
+
white-space: pre-wrap;
|
|
515
|
+
word-break: break-word;
|
|
516
|
+
color: var(--text);
|
|
517
|
+
}
|
|
311
518
|
|
|
312
|
-
.llm-output .llm-prose { margin: 0 0 0.75rem; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
|
313
519
|
.llm-output .llm-prose:last-child { margin-bottom: 0; }
|
|
314
520
|
|
|
315
|
-
.llm-
|
|
316
|
-
.llm-next-steps ul { margin: 0; padding-left: 1.25rem; color: var(--text); line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
|
317
|
-
|
|
318
|
-
/* Priority matrix */
|
|
319
|
-
.priority-matrix { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-top: 0.75rem; }
|
|
320
|
-
.priority-matrix .quadrant { padding: 0.75rem; border: 1px solid var(--border); }
|
|
321
|
-
.priority-matrix .quadrant h4 { margin: 0 0 0.35rem; font-size: 0.9rem; font-weight: bold; }
|
|
322
|
-
.priority-matrix .quadrant p { margin: 0; font-size: 13px; color: var(--text-muted); }
|
|
521
|
+
.panel-llm .panel-body .llm-output { margin: 0; }
|
|
@@ -166,58 +166,82 @@ document.getElementById("q2").innerHTML = highImpact
|
|
|
166
166
|
.map(function (d) { return '<li style="font-size:0.8rem">' + escapeHtml(d.file) + "</li>"; })
|
|
167
167
|
.join("");
|
|
168
168
|
|
|
169
|
-
// Debt list:
|
|
169
|
+
// Debt list: one row per file; static = worst severity across that file's issues, LLM = file's rating
|
|
170
170
|
var sev = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
171
171
|
var list = document.getElementById("debtList");
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
var
|
|
176
|
-
var
|
|
172
|
+
var filesWithDebt = Array.from(debtByFile.keys()).sort(function (fa, fb) {
|
|
173
|
+
var itemsA = debtByFile.get(fa);
|
|
174
|
+
var itemsB = debtByFile.get(fb);
|
|
175
|
+
var worstA = itemsA.length ? Math.max.apply(null, itemsA.map(function (d) { return sev[d.severity] || 0; })) : 0;
|
|
176
|
+
var worstB = itemsB.length ? Math.max.apply(null, itemsB.map(function (d) { return sev[d.severity] || 0; })) : 0;
|
|
177
|
+
if (worstB !== worstA) return worstB - worstA;
|
|
178
|
+
return fa.localeCompare(fb);
|
|
179
|
+
});
|
|
180
|
+
filesWithDebt.forEach(function (file) {
|
|
181
|
+
var items = debtByFile.get(file);
|
|
182
|
+
var worstSeverityVal = items.length ? items.reduce(function (best, d) {
|
|
183
|
+
return (sev[d.severity] || 0) > (sev[best.severity] || 0) ? d : best;
|
|
184
|
+
}, items[0]) : null;
|
|
185
|
+
var staticSeverity = worstSeverityVal ? worstSeverityVal.severity : "low";
|
|
186
|
+
var fileM = DATA.fileMetrics.find(function (m) { return m.file === file; });
|
|
177
187
|
var fileLlmSeverity = fileM && fileM.llmSeverity ? fileM.llmSeverity : null;
|
|
178
|
-
var staticBadge = '<span class="badge badge-' +
|
|
188
|
+
var staticBadge = '<span class="badge badge-' + staticSeverity + '" title="Static analysis (worst of ' + items.length + ' issue(s))">' + staticSeverity + "</span>";
|
|
179
189
|
var llmRating = fileLlmSeverity
|
|
180
190
|
? '<span class="badge badge-' + fileLlmSeverity + '" title="LLM rating for this file">' + fileLlmSeverity + "</span>"
|
|
181
191
|
: '<span class="debt-list-llm-none">—</span>';
|
|
192
|
+
var titleText = items.length === 1
|
|
193
|
+
? items[0].title
|
|
194
|
+
: worstSeverityVal.title + " (+" + (items.length - 1) + " more)";
|
|
182
195
|
var ratingsRow =
|
|
183
196
|
'<div class="debt-list-ratings">' +
|
|
184
197
|
'<span class="debt-list-rating"><span class="debt-list-rating-label">Static</span> ' + staticBadge + "</span>" +
|
|
185
198
|
'<span class="debt-list-rating"><span class="debt-list-rating-label">LLM</span> ' + llmRating + "</span>" +
|
|
186
199
|
"</div>";
|
|
200
|
+
var li = document.createElement("li");
|
|
187
201
|
li.innerHTML =
|
|
188
202
|
'<span class="title">' +
|
|
189
|
-
escapeHtml(
|
|
203
|
+
escapeHtml(titleText) +
|
|
190
204
|
"</span> " +
|
|
191
205
|
ratingsRow +
|
|
192
|
-
'<span class="meta">' +
|
|
193
|
-
|
|
194
|
-
(d.line ? ":" + d.line : "") +
|
|
195
|
-
"</span>";
|
|
196
|
-
li.addEventListener("click", function () { showDetail(d.file, [d]); });
|
|
206
|
+
'<span class="meta">' + escapeHtml(file) + "</span>";
|
|
207
|
+
li.addEventListener("click", function () { showDetail(file, items); });
|
|
197
208
|
list.appendChild(li);
|
|
198
209
|
});
|
|
199
210
|
|
|
200
211
|
function showDetail(file, items) {
|
|
201
212
|
var panel = document.getElementById("detail");
|
|
202
|
-
var item = items.length ? items[0] : null;
|
|
203
213
|
var fileMetric = DATA.fileMetrics.find(function (m) { return m.file === file; });
|
|
214
|
+
var worstItem = items.length ? items.reduce(function (best, d) {
|
|
215
|
+
return (sev[d.severity] || 0) > (sev[best.severity] || 0) ? d : best;
|
|
216
|
+
}, items[0]) : null;
|
|
217
|
+
var staticSeverity = worstItem ? worstItem.severity : "low";
|
|
204
218
|
|
|
205
|
-
document.getElementById("detailTitle").textContent =
|
|
219
|
+
document.getElementById("detailTitle").textContent = items.length === 1
|
|
220
|
+
? (items[0].title || "Debt item")
|
|
221
|
+
: items.length + " static issues";
|
|
206
222
|
document.getElementById("detailFile").textContent = file;
|
|
207
223
|
|
|
208
224
|
var explanationEl = document.getElementById("detailExplanation");
|
|
209
225
|
var parts = [];
|
|
210
|
-
var staticSev = item ? item.severity : "—";
|
|
211
|
-
var llmSev = fileMetric && fileMetric.llmSeverity ? fileMetric.llmSeverity : "—";
|
|
212
226
|
parts.push(
|
|
213
227
|
'<div class="detail-severities">' +
|
|
214
|
-
'<span class="detail-sev"><strong>Static</strong> <span class="badge badge-' +
|
|
228
|
+
'<span class="detail-sev"><strong>Static</strong> <span class="badge badge-' + staticSeverity + '">' + staticSeverity + "</span> (worst of " + items.length + ")</span> " +
|
|
215
229
|
'<span class="detail-sev"><strong>LLM</strong> ' +
|
|
216
230
|
(fileMetric && fileMetric.llmSeverity ? '<span class="badge badge-' + fileMetric.llmSeverity + '">' + fileMetric.llmSeverity + "</span>" : "<span class=\"debt-list-llm-none\">—</span>") +
|
|
217
231
|
"</span></div>"
|
|
218
232
|
);
|
|
219
|
-
|
|
220
|
-
|
|
233
|
+
parts.push('<div class="detail-static-desc"><strong>Static issues</strong><ul class="detail-issues-list">');
|
|
234
|
+
items.forEach(function (item) {
|
|
235
|
+
parts.push(
|
|
236
|
+
'<li><span class="badge badge-' + item.severity + '">' + item.severity + "</span> " +
|
|
237
|
+
escapeHtml(item.title || "Issue") +
|
|
238
|
+
(item.line ? " <span class=\"detail-line\">line " + item.line + "</span>" : "")
|
|
239
|
+
);
|
|
240
|
+
if (item.description)
|
|
241
|
+
parts.push('<div class="detail-issue-desc">' + escapeHtml(item.description).replace(/\n/g, "<br>") + "</div>");
|
|
242
|
+
parts.push("</li>");
|
|
243
|
+
});
|
|
244
|
+
parts.push("</ul></div>");
|
|
221
245
|
parts.push('<div class="detail-llm-label"><strong>LLM assessment</strong></div>');
|
|
222
246
|
if (fileMetric && (fileMetric.llmRawAssessment || fileMetric.llmAssessment)) {
|
|
223
247
|
if (fileMetric.llmRawAssessment)
|
package/dist/reports/html.js
CHANGED
|
@@ -78,17 +78,20 @@ function buildHtml(run, title, darkMode, css, script) {
|
|
|
78
78
|
};
|
|
79
79
|
const tierColor = tierColors[cleanliness.tier] ?? "#666";
|
|
80
80
|
const scoreBadgeSvg = buildScoreBadgeSvg(cleanliness.tier, tierColor);
|
|
81
|
-
const
|
|
81
|
+
const llmPanelHtml = run.llmOverallAssessment || run.llmOverallRaw
|
|
82
82
|
? `
|
|
83
|
-
<div class="
|
|
84
|
-
<
|
|
85
|
-
|
|
83
|
+
<div class="panel panel-llm">
|
|
84
|
+
<div class="panel-header">
|
|
85
|
+
<h2 class="panel-title">LLM overall assessment</h2>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="panel-body">
|
|
88
|
+
<div class="llm-output">${run.llmOverallAssessment
|
|
86
89
|
? renderLlmOutputToHtml(run.llmOverallAssessment)
|
|
87
90
|
: '<div class="llm-prose">' +
|
|
88
91
|
escapeHtml(stripTrailingSeverityAndScore(run.llmOverallRaw ?? "")).replace(/\n/g, "<br>") +
|
|
89
92
|
"</div>"}</div>
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
</div>
|
|
94
|
+
</div>`
|
|
92
95
|
: "";
|
|
93
96
|
return `<!DOCTYPE html>
|
|
94
97
|
<html lang="en" data-theme="${theme}">
|
|
@@ -104,54 +107,113 @@ function buildHtml(run, title, darkMode, css, script) {
|
|
|
104
107
|
<meta name="twitter:description" content="${escapeHtml(cleanliness.label)}: ${escapeHtml(cleanliness.description)}" />
|
|
105
108
|
<style>${css}</style>
|
|
106
109
|
</head>
|
|
107
|
-
<body>
|
|
110
|
+
<body class="dashboard-page">
|
|
108
111
|
${!hasLlm ? `<div class="no-llm-banner"><p class="no-llm-cta">Analysis run without LLM — for full results, run with LLM</p></div>` : ""}
|
|
109
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<
|
|
112
|
+
<header class="dashboard-header">
|
|
113
|
+
<div class="dashboard-header-left">
|
|
114
|
+
<h1 class="dashboard-title">${escapeHtml(title)}</h1>
|
|
115
|
+
<span class="dashboard-meta">${escapeHtml(run.repoPath)}</span>
|
|
116
|
+
</div>
|
|
117
|
+
<div class="dashboard-header-right">
|
|
118
|
+
<div class="dashboard-score tier-${cleanliness.tier}" aria-label="Score ${cleanliness.tier} of 5">
|
|
119
|
+
<span class="dashboard-score-value">${cleanliness.tier}</span>
|
|
120
|
+
<span class="dashboard-score-of">/ 5</span>
|
|
121
|
+
<span class="dashboard-score-label">${escapeHtml(cleanliness.label)}</span>
|
|
122
|
+
</div>
|
|
123
|
+
<span class="dashboard-date">${run.completedAt ?? run.startedAt}</span>
|
|
116
124
|
</div>
|
|
125
|
+
</header>
|
|
117
126
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
<div class="
|
|
121
|
-
|
|
122
|
-
|
|
127
|
+
<main class="dashboard-main">
|
|
128
|
+
<div class="dashboard-grid dashboard-grid-stats">
|
|
129
|
+
<div class="panel stat-panel">
|
|
130
|
+
<div class="panel-body">
|
|
131
|
+
<div class="stat-value">${run.fileMetrics.length}</div>
|
|
132
|
+
<div class="stat-label">Files analyzed</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="panel stat-panel">
|
|
136
|
+
<div class="panel-body">
|
|
137
|
+
<div class="stat-value">${run.debtItems.length}</div>
|
|
138
|
+
<div class="stat-label">Debt items</div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="panel stat-panel stat-panel-warn">
|
|
142
|
+
<div class="panel-body">
|
|
143
|
+
<div class="stat-value">${highCriticalCount}</div>
|
|
144
|
+
<div class="stat-label">High / Critical</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="panel stat-panel">
|
|
148
|
+
<div class="panel-body">
|
|
149
|
+
<div class="stat-value">${hotspotCount}</div>
|
|
150
|
+
<div class="stat-label">Hotspots</div>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
123
153
|
</div>
|
|
124
154
|
|
|
125
|
-
|
|
155
|
+
<div class="dashboard-grid">
|
|
156
|
+
<div class="panel panel-score tier-${cleanliness.tier}">
|
|
157
|
+
<div class="panel-header">
|
|
158
|
+
<h2 class="panel-title">Cleanliness score</h2>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="panel-body panel-body-center">
|
|
161
|
+
<div class="score-badge" aria-hidden="true">${scoreBadgeSvg}</div>
|
|
162
|
+
<p class="score-desc">${escapeHtml(cleanliness.description)}</p>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
${llmPanelHtml}
|
|
126
168
|
|
|
127
|
-
<div class="
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
169
|
+
<div class="panel">
|
|
170
|
+
<div class="panel-header">
|
|
171
|
+
<h2 class="panel-title">Files by debt</h2>
|
|
172
|
+
<p class="panel-desc">Size = complexity + churn. Color = LLM severity. Click for details.</p>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="panel-body">
|
|
175
|
+
<div class="legend">
|
|
176
|
+
<span><span class="swatch swatch-crit"></span> Critical</span>
|
|
177
|
+
<span><span class="swatch swatch-high"></span> High</span>
|
|
178
|
+
<span><span class="swatch swatch-med"></span> Medium</span>
|
|
179
|
+
<span><span class="swatch swatch-low"></span> Low</span>
|
|
180
|
+
<span><span class="swatch swatch-none"></span> No debt</span>
|
|
181
|
+
</div>
|
|
182
|
+
<div id="treemap"></div>
|
|
136
183
|
</div>
|
|
137
|
-
<div id="treemap"></div>
|
|
138
184
|
</div>
|
|
139
185
|
|
|
140
|
-
<div class="
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
186
|
+
<div class="dashboard-grid dashboard-grid-half">
|
|
187
|
+
<div class="panel">
|
|
188
|
+
<div class="panel-header">
|
|
189
|
+
<h2 class="panel-title">High impact, easier</h2>
|
|
190
|
+
<p class="panel-desc">High severity in smaller files.</p>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="panel-body">
|
|
193
|
+
<ul id="q1" class="priority-list"></ul>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div class="panel">
|
|
197
|
+
<div class="panel-header">
|
|
198
|
+
<h2 class="panel-title">High impact, harder</h2>
|
|
199
|
+
<p class="panel-desc">Critical or hotspot files.</p>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="panel-body">
|
|
202
|
+
<ul id="q2" class="priority-list"></ul>
|
|
203
|
+
</div>
|
|
146
204
|
</div>
|
|
147
205
|
</div>
|
|
148
206
|
|
|
149
|
-
<div class="
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
207
|
+
<div class="panel">
|
|
208
|
+
<div class="panel-header">
|
|
209
|
+
<h2 class="panel-title">All debt items</h2>
|
|
210
|
+
<p class="panel-desc">Static and LLM ratings. Click a row for details.</p>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="panel-body">
|
|
213
|
+
<ul class="debt-list" id="debtList"></ul>
|
|
214
|
+
</div>
|
|
153
215
|
</div>
|
|
154
|
-
</
|
|
216
|
+
</main>
|
|
155
217
|
|
|
156
218
|
<div id="detail">
|
|
157
219
|
<div class="panel">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tech-debt-visualizer",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Language-agnostic CLI that analyzes repos and generates interactive technical debt visualizations with AI-powered insights",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|