tech-debt-visualizer 0.2.2 → 0.2.4

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.
@@ -43,7 +43,7 @@ body.dashboard-page {
43
43
  padding-bottom: 2rem;
44
44
  }
45
45
 
46
- /* ——— Dashboard header (Grafana-style top bar) ——— */
46
+ /* ——— Dashboard header (Grafana top bar: title left, score right) ——— */
47
47
  .dashboard-header {
48
48
  display: flex;
49
49
  align-items: center;
@@ -100,25 +100,9 @@ body.dashboard-page {
100
100
  font-weight: 600;
101
101
  }
102
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
- }
103
+ .dashboard-score-value { font-size: 1.25rem; line-height: 1; }
104
+ .dashboard-score-of { font-size: 0.85rem; font-weight: 500; color: var(--text-muted); }
105
+ .dashboard-score-label { font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; margin-left: 0.35rem; opacity: 0.95; }
122
106
 
123
107
  .dashboard-score.tier-1 { background: rgba(204, 0, 0, 0.2); color: #f66; }
124
108
  .dashboard-score.tier-2 { background: rgba(232, 93, 0, 0.2); color: #f90; }
@@ -131,36 +115,56 @@ body.dashboard-page {
131
115
  color: var(--text-muted);
132
116
  }
133
117
 
134
- /* ——— Main content & grid ——— */
118
+ /* ——— Main content: 2×2 dashboard grid ——— */
135
119
  .dashboard-main {
136
- max-width: 1200px;
120
+ max-width: 1400px;
137
121
  margin: 0 auto;
138
- padding: 1.25rem 1.5rem;
122
+ padding: 1rem 1.5rem;
123
+ min-height: calc(100vh - 52px);
139
124
  }
140
125
 
141
- .dashboard-grid {
126
+ .dashboard-grid-2x2 {
142
127
  display: grid;
128
+ grid-template-columns: 1fr 1fr;
129
+ grid-template-rows: auto minmax(320px, 1fr);
143
130
  gap: 1rem;
144
- margin-bottom: 1rem;
131
+ align-items: start;
145
132
  }
146
133
 
147
- .dashboard-grid-stats {
148
- grid-template-columns: repeat(4, 1fr);
134
+ .dashboard-cell {
135
+ min-height: 0;
136
+ min-width: 0;
149
137
  }
150
138
 
151
- .dashboard-grid-half {
152
- grid-template-columns: repeat(2, 1fr);
139
+ .dashboard-cell .panel {
140
+ height: 100%;
141
+ display: flex;
142
+ flex-direction: column;
143
+ }
144
+
145
+ .dashboard-cell .panel-body {
146
+ flex: 1;
147
+ min-height: 0;
148
+ overflow: auto;
149
+ }
150
+
151
+ .dashboard-cell-score .panel-body-score { flex: 1 1 auto; overflow: visible; }
152
+
153
+ .dashboard-cell-heatmap .panel-body-heatmap,
154
+ .dashboard-cell-list .panel-body {
155
+ min-height: 280px;
153
156
  }
154
157
 
155
158
  @media (max-width: 900px) {
156
- .dashboard-grid-stats { grid-template-columns: repeat(2, 1fr); }
157
- .dashboard-grid-half { grid-template-columns: 1fr; }
159
+ .dashboard-grid-2x2 {
160
+ grid-template-columns: 1fr;
161
+ grid-template-rows: auto auto auto auto;
162
+ }
158
163
  }
159
164
 
160
165
  @media (max-width: 560px) {
161
166
  .dashboard-header { padding: 0.6rem 1rem; }
162
- .dashboard-main { padding: 1rem; }
163
- .dashboard-grid-stats { grid-template-columns: 1fr; }
167
+ .dashboard-main { padding: 0.75rem 1rem; }
164
168
  }
165
169
 
166
170
  /* ——— Panels (Grafana-style) ——— */
@@ -205,32 +209,9 @@ body.dashboard-page {
205
209
  text-align: center;
206
210
  }
207
211
 
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 {
212
+ /* ——— Score panel (top-left: shield + caption + stats) ——— */
213
+ .panel-score .panel-body-score {
214
+ text-align: center;
234
215
  padding: 1.25rem 1rem;
235
216
  }
236
217
 
@@ -242,32 +223,105 @@ body.dashboard-page {
242
223
 
243
224
  .panel-score .score-badge-svg {
244
225
  display: block;
245
- width: 120px;
226
+ width: 100px;
246
227
  height: auto;
228
+ margin: 0 auto;
247
229
  }
248
230
 
249
231
  .panel-score .score-badge-svg .score-badge-num {
250
- font-size: 42px;
232
+ font-size: 36px;
251
233
  font-weight: 800;
252
234
  letter-spacing: -0.02em;
253
235
  }
254
236
 
255
237
  .panel-score .score-badge-svg .score-badge-of {
256
- font-size: 12px;
238
+ font-size: 11px;
257
239
  font-weight: 700;
258
240
  letter-spacing: 0.08em;
259
241
  text-transform: uppercase;
260
242
  opacity: 0.95;
261
243
  }
262
244
 
245
+ .panel-score .score-label {
246
+ margin: 0 0 0.25rem;
247
+ font-size: 1rem;
248
+ font-weight: 600;
249
+ color: var(--text);
250
+ }
251
+
263
252
  .panel-score .score-desc {
253
+ margin: 0 0 0.5rem;
264
254
  font-size: 13px;
265
255
  color: var(--text-muted);
266
- margin: 0;
267
- max-width: 320px;
256
+ line-height: 1.45;
257
+ max-width: 280px;
268
258
  margin-left: auto;
269
259
  margin-right: auto;
270
- line-height: 1.45;
260
+ }
261
+
262
+ .panel-score .score-stats {
263
+ margin: 0;
264
+ font-size: 11px;
265
+ color: var(--text-muted);
266
+ text-transform: uppercase;
267
+ letter-spacing: 0.03em;
268
+ }
269
+
270
+ /* ——— Description of problems panel (top-right) ——— */
271
+ .panel-empty {
272
+ margin: 0;
273
+ font-size: 13px;
274
+ color: var(--text-muted);
275
+ font-style: italic;
276
+ }
277
+
278
+ .priority-inline {
279
+ display: grid;
280
+ grid-template-columns: 1fr 1fr;
281
+ gap: 1rem;
282
+ margin-top: 1rem;
283
+ padding-top: 1rem;
284
+ border-top: 1px solid var(--border-subtle);
285
+ }
286
+
287
+ .priority-inline h4 {
288
+ margin: 0 0 0.5rem;
289
+ font-size: 11px;
290
+ font-weight: 600;
291
+ text-transform: uppercase;
292
+ letter-spacing: 0.04em;
293
+ color: var(--text-muted);
294
+ }
295
+
296
+ .priority-inline .priority-list { margin: 0; }
297
+
298
+ @media (max-width: 600px) {
299
+ .priority-inline { grid-template-columns: 1fr; }
300
+ }
301
+
302
+ /* ——— Heatmap panel (bottom-left) ——— */
303
+ .panel-heatmap .panel-header-heatmap {
304
+ display: flex;
305
+ flex-wrap: wrap;
306
+ align-items: baseline;
307
+ gap: 0.75rem 1.25rem;
308
+ }
309
+
310
+ .panel-heatmap .panel-title { margin-right: 0.5rem; }
311
+ .panel-heatmap .panel-desc { margin: 0.15rem 0 0; flex-basis: 100%; }
312
+
313
+ .legend-inline {
314
+ margin: 0;
315
+ margin-left: auto;
316
+ }
317
+
318
+ .panel-body-heatmap {
319
+ padding: 1rem;
320
+ padding-top: 0.5rem;
321
+ }
322
+
323
+ .panel-body-heatmap #treemap {
324
+ border-radius: var(--radius);
271
325
  }
272
326
 
273
327
  /* LLM panel accent */
@@ -379,8 +433,18 @@ body.dashboard-page {
379
433
 
380
434
  .debt-list .title { font-weight: 600; margin-bottom: 0.2rem; }
381
435
  .debt-list .meta { font-size: 12px; color: var(--text-muted); display: block; margin-top: 0.25rem; font-family: ui-monospace, monospace; }
436
+ .debt-list-explanation {
437
+ font-size: 12px;
438
+ color: var(--text-muted);
439
+ margin: 0.35rem 0 0;
440
+ line-height: 1.4;
441
+ display: -webkit-box;
442
+ -webkit-line-clamp: 2;
443
+ -webkit-box-orient: vertical;
444
+ overflow: hidden;
445
+ }
382
446
  .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; }
383
- .debt-list-ratings { display: flex; align-items: center; gap: 1rem; margin: 0.25rem 0; flex-wrap: wrap; }
447
+ .debt-list-ratings { display: flex; align-items: center; gap: 1rem; margin: 0.25rem 0 0; flex-wrap: wrap; }
384
448
  .debt-list-rating { display: inline-flex; align-items: center; gap: 0.35rem; }
385
449
  .debt-list-rating-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: var(--text-muted); }
386
450
  .debt-list-llm-none { color: var(--text-muted); font-size: 12px; }
@@ -166,44 +166,81 @@ 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: one row per file; static = worst severity across that file's issues, LLM = file's rating
170
- var sev = { critical: 4, high: 3, medium: 2, low: 1 };
169
+ // Debt list: every file rated above none by static OR LLM; show both ratings and short explanation
170
+ var sev = { critical: 4, high: 3, medium: 2, low: 1, none: 0 };
171
171
  var list = document.getElementById("debtList");
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;
172
+
173
+ function severityNum(s) { return sev[s] || 0; }
174
+ function fileWorstStatic(items) {
175
+ if (!items || !items.length) return 0;
176
+ return Math.max.apply(null, items.map(function (d) { return severityNum(d.severity); }));
177
+ }
178
+ function fileWorstLlm(metric) {
179
+ if (!metric || !metric.llmSeverity || metric.llmSeverity === "none") return 0;
180
+ return severityNum(metric.llmSeverity);
181
+ }
182
+
183
+ var filesFromStatic = new Set(debtByFile.keys());
184
+ var filesFromLlm = new Set();
185
+ DATA.fileMetrics.forEach(function (m) {
186
+ if (m.llmSeverity && m.llmSeverity !== "none") filesFromLlm.add(m.file);
187
+ });
188
+ var fileSet = new Set([].concat(Array.from(filesFromStatic), Array.from(filesFromLlm)));
189
+
190
+ var filesWithDebt = Array.from(fileSet).sort(function (fa, fb) {
191
+ var itemsA = debtByFile.get(fa) || [];
192
+ var itemsB = debtByFile.get(fb) || [];
193
+ var metricA = DATA.fileMetrics.find(function (m) { return m.file === fa; });
194
+ var metricB = DATA.fileMetrics.find(function (m) { return m.file === fb; });
195
+ var worstA = Math.max(fileWorstStatic(itemsA), fileWorstLlm(metricA));
196
+ var worstB = Math.max(fileWorstStatic(itemsB), fileWorstLlm(metricB));
177
197
  if (worstB !== worstA) return worstB - worstA;
178
198
  return fa.localeCompare(fb);
179
199
  });
200
+
201
+ function firstLine(text) {
202
+ if (!text || !String(text).trim()) return "";
203
+ return String(text).trim().split(/\n/)[0].trim().slice(0, 120);
204
+ }
205
+
180
206
  filesWithDebt.forEach(function (file) {
181
- var items = debtByFile.get(file);
207
+ var items = debtByFile.get(file) || [];
208
+ var fileM = DATA.fileMetrics.find(function (m) { return m.file === file; });
182
209
  var worstSeverityVal = items.length ? items.reduce(function (best, d) {
183
- return (sev[d.severity] || 0) > (sev[best.severity] || 0) ? d : best;
210
+ return severityNum(d.severity) > severityNum(best.severity) ? d : best;
184
211
  }, items[0]) : null;
185
- var staticSeverity = worstSeverityVal ? worstSeverityVal.severity : "low";
186
- var fileM = DATA.fileMetrics.find(function (m) { return m.file === file; });
187
- var fileLlmSeverity = fileM && fileM.llmSeverity ? fileM.llmSeverity : null;
188
- var staticBadge = '<span class="badge badge-' + staticSeverity + '" title="Static analysis (worst of ' + items.length + ' issue(s))">' + staticSeverity + "</span>";
189
- var llmRating = fileLlmSeverity
190
- ? '<span class="badge badge-' + fileLlmSeverity + '" title="LLM rating for this file">' + fileLlmSeverity + "</span>"
212
+ var staticSeverity = worstSeverityVal ? worstSeverityVal.severity : null;
213
+ var fileLlmSeverity = fileM && fileM.llmSeverity && fileM.llmSeverity !== "none" ? fileM.llmSeverity : null;
214
+
215
+ var staticBadge = staticSeverity
216
+ ? '<span class="badge badge-' + staticSeverity + '" title="Static (worst of ' + items.length + ')">' + staticSeverity + "</span>"
191
217
  : '<span class="debt-list-llm-none">—</span>';
192
- var titleText = items.length === 1
193
- ? items[0].title
194
- : worstSeverityVal.title + " (+" + (items.length - 1) + " more)";
218
+ var llmBadge = fileLlmSeverity
219
+ ? '<span class="badge badge-' + fileLlmSeverity + '" title="LLM">' + fileLlmSeverity + "</span>"
220
+ : '<span class="debt-list-llm-none">—</span>';
221
+
222
+ var explanation = "";
223
+ if (items.length && worstSeverityVal) {
224
+ explanation = escapeHtml(firstLine(worstSeverityVal.title || worstSeverityVal.description || ""));
225
+ if (items.length > 1) explanation += " (+" + (items.length - 1) + " more)";
226
+ }
227
+ if (fileM && (fileM.llmAssessment || fileM.llmRawAssessment)) {
228
+ var llmBlurb = firstLine(stripTrailingSeverityAndScore(fileM.llmRawAssessment || fileM.llmAssessment || ""));
229
+ if (llmBlurb) explanation = explanation ? explanation + " · " + escapeHtml(llmBlurb) : escapeHtml(llmBlurb);
230
+ }
231
+ if (!explanation) explanation = "Rated by static or LLM.";
232
+
195
233
  var ratingsRow =
196
234
  '<div class="debt-list-ratings">' +
197
235
  '<span class="debt-list-rating"><span class="debt-list-rating-label">Static</span> ' + staticBadge + "</span>" +
198
- '<span class="debt-list-rating"><span class="debt-list-rating-label">LLM</span> ' + llmRating + "</span>" +
236
+ '<span class="debt-list-rating"><span class="debt-list-rating-label">LLM</span> ' + llmBadge + "</span>" +
199
237
  "</div>";
200
238
  var li = document.createElement("li");
201
239
  li.innerHTML =
202
- '<span class="title">' +
203
- escapeHtml(titleText) +
204
- "</span> " +
240
+ '<span class="title">' + escapeHtml(file.split("/").pop() || file) + "</span> " +
205
241
  ratingsRow +
206
- '<span class="meta">' + escapeHtml(file) + "</span>";
242
+ '<span class="meta">' + escapeHtml(file) + "</span>" +
243
+ '<p class="debt-list-explanation">' + explanation + "</p>";
207
244
  li.addEventListener("click", function () { showDetail(file, items); });
208
245
  list.appendChild(li);
209
246
  });
@@ -212,35 +249,44 @@ function showDetail(file, items) {
212
249
  var panel = document.getElementById("detail");
213
250
  var fileMetric = DATA.fileMetrics.find(function (m) { return m.file === file; });
214
251
  var worstItem = items.length ? items.reduce(function (best, d) {
215
- return (sev[d.severity] || 0) > (sev[best.severity] || 0) ? d : best;
252
+ return severityNum(d.severity) > severityNum(best.severity) ? d : best;
216
253
  }, items[0]) : null;
217
- var staticSeverity = worstItem ? worstItem.severity : "low";
254
+ var staticSeverity = worstItem ? worstItem.severity : null;
218
255
 
219
- document.getElementById("detailTitle").textContent = items.length === 1
256
+ var titleText = items.length === 1
220
257
  ? (items[0].title || "Debt item")
221
- : items.length + " static issues";
258
+ : items.length > 1
259
+ ? items.length + " static issues"
260
+ : (fileMetric && fileMetric.llmSeverity ? "LLM assessment" : "File details");
261
+ document.getElementById("detailTitle").textContent = titleText;
222
262
  document.getElementById("detailFile").textContent = file;
223
263
 
224
264
  var explanationEl = document.getElementById("detailExplanation");
225
265
  var parts = [];
226
266
  parts.push(
227
267
  '<div class="detail-severities">' +
228
- '<span class="detail-sev"><strong>Static</strong> <span class="badge badge-' + staticSeverity + '">' + staticSeverity + "</span> (worst of " + items.length + ")</span> " +
268
+ '<span class="detail-sev"><strong>Static</strong> ' +
269
+ (staticSeverity ? '<span class="badge badge-' + staticSeverity + '">' + staticSeverity + "</span> (worst of " + items.length + ")" : "<span class=\"debt-list-llm-none\">\u2014</span>") +
270
+ "</span> " +
229
271
  '<span class="detail-sev"><strong>LLM</strong> ' +
230
- (fileMetric && fileMetric.llmSeverity ? '<span class="badge badge-' + fileMetric.llmSeverity + '">' + fileMetric.llmSeverity + "</span>" : "<span class=\"debt-list-llm-none\">—</span>") +
272
+ (fileMetric && fileMetric.llmSeverity && fileMetric.llmSeverity !== "none" ? '<span class="badge badge-' + fileMetric.llmSeverity + '">' + fileMetric.llmSeverity + "</span>" : "<span class=\"debt-list-llm-none\">\u2014</span>") +
231
273
  "</span></div>"
232
274
  );
233
275
  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
- });
276
+ if (items.length) {
277
+ items.forEach(function (item) {
278
+ parts.push(
279
+ '<li><span class="badge badge-' + item.severity + '">' + item.severity + "</span> " +
280
+ escapeHtml(item.title || "Issue") +
281
+ (item.line ? " <span class=\"detail-line\">line " + item.line + "</span>" : "")
282
+ );
283
+ if (item.description)
284
+ parts.push('<div class="detail-issue-desc">' + escapeHtml(item.description).replace(/\n/g, "<br>") + "</div>");
285
+ parts.push("</li>");
286
+ });
287
+ } else {
288
+ parts.push("<li class=\"detail-no-llm\">No static issues for this file.</li>");
289
+ }
244
290
  parts.push("</ul></div>");
245
291
  parts.push('<div class="detail-llm-label"><strong>LLM assessment</strong></div>');
246
292
  if (fileMetric && (fileMetric.llmRawAssessment || fileMetric.llmAssessment)) {
@@ -78,21 +78,7 @@ 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 llmPanelHtml = run.llmOverallAssessment || run.llmOverallRaw
82
- ? `
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
89
- ? renderLlmOutputToHtml(run.llmOverallAssessment)
90
- : '<div class="llm-prose">' +
91
- escapeHtml(stripTrailingSeverityAndScore(run.llmOverallRaw ?? "")).replace(/\n/g, "<br>") +
92
- "</div>"}</div>
93
- </div>
94
- </div>`
95
- : "";
81
+ const statsLine = `${run.fileMetrics.length} files · ${run.debtItems.length} items · ${highCriticalCount} high/crit · ${hotspotCount} hotspots`;
96
82
  return `<!DOCTYPE html>
97
83
  <html lang="en" data-theme="${theme}">
98
84
  <head>
@@ -125,92 +111,77 @@ function buildHtml(run, title, darkMode, css, script) {
125
111
  </header>
126
112
 
127
113
  <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>
114
+ <div class="dashboard-grid-2x2">
115
+ <div class="dashboard-cell dashboard-cell-score">
116
+ <div class="panel panel-score tier-${cleanliness.tier}">
117
+ <div class="panel-header">
118
+ <h2 class="panel-title">Technical Debt Cleanliness Score</h2>
119
+ </div>
120
+ <div class="panel-body panel-body-score">
121
+ <div class="score-badge" aria-hidden="true">${scoreBadgeSvg}</div>
122
+ <p class="score-label">${escapeHtml(cleanliness.label)}</p>
123
+ <p class="score-desc">${escapeHtml(cleanliness.description)}</p>
124
+ <p class="score-stats">${statsLine}</p>
125
+ </div>
139
126
  </div>
140
127
  </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>
153
- </div>
154
128
 
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>
129
+ <div class="dashboard-cell dashboard-cell-problems">
130
+ <div class="panel panel-llm">
131
+ <div class="panel-header">
132
+ <h2 class="panel-title">Description of problems</h2>
133
+ </div>
134
+ <div class="panel-body">
135
+ ${run.llmOverallAssessment || run.llmOverallRaw
136
+ ? `<div class="llm-output">${run.llmOverallAssessment
137
+ ? renderLlmOutputToHtml(run.llmOverallAssessment)
138
+ : '<div class="llm-prose">' +
139
+ escapeHtml(stripTrailingSeverityAndScore(run.llmOverallRaw ?? "")).replace(/\n/g, "<br>") +
140
+ "</div>"}</div>`
141
+ : '<p class="panel-empty">Run with LLM for an overall assessment of problems.</p>'}
142
+ <div class="priority-inline">
143
+ <div class="priority-inline-col">
144
+ <h4>High impact, easier</h4>
145
+ <ul id="q1" class="priority-list"></ul>
146
+ </div>
147
+ <div class="priority-inline-col">
148
+ <h4>High impact, harder</h4>
149
+ <ul id="q2" class="priority-list"></ul>
150
+ </div>
151
+ </div>
152
+ </div>
163
153
  </div>
164
154
  </div>
165
- </div>
166
155
 
167
- ${llmPanelHtml}
168
-
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>
156
+ <div class="dashboard-cell dashboard-cell-heatmap">
157
+ <div class="panel panel-heatmap">
158
+ <div class="panel-header panel-header-heatmap">
159
+ <h2 class="panel-title">Files by debt</h2>
160
+ <p class="panel-desc">Size = complexity + churn. Color = severity. Click for details.</p>
161
+ <div class="legend legend-inline">
162
+ <span><span class="swatch swatch-crit"></span> Critical</span>
163
+ <span><span class="swatch swatch-high"></span> High</span>
164
+ <span><span class="swatch swatch-med"></span> Medium</span>
165
+ <span><span class="swatch swatch-low"></span> Low</span>
166
+ <span><span class="swatch swatch-none"></span> None</span>
167
+ </div>
168
+ </div>
169
+ <div class="panel-body panel-body-heatmap">
170
+ <div id="treemap"></div>
171
+ </div>
181
172
  </div>
182
- <div id="treemap"></div>
183
173
  </div>
184
- </div>
185
174
 
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>
175
+ <div class="dashboard-cell dashboard-cell-list">
176
+ <div class="panel">
177
+ <div class="panel-header">
178
+ <h2 class="panel-title">Ratings & files</h2>
179
+ <p class="panel-desc">Files rated above none by static or LLM. Click for full details.</p>
180
+ </div>
181
+ <div class="panel-body">
182
+ <ul class="debt-list" id="debtList"></ul>
183
+ </div>
191
184
  </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>
204
- </div>
205
- </div>
206
-
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
185
  </div>
215
186
  </div>
216
187
  </main>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tech-debt-visualizer",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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",