fallow-code-scan 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js ADDED
@@ -0,0 +1,395 @@
1
+ {
2
+ const AUTO_REFRESH_MS = 120000;
3
+ const POLL_WHILE_RUNNING_MS = 1500;
4
+ const elements = {
5
+ autoRefresh: document.getElementById("auto-refresh"),
6
+ blockingCount: document.getElementById("blocking-count"),
7
+ checkSections: document.getElementById("check-sections"),
8
+ copyBlocking: document.getElementById("copy-blocking"),
9
+ copyReport: document.getElementById("copy-report"),
10
+ copyRefactoring: document.getElementById("copy-refactoring"),
11
+ errorBanner: document.getElementById("error-banner"),
12
+ fileScores: document.getElementById("file-scores"),
13
+ healthSummary: document.getElementById("health-summary"),
14
+ overviewGrid: document.getElementById("overview-grid"),
15
+ refreshButton: document.getElementById("refresh-button"),
16
+ runSummary: document.getElementById("run-summary"),
17
+ statusPill: document.getElementById("status-pill"),
18
+ targetCount: document.getElementById("target-count"),
19
+ targetsList: document.getElementById("targets-list")
20
+ };
21
+ const format = window.codeScanFormat;
22
+ const copy = window.codeScanCopy;
23
+ const findings = window.codeScanFindings;
24
+ const icons = window.codeScanIcons;
25
+ let autoRefreshId = null;
26
+ let latestReport = null;
27
+ let runningPollId = null;
28
+
29
+ elements.refreshButton.addEventListener("click", () => {
30
+ requestScan().catch(showError);
31
+ });
32
+
33
+ elements.autoRefresh.addEventListener("change", () => {
34
+ configureAutoRefresh();
35
+ });
36
+
37
+ elements.copyReport.addEventListener("click", () => {
38
+ copyReport().catch(showError);
39
+ });
40
+
41
+ elements.copyBlocking.addEventListener("click", () => {
42
+ copySection(elements.copyBlocking, format.formatBlockingFindingsText).catch(showError);
43
+ });
44
+
45
+ elements.copyRefactoring.addEventListener("click", () => {
46
+ copySection(elements.copyRefactoring, format.formatRefactoringSuggestionsText).catch(showError);
47
+ });
48
+
49
+ icons.refresh();
50
+ loadReport().then((payload) => {
51
+ if (payload.state === "idle") return requestScan();
52
+ return payload;
53
+ }).catch(showError);
54
+
55
+ async function requestScan() {
56
+ setButtonBusy(true);
57
+ const response = await fetch("/api/scan", { method: "POST" });
58
+ const payload = await readJsonResponse(response);
59
+ renderPayload(payload);
60
+ scheduleRunningPoll(payload);
61
+ return payload;
62
+ }
63
+
64
+ async function loadReport() {
65
+ const response = await fetch("/api/report");
66
+ const payload = await readJsonResponse(response);
67
+ renderPayload(payload);
68
+ scheduleRunningPoll(payload);
69
+ return payload;
70
+ }
71
+
72
+ async function readJsonResponse(response) {
73
+ const payload = await response.json();
74
+ if (!response.ok) throw new Error(payload.error || "Code scan request failed");
75
+ return payload;
76
+ }
77
+
78
+ function configureAutoRefresh() {
79
+ if (autoRefreshId) window.clearInterval(autoRefreshId);
80
+ autoRefreshId = null;
81
+
82
+ if (elements.autoRefresh.checked) {
83
+ autoRefreshId = window.setInterval(() => {
84
+ requestScan().catch(showError);
85
+ }, AUTO_REFRESH_MS);
86
+ }
87
+ }
88
+
89
+ function scheduleRunningPoll(payload) {
90
+ if (runningPollId) window.clearTimeout(runningPollId);
91
+ runningPollId = null;
92
+
93
+ if (payload.running) {
94
+ runningPollId = window.setTimeout(() => {
95
+ loadReport().catch(showError);
96
+ }, POLL_WHILE_RUNNING_MS);
97
+ } else {
98
+ setButtonBusy(false);
99
+ }
100
+ }
101
+
102
+ function renderPayload(payload) {
103
+ renderStatus(payload);
104
+ if (!payload.report) return renderEmptyReport();
105
+
106
+ latestReport = payload.report;
107
+ setCopyButtonsDisabled(false);
108
+ renderOverview(payload.report.overview);
109
+ renderHardFindings(payload.report.hardFindings);
110
+ renderHealth(payload.report.health);
111
+ renderTargets(payload.report.health.targets);
112
+ icons.refresh();
113
+ }
114
+
115
+ function renderStatus(payload) {
116
+ elements.statusPill.className = `status-pill status-${payload.state}`;
117
+ elements.statusPill.textContent = format.statusText(payload);
118
+ elements.runSummary.innerHTML = format.runSummaryHtml(payload);
119
+ icons.refresh(elements.runSummary);
120
+ renderError(payload.error);
121
+ }
122
+
123
+ function renderError(message) {
124
+ elements.errorBanner.hidden = !message;
125
+ elements.errorBanner.textContent = message || "";
126
+ }
127
+
128
+ function renderEmptyReport() {
129
+ latestReport = null;
130
+ setCopyButtonsDisabled(true);
131
+ elements.overviewGrid.replaceChildren();
132
+ elements.checkSections.replaceChildren(emptyState("No scan results yet."));
133
+ elements.healthSummary.replaceChildren();
134
+ elements.fileScores.replaceChildren(emptyState("No health results yet."));
135
+ elements.targetsList.replaceChildren(emptyState("No refactoring suggestions yet."));
136
+ }
137
+
138
+ function renderOverview(items) {
139
+ elements.overviewGrid.replaceChildren(...items.map((item) => {
140
+ const node = document.createElement("article");
141
+ node.className = `metric metric-${item.id} metric-${item.tone}`;
142
+ node.innerHTML = `
143
+ <div class="metric-header">
144
+ <div class="metric-label">${escapeHtml(item.label)}</div>
145
+ ${icons.metricIconHtml(item.id)}
146
+ </div>
147
+ ${metricValueHtml(item)}
148
+ `;
149
+ return node;
150
+ }));
151
+ }
152
+
153
+ function metricValueHtml(item) {
154
+ const value = `<div class="metric-value">${escapeHtml(`${item.value}${item.suffix || ""}`)}</div>`;
155
+ if (item.id !== "maintainability") return value;
156
+ return `<div class="metric-score-row">${value}${metricProgressHtml(item)}</div>`;
157
+ }
158
+
159
+ function metricProgressHtml(item) {
160
+ if (item.id !== "maintainability") return "";
161
+ const value = Math.max(0, Math.min(100, Number(item.value) || 0));
162
+ return `
163
+ <progress
164
+ class="metric-progress"
165
+ aria-label="Maintainability score"
166
+ max="100"
167
+ value="${escapeHtml(value)}"
168
+ ></progress>
169
+ `;
170
+ }
171
+
172
+ function renderHardFindings(hardFindings) {
173
+ elements.blockingCount.textContent = String(hardFindings.count);
174
+ if (latestReport) {
175
+ elements.copyBlocking.dataset.copyText = format.formatBlockingFindingsText(latestReport);
176
+ }
177
+
178
+ if (hardFindings.sections.length === 0) {
179
+ elements.checkSections.replaceChildren(emptyState("No blocking findings."));
180
+ return;
181
+ }
182
+
183
+ const nodes = hardFindings.sections.map((section) => findings.renderSection(section, escapeHtml));
184
+ elements.checkSections.replaceChildren(...nodes);
185
+ }
186
+
187
+ function renderHealth(health) {
188
+ const findingsByPath = countFindingsByPath(health.findings);
189
+ elements.healthSummary.replaceChildren(
190
+ healthSection("Scan Scope", [
191
+ ["Analyzed files", format.formatNumber(health.summary.filesAnalyzed)],
192
+ ["Analyzed functions", format.formatNumber(health.summary.functionsAnalyzed)],
193
+ ["Source lines", format.formatNumber(health.vitalSigns.totalLoc)]
194
+ ]),
195
+ riskSignalsSection([
196
+ riskSignalRow(
197
+ "Functions over complexity limit",
198
+ format.formatNumber(health.summary.functionsAboveThreshold),
199
+ format.thresholdTone(health.summary.functionsAboveThreshold, 0, 10)
200
+ ),
201
+ riskSignalRow(
202
+ "90th percentile function complexity",
203
+ format.formatNumber(health.vitalSigns.p90Cyclomatic),
204
+ format.thresholdTone(health.vitalSigns.p90Cyclomatic, 5, 10)
205
+ ),
206
+ riskSignalRow(
207
+ "Files with high coupling",
208
+ `${health.vitalSigns.couplingHighPercent}% of scored files`,
209
+ format.thresholdTone(health.vitalSigns.couplingHighPercent, 0, 5)
210
+ )
211
+ ])
212
+ );
213
+
214
+ if (health.fileScores.length === 0) {
215
+ elements.fileScores.replaceChildren(emptyState("No file scores."));
216
+ return;
217
+ }
218
+
219
+ elements.fileScores.replaceChildren(...health.fileScores.map((score) => {
220
+ return renderFileScore(score, findingsByPath.get(score.path) || 0);
221
+ }));
222
+ }
223
+
224
+ function healthSection(title, rows) {
225
+ const node = document.createElement("section");
226
+ node.className = "health-section";
227
+ node.innerHTML = `
228
+ <div class="health-section-header">
229
+ <h3>${escapeHtml(title)}</h3>
230
+ </div>
231
+ <div class="health-rows">
232
+ ${rows.map(([label, value]) => healthRowHtml(label, value)).join("")}
233
+ </div>
234
+ `;
235
+ return node;
236
+ }
237
+
238
+ function riskSignalsSection(rows) {
239
+ const node = healthSection("Risk Signals", rows.map((row) => [row.label, row.value]));
240
+ node.querySelector(".health-rows").innerHTML = rows.map(riskSignalRowHtml).join("");
241
+ node.querySelector(".health-section-header").appendChild(createRiskCopyButton());
242
+ return node;
243
+ }
244
+
245
+ function createRiskCopyButton() {
246
+ const button = document.createElement("button");
247
+ button.id = "copy-risk";
248
+ button.className = "copy-section icon-button";
249
+ button.type = "button";
250
+ button.innerHTML = '<i data-lucide="copy" aria-hidden="true"></i><span class="button-label">Copy</span>';
251
+ button.dataset.copyText = latestReport ? format.formatRiskSignalsText(latestReport) : "";
252
+ button.addEventListener("click", () => {
253
+ copySection(button, format.formatRiskSignalsText).catch(showError);
254
+ });
255
+ return button;
256
+ }
257
+
258
+ function healthRowHtml(label, value) {
259
+ return healthRowMarkup(label, value, "neutral");
260
+ }
261
+
262
+ function riskSignalRow(label, value, tone) {
263
+ return {
264
+ label,
265
+ tone,
266
+ value
267
+ };
268
+ }
269
+
270
+ function riskSignalRowHtml(row) {
271
+ return healthRowMarkup(row.label, row.value, row.tone);
272
+ }
273
+
274
+ function healthRowMarkup(label, value, tone) {
275
+ return `
276
+ <div class="health-row health-row-${escapeHtml(tone)}">
277
+ <span>${escapeHtml(label)}</span>
278
+ <span>${escapeHtml(value)}</span>
279
+ </div>
280
+ `;
281
+ }
282
+
283
+ function countFindingsByPath(findings) {
284
+ return findings.reduce((counts, finding) => {
285
+ counts.set(finding.path, (counts.get(finding.path) || 0) + 1);
286
+ return counts;
287
+ }, new Map());
288
+ }
289
+
290
+ function renderFileScore(score, complexityCount) {
291
+ const node = document.createElement("article");
292
+ node.className = `file-score ${complexityCount > 0 ? "file-score-warn" : ""}`;
293
+ node.innerHTML = `
294
+ <div>
295
+ <span class="item-title">${escapeHtml(score.path)}</span>
296
+ <span class="item-detail">
297
+ ${escapeHtml(format.formatNumber(score.lines))} lines of code
298
+ </span>
299
+ </div>
300
+ <div class="file-score-side">
301
+ ${complexityBadgeHtml(complexityCount)}
302
+ <div class="score-value">${escapeHtml(score.maintainability.toFixed(1))}</div>
303
+ </div>
304
+ `;
305
+ return node;
306
+ }
307
+
308
+ function complexityBadgeHtml(count) {
309
+ if (count === 0) return "";
310
+ const label = count === 1 ? "complexity finding" : "complexity findings";
311
+ return `<div class="complexity-badge">${escapeHtml(count)} ${label}</div>`;
312
+ }
313
+
314
+ function renderTargets(targets) {
315
+ elements.targetCount.textContent = format.formatNumber(targets.length);
316
+ if (latestReport) {
317
+ elements.copyRefactoring.dataset.copyText = format.formatRefactoringSuggestionsText(latestReport);
318
+ }
319
+
320
+ if (targets.length === 0) {
321
+ elements.targetsList.replaceChildren(emptyState("No refactoring suggestions."));
322
+ return;
323
+ }
324
+
325
+ elements.targetsList.replaceChildren(...targets.map(renderTarget));
326
+ }
327
+
328
+ function renderTarget(target) {
329
+ const node = document.createElement("article");
330
+ node.className = `target-card target-${format.effortTone(target.effort)}`;
331
+ node.innerHTML = `
332
+ <h3>${escapeHtml(target.path)}</h3>
333
+ <p>${escapeHtml(format.formatRecommendation(target.recommendation))}</p>
334
+ <div class="target-score">Priority ${escapeHtml(target.priority.toFixed(1))}</div>
335
+ `;
336
+ return node;
337
+ }
338
+
339
+ function emptyState(message) {
340
+ const node = document.createElement("div");
341
+ node.className = "empty-state";
342
+ node.textContent = message;
343
+ return node;
344
+ }
345
+
346
+ function setButtonBusy(isBusy) {
347
+ elements.refreshButton.disabled = isBusy;
348
+ icons.setButtonLabel(elements.refreshButton, isBusy ? "Scanning" : "Refresh");
349
+ elements.refreshButton.classList.toggle("is-spinning", isBusy);
350
+ }
351
+
352
+ async function copyReport() {
353
+ if (!latestReport) return;
354
+ const reportText = format.formatReportText(latestReport);
355
+ await copyText(elements.copyReport, reportText);
356
+ }
357
+
358
+ async function copySection(button, formatter) {
359
+ if (!latestReport) return;
360
+ await copyText(button, button.dataset.copyText || formatter(latestReport));
361
+ }
362
+
363
+ async function copyText(button, text) {
364
+ await copy.writeText(text);
365
+ const previousLabel = button.querySelector(".button-label").textContent;
366
+ icons.setButtonLabel(button, "Copied");
367
+ window.setTimeout(() => {
368
+ icons.setButtonLabel(button, previousLabel);
369
+ }, 1400);
370
+ }
371
+
372
+ function setCopyButtonsDisabled(isDisabled) {
373
+ elements.copyReport.disabled = isDisabled;
374
+ elements.copyBlocking.disabled = isDisabled;
375
+ elements.copyRefactoring.disabled = isDisabled;
376
+ const copyRisk = document.getElementById("copy-risk");
377
+ if (copyRisk) copyRisk.disabled = isDisabled;
378
+ }
379
+
380
+ function showError(error) {
381
+ setButtonBusy(false);
382
+ elements.statusPill.className = "status-pill status-failed";
383
+ elements.statusPill.textContent = "Failed";
384
+ renderError(error.message || String(error));
385
+ }
386
+
387
+ function escapeHtml(value) {
388
+ return String(value)
389
+ .replace(/&/g, "&amp;")
390
+ .replace(/</g, "&lt;")
391
+ .replace(/>/g, "&gt;")
392
+ .replace(/"/g, "&quot;")
393
+ .replace(/'/g, "&#039;");
394
+ }
395
+ }
@@ -0,0 +1,97 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Code Scan</title>
7
+ <link rel="stylesheet" href="/styles.css">
8
+ </head>
9
+ <body>
10
+ <main class="app-shell">
11
+ <header class="topbar">
12
+ <div>
13
+ <div class="title-line">
14
+ <i class="title-icon" data-lucide="code-2" aria-hidden="true"></i>
15
+ <h1>Code Scan</h1>
16
+ <div id="status-pill" class="status-pill status-idle" aria-live="polite">Idle</div>
17
+ </div>
18
+ <p id="run-summary" class="muted">No scan has run yet.</p>
19
+ </div>
20
+ <div class="toolbar">
21
+ <label class="auto-refresh">
22
+ <input id="auto-refresh" type="checkbox">
23
+ Auto refresh
24
+ </label>
25
+ <button id="copy-report" class="secondary-button icon-button" type="button" disabled>
26
+ <i data-lucide="copy" aria-hidden="true"></i>
27
+ <span class="button-label">Copy report</span>
28
+ </button>
29
+ <button id="refresh-button" class="icon-button" type="button">
30
+ <i data-lucide="refresh-cw" aria-hidden="true"></i>
31
+ <span class="button-label">Refresh</span>
32
+ </button>
33
+ </div>
34
+ </header>
35
+
36
+ <section id="error-banner" class="error-banner" aria-live="assertive" hidden></section>
37
+
38
+ <section id="overview-grid" class="overview-grid"></section>
39
+
40
+ <section class="analysis-grid">
41
+ <div class="main-column">
42
+ <section class="panel panel-critical">
43
+ <div class="panel-header">
44
+ <div class="section-title section-title-critical">
45
+ <i class="section-icon" data-lucide="circle-x" aria-hidden="true"></i>
46
+ <h2>Blocking Findings</h2>
47
+ <span id="blocking-count" class="panel-count">0</span>
48
+ </div>
49
+ <button id="copy-blocking" class="copy-section icon-button" type="button" disabled>
50
+ <i data-lucide="copy" aria-hidden="true"></i>
51
+ <span class="button-label">Copy</span>
52
+ </button>
53
+ </div>
54
+ <div id="check-sections" class="stack-list"></div>
55
+ </section>
56
+
57
+ <section class="panel panel-refactoring targets-panel">
58
+ <div class="panel-header">
59
+ <div class="section-title section-title-refactoring">
60
+ <i class="section-icon" data-lucide="scissors" aria-hidden="true"></i>
61
+ <h2>Refactoring Suggestions</h2>
62
+ <span id="target-count" class="panel-count">0</span>
63
+ </div>
64
+ <button id="copy-refactoring" class="copy-section icon-button" type="button" disabled>
65
+ <i data-lucide="copy" aria-hidden="true"></i>
66
+ <span class="button-label">Copy</span>
67
+ </button>
68
+ </div>
69
+ <div id="targets-list" class="target-grid"></div>
70
+ </section>
71
+ </div>
72
+
73
+ <aside class="panel health-panel">
74
+ <div class="panel-header">
75
+ <div class="section-title">
76
+ <i class="section-icon" data-lucide="bar-chart-3" aria-hidden="true"></i>
77
+ <h2>Code Health</h2>
78
+ </div>
79
+ </div>
80
+ <div id="health-summary" class="health-summary"></div>
81
+ <h3 class="section-subhead">
82
+ <i data-lucide="alert-triangle" aria-hidden="true"></i>
83
+ <span>Lowest Maintainability</span>
84
+ </h3>
85
+ <div id="file-scores" class="stack-list"></div>
86
+ </aside>
87
+ </section>
88
+ </main>
89
+
90
+ <script src="/vendor/lucide.js"></script>
91
+ <script src="/app-format.js"></script>
92
+ <script src="/app-copy.js"></script>
93
+ <script src="/app-icons.js"></script>
94
+ <script src="/app-findings.js"></script>
95
+ <script src="/app.js"></script>
96
+ </body>
97
+ </html>