thuban 0.3.2 → 0.3.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.
- package/README.md +137 -102
- package/dist/LICENSE +21 -0
- package/dist/README.md +185 -0
- package/dist/cli.js +2 -0
- package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
- package/dist/packages/scanner/alert-manager.js +1 -0
- package/dist/packages/scanner/baseline-manager.js +1 -0
- package/dist/packages/scanner/code-scanner.js +1 -0
- package/dist/packages/scanner/codebase-passport.js +1 -0
- package/dist/packages/scanner/copy-paste-detector.js +1 -0
- package/dist/packages/scanner/dependency-graph.js +1 -0
- package/dist/packages/scanner/drift-detector.js +1 -0
- package/dist/packages/scanner/executive-report.js +1 -0
- package/dist/packages/scanner/file-collector.js +1 -0
- package/dist/packages/scanner/file-watcher.js +1 -0
- package/dist/packages/scanner/ghost-code-detector.js +1 -0
- package/dist/packages/scanner/hallucination-detector.js +1 -0
- package/dist/packages/scanner/health-checker.js +1 -0
- package/dist/packages/scanner/html-report.js +1 -0
- package/dist/packages/scanner/index.js +1 -0
- package/dist/packages/scanner/license-manager.js +1 -0
- package/dist/packages/scanner/master-health-checker.js +1 -0
- package/dist/packages/scanner/monitor-notifier.js +1 -0
- package/dist/packages/scanner/monitor-service.js +1 -0
- package/dist/packages/scanner/monitor-store.js +1 -0
- package/dist/packages/scanner/pre-commit-gate.js +1 -0
- package/dist/packages/scanner/scan-diff.js +1 -0
- package/dist/packages/scanner/scan-runner.js +1 -0
- package/dist/packages/scanner/secret-scanner.js +1 -0
- package/dist/packages/scanner/sentinel-core.js +1 -0
- package/dist/packages/scanner/sentinel-knowledge.js +1 -0
- package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
- package/dist/packages/scanner/tech-debt-cost.js +1 -0
- package/dist/packages/scanner/widget-generator.js +1 -0
- package/package.json +16 -8
- package/cli.js +0 -2412
- package/packages/scanner/ai-confidence-scorer.js +0 -260
- package/packages/scanner/alert-manager.js +0 -398
- package/packages/scanner/baseline-manager.js +0 -109
- package/packages/scanner/code-scanner.js +0 -713
- package/packages/scanner/codebase-passport.js +0 -299
- package/packages/scanner/copy-paste-detector.js +0 -250
- package/packages/scanner/dependency-graph.js +0 -536
- package/packages/scanner/drift-detector.js +0 -423
- package/packages/scanner/executive-report.js +0 -774
- package/packages/scanner/file-watcher.js +0 -323
- package/packages/scanner/ghost-code-detector.js +0 -226
- package/packages/scanner/hallucination-detector.js +0 -652
- package/packages/scanner/health-checker.js +0 -586
- package/packages/scanner/html-report.js +0 -634
- package/packages/scanner/index.js +0 -92
- package/packages/scanner/license-manager.js +0 -318
- package/packages/scanner/master-health-checker.js +0 -513
- package/packages/scanner/pre-commit-gate.js +0 -216
- package/packages/scanner/sentinel-core.js +0 -608
- package/packages/scanner/sentinel-knowledge.js +0 -322
- package/packages/scanner/tech-debt-analyzer.js +0 -565
- package/packages/scanner/tech-debt-cost.js +0 -194
- package/packages/scanner/widget-generator.js +0 -415
|
@@ -1,634 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @thuban-dna
|
|
3
|
-
* @purpose Generate visual HTML tech debt dashboards
|
|
4
|
-
* @module thuban
|
|
5
|
-
* @layer presentation
|
|
6
|
-
* @imports-from tech-debt-analyzer, dependency-graph
|
|
7
|
-
* @exports-to cli, api
|
|
8
|
-
* @side-effects File system write (HTML output)
|
|
9
|
-
* @critical-for CTO-facing reports, sales demos, enterprise value proposition
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
|
|
15
|
-
class HtmlReportGenerator {
|
|
16
|
-
constructor(config = {}) {
|
|
17
|
-
this.config = {
|
|
18
|
-
rootPath: config.rootPath || process.cwd(),
|
|
19
|
-
outputDir: config.outputDir || process.cwd(),
|
|
20
|
-
...config
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Generate a full HTML dashboard report
|
|
26
|
-
*/
|
|
27
|
-
generate(debt, depData = {}, meta = {}) {
|
|
28
|
-
const timestamp = new Date().toISOString();
|
|
29
|
-
const projectName = meta.projectName || path.basename(this.config.rootPath);
|
|
30
|
-
|
|
31
|
-
const html = `<!DOCTYPE html>
|
|
32
|
-
<html lang="en">
|
|
33
|
-
<head>
|
|
34
|
-
<meta charset="UTF-8">
|
|
35
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
36
|
-
<title>Thuban Report — ${this._esc(projectName)}</title>
|
|
37
|
-
<style>
|
|
38
|
-
${this._css()}
|
|
39
|
-
</style>
|
|
40
|
-
</head>
|
|
41
|
-
<body>
|
|
42
|
-
<div class="container">
|
|
43
|
-
${this._header(projectName, timestamp, debt)}
|
|
44
|
-
${this._overallScore(debt)}
|
|
45
|
-
${this._categoryCards(debt)}
|
|
46
|
-
${this._motherCodeSection(debt)}
|
|
47
|
-
${this._topIssues(debt)}
|
|
48
|
-
${this._fixableItems(debt)}
|
|
49
|
-
${this._dependencySection(depData)}
|
|
50
|
-
${this._recommendations(debt)}
|
|
51
|
-
${this._footer(debt, meta)}
|
|
52
|
-
</div>
|
|
53
|
-
<script>
|
|
54
|
-
${this._js()}
|
|
55
|
-
</script>
|
|
56
|
-
</body>
|
|
57
|
-
</html>`;
|
|
58
|
-
|
|
59
|
-
return html;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Generate and write to file
|
|
64
|
-
*/
|
|
65
|
-
async writeReport(debt, depData = {}, meta = {}) {
|
|
66
|
-
const html = this.generate(debt, depData, meta);
|
|
67
|
-
const filename = `thuban-report-${new Date().toISOString().slice(0, 10)}.html`;
|
|
68
|
-
const outputPath = path.join(this.config.outputDir, filename);
|
|
69
|
-
fs.writeFileSync(outputPath, html, 'utf-8');
|
|
70
|
-
return outputPath;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ─── HTML Sections ──────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
_header(projectName, timestamp, debt) {
|
|
76
|
-
return `
|
|
77
|
-
<header class="header">
|
|
78
|
-
<div class="header-left">
|
|
79
|
-
<div class="logo">
|
|
80
|
-
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
|
|
81
|
-
<polygon points="16,2 30,28 2,28" stroke="#00f6ff" stroke-width="2" fill="none"/>
|
|
82
|
-
<polygon points="16,8 25,25 7,25" stroke="#00f6ff" stroke-width="1" fill="rgba(0,246,255,0.1)"/>
|
|
83
|
-
<circle cx="16" cy="18" r="3" fill="#00f6ff"/>
|
|
84
|
-
</svg>
|
|
85
|
-
<h1>THUBAN</h1>
|
|
86
|
-
</div>
|
|
87
|
-
<span class="subtitle">Code Health Report</span>
|
|
88
|
-
</div>
|
|
89
|
-
<div class="header-right">
|
|
90
|
-
<div class="meta-item"><span class="meta-label">Project</span><span class="meta-value">${this._esc(projectName)}</span></div>
|
|
91
|
-
<div class="meta-item"><span class="meta-label">Generated</span><span class="meta-value">${new Date(timestamp).toLocaleString()}</span></div>
|
|
92
|
-
<div class="meta-item"><span class="meta-label">Files</span><span class="meta-value">${debt.stats.totalFiles}</span></div>
|
|
93
|
-
<div class="meta-item"><span class="meta-label">Scan Time</span><span class="meta-value">${debt.stats.scanTime}ms</span></div>
|
|
94
|
-
</div>
|
|
95
|
-
</header>`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
_overallScore(debt) {
|
|
99
|
-
const score = debt.scores.overall;
|
|
100
|
-
const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
|
|
101
|
-
const gradeClass = score >= 80 ? 'grade-good' : score >= 60 ? 'grade-warn' : 'grade-bad';
|
|
102
|
-
const circumference = 2 * Math.PI * 54;
|
|
103
|
-
const offset = circumference - (score / 100) * circumference;
|
|
104
|
-
|
|
105
|
-
return `
|
|
106
|
-
<section class="score-section">
|
|
107
|
-
<div class="score-ring-container">
|
|
108
|
-
<svg class="score-ring" viewBox="0 0 120 120">
|
|
109
|
-
<circle cx="60" cy="60" r="54" stroke="#1a1a2e" stroke-width="8" fill="none"/>
|
|
110
|
-
<circle cx="60" cy="60" r="54" stroke="${this._scoreColor(score)}" stroke-width="8" fill="none"
|
|
111
|
-
stroke-dasharray="${circumference}" stroke-dashoffset="${offset}"
|
|
112
|
-
stroke-linecap="round" transform="rotate(-90 60 60)" class="score-progress"/>
|
|
113
|
-
</svg>
|
|
114
|
-
<div class="score-inner">
|
|
115
|
-
<span class="score-number">${score}</span>
|
|
116
|
-
<span class="score-label">/ 100</span>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
<div class="score-details">
|
|
120
|
-
<div class="grade ${gradeClass}">Grade ${grade}</div>
|
|
121
|
-
<div class="score-summary">
|
|
122
|
-
<div class="stat"><span class="stat-num">${debt.stats.totalIssues}</span><span class="stat-label">Issues</span></div>
|
|
123
|
-
<div class="stat"><span class="stat-num">${debt.stats.fixableCount}</span><span class="stat-label">Auto-fixable</span></div>
|
|
124
|
-
<div class="stat"><span class="stat-num">${debt.stats.manualCount}</span><span class="stat-label">Manual</span></div>
|
|
125
|
-
<div class="stat"><span class="stat-num">${debt.stats.estimatedFixTime}</span><span class="stat-label">Est. Fix Time</span></div>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
</section>`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
_categoryCards(debt) {
|
|
132
|
-
const categories = [
|
|
133
|
-
{ key: 'security', icon: '🛡️', label: 'Security' },
|
|
134
|
-
{ key: 'architecture', icon: '🏗️', label: 'Architecture' },
|
|
135
|
-
{ key: 'codeQuality', icon: '📝', label: 'Code Quality' },
|
|
136
|
-
{ key: 'motherCode', icon: '🧬', label: 'Mother Code' },
|
|
137
|
-
{ key: 'dependencies', icon: '🔗', label: 'Dependencies' },
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
const cards = categories.map(cat => {
|
|
141
|
-
const score = debt.scores[cat.key] || 0;
|
|
142
|
-
const catData = debt.categories[cat.key] || {};
|
|
143
|
-
const issueCount = catData.issues?.length || 0;
|
|
144
|
-
return `
|
|
145
|
-
<div class="category-card">
|
|
146
|
-
<div class="card-header">
|
|
147
|
-
<span class="card-icon">${cat.icon}</span>
|
|
148
|
-
<span class="card-label">${cat.label}</span>
|
|
149
|
-
</div>
|
|
150
|
-
<div class="card-score" style="color: ${this._scoreColor(score)}">${score}%</div>
|
|
151
|
-
<div class="card-bar">
|
|
152
|
-
<div class="card-bar-fill" style="width: ${score}%; background: ${this._scoreColor(score)}"></div>
|
|
153
|
-
</div>
|
|
154
|
-
<div class="card-issues">${issueCount} issue${issueCount !== 1 ? 's' : ''}</div>
|
|
155
|
-
</div>`;
|
|
156
|
-
}).join('');
|
|
157
|
-
|
|
158
|
-
return `
|
|
159
|
-
<section class="categories-section">
|
|
160
|
-
<h2>Category Breakdown</h2>
|
|
161
|
-
<div class="category-grid">${cards}</div>
|
|
162
|
-
</section>`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
_motherCodeSection(debt) {
|
|
166
|
-
const mc = debt.categories.motherCode;
|
|
167
|
-
if (!mc) return '';
|
|
168
|
-
|
|
169
|
-
const coverage = mc.coverage || 0;
|
|
170
|
-
|
|
171
|
-
return `
|
|
172
|
-
<section class="mother-code-section">
|
|
173
|
-
<h2>🧬 Mother Code Coverage</h2>
|
|
174
|
-
<div class="mc-grid">
|
|
175
|
-
<div class="mc-stat">
|
|
176
|
-
<div class="mc-circle" style="background: conic-gradient(${this._scoreColor(coverage)} ${coverage * 3.6}deg, #1a1a2e ${coverage * 3.6}deg)">
|
|
177
|
-
<span>${coverage}%</span>
|
|
178
|
-
</div>
|
|
179
|
-
<span class="mc-label">Coverage</span>
|
|
180
|
-
</div>
|
|
181
|
-
<div class="mc-details">
|
|
182
|
-
<div class="mc-row"><span class="mc-dot dot-green"></span> Annotated: <strong>${mc.annotated || 0}</strong> files</div>
|
|
183
|
-
<div class="mc-row"><span class="mc-dot dot-yellow"></span> Missing: <strong>${mc.missing || 0}</strong> files</div>
|
|
184
|
-
<div class="mc-row"><span class="mc-dot dot-red"></span> Drifted: <strong>${mc.drifted || 0}</strong> files</div>
|
|
185
|
-
</div>
|
|
186
|
-
<div class="mc-action">
|
|
187
|
-
<p>Inject Mother Code DNA into all files:</p>
|
|
188
|
-
<code>thuban inject [path] --fix</code>
|
|
189
|
-
</div>
|
|
190
|
-
</div>
|
|
191
|
-
</section>`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
_topIssues(debt) {
|
|
195
|
-
const allIssues = [...(debt.manual || []), ...(debt.fixable || [])];
|
|
196
|
-
const sorted = allIssues
|
|
197
|
-
.filter(i => i.severity !== 'info')
|
|
198
|
-
.sort((a, b) => {
|
|
199
|
-
const sevOrder = { critical: 0, high: 1, error: 1, medium: 2, warning: 2, low: 3 };
|
|
200
|
-
return (sevOrder[a.severity] || 4) - (sevOrder[b.severity] || 4);
|
|
201
|
-
})
|
|
202
|
-
.slice(0, 20);
|
|
203
|
-
|
|
204
|
-
if (sorted.length === 0) return '';
|
|
205
|
-
|
|
206
|
-
const rows = sorted.map(issue => `
|
|
207
|
-
<tr>
|
|
208
|
-
<td><span class="sev sev-${issue.severity}">${(issue.severity || '').toUpperCase()}</span></td>
|
|
209
|
-
<td class="file-cell">${this._esc(issue.file || '')}</td>
|
|
210
|
-
<td>${this._esc(issue.message || issue.type || '')}</td>
|
|
211
|
-
<td>${issue.fixable ? '<span class="fixable-badge">Auto-fix</span>' : '<span class="manual-badge">Manual</span>'}</td>
|
|
212
|
-
</tr>`).join('');
|
|
213
|
-
|
|
214
|
-
return `
|
|
215
|
-
<section class="issues-section">
|
|
216
|
-
<h2>Top Issues</h2>
|
|
217
|
-
<table class="issues-table">
|
|
218
|
-
<thead><tr><th>Severity</th><th>File</th><th>Issue</th><th>Fix</th></tr></thead>
|
|
219
|
-
<tbody>${rows}</tbody>
|
|
220
|
-
</table>
|
|
221
|
-
</section>`;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
_fixableItems(debt) {
|
|
225
|
-
if (!debt.fixable || debt.fixable.length === 0) return '';
|
|
226
|
-
|
|
227
|
-
const grouped = {};
|
|
228
|
-
for (const item of debt.fixable) {
|
|
229
|
-
const action = item.fixAction || 'unknown';
|
|
230
|
-
if (!grouped[action]) grouped[action] = { count: 0, label: action };
|
|
231
|
-
grouped[action].count++;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const labels = {
|
|
235
|
-
inject_widget: 'Inject Mother Code DNA',
|
|
236
|
-
regenerate_widget: 'Update stale annotations',
|
|
237
|
-
remove_console_log: 'Remove debug console.logs',
|
|
238
|
-
extract_to_env: 'Extract hardcoded secrets to .env',
|
|
239
|
-
flag_for_removal: 'Remove orphan files',
|
|
240
|
-
break_circular: 'Break circular dependencies',
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const items = Object.entries(grouped)
|
|
244
|
-
.sort((a, b) => b[1].count - a[1].count)
|
|
245
|
-
.map(([action, data]) => `
|
|
246
|
-
<div class="fix-item">
|
|
247
|
-
<span class="fix-count">${data.count}</span>
|
|
248
|
-
<span class="fix-label">${labels[action] || action}</span>
|
|
249
|
-
</div>`).join('');
|
|
250
|
-
|
|
251
|
-
return `
|
|
252
|
-
<section class="fixable-section">
|
|
253
|
-
<h2>Auto-Fixable Tech Debt</h2>
|
|
254
|
-
<p class="fix-intro">Run <code>thuban fix [path] --fix</code> to resolve ${debt.stats.fixableCount} items automatically:</p>
|
|
255
|
-
<div class="fix-grid">${items}</div>
|
|
256
|
-
</section>`;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
_dependencySection(depData) {
|
|
260
|
-
if (!depData || (!depData.circular?.length && !depData.orphans?.length && !depData.critical?.length)) {
|
|
261
|
-
return '';
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
let content = '<section class="deps-section"><h2>Dependency Analysis</h2><div class="deps-grid">';
|
|
265
|
-
|
|
266
|
-
if (depData.circular?.length > 0) {
|
|
267
|
-
content += `
|
|
268
|
-
<div class="dep-card dep-danger">
|
|
269
|
-
<h3>Circular Dependencies</h3>
|
|
270
|
-
<span class="dep-count">${depData.circular.length}</span>
|
|
271
|
-
<ul>${depData.circular.slice(0, 5).map(c => `<li>${this._esc(Array.isArray(c) ? c.join(' → ') : String(c))}</li>`).join('')}</ul>
|
|
272
|
-
</div>`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (depData.orphans?.length > 0) {
|
|
276
|
-
content += `
|
|
277
|
-
<div class="dep-card dep-warn">
|
|
278
|
-
<h3>Orphan Files</h3>
|
|
279
|
-
<span class="dep-count">${depData.orphans.length}</span>
|
|
280
|
-
<p>Files with no imports and no consumers</p>
|
|
281
|
-
</div>`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (depData.critical?.length > 0) {
|
|
285
|
-
content += `
|
|
286
|
-
<div class="dep-card dep-info">
|
|
287
|
-
<h3>Critical Files</h3>
|
|
288
|
-
<span class="dep-count">${depData.critical.length}</span>
|
|
289
|
-
<p>Files with the most dependents — high blast radius</p>
|
|
290
|
-
</div>`;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
content += '</div></section>';
|
|
294
|
-
return content;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
_recommendations(debt) {
|
|
298
|
-
const recs = [];
|
|
299
|
-
if (debt.scores.security < 80) recs.push({ priority: 'critical', text: 'Fix security issues immediately — hardcoded secrets or injection risks detected' });
|
|
300
|
-
if ((debt.categories.architecture?.issues || []).filter(i => i.type === 'circular_dependency').length > 0) recs.push({ priority: 'high', text: 'Resolve circular dependencies to prevent build fragility' });
|
|
301
|
-
if ((debt.categories.motherCode?.coverage || 0) < 50) recs.push({ priority: 'medium', text: `Run <code>thuban inject --fix</code> to raise Mother Code coverage from ${debt.categories.motherCode?.coverage || 0}% to 100%` });
|
|
302
|
-
if (debt.stats.fixableCount > 0) recs.push({ priority: 'medium', text: `Run <code>thuban fix --fix</code> to auto-resolve ${debt.stats.fixableCount} items` });
|
|
303
|
-
if ((debt.categories.motherCode?.drifted || 0) > 0) recs.push({ priority: 'low', text: `${debt.categories.motherCode.drifted} files have drifted from their Mother Code annotations — regenerate with <code>thuban fix --fix</code>` });
|
|
304
|
-
|
|
305
|
-
if (recs.length === 0) {
|
|
306
|
-
return `
|
|
307
|
-
<section class="recs-section">
|
|
308
|
-
<h2>Recommendations</h2>
|
|
309
|
-
<div class="rec-clean">Codebase is in excellent health. No critical actions needed.</div>
|
|
310
|
-
</section>`;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const items = recs.map(r => `
|
|
314
|
-
<div class="rec-item rec-${r.priority}">
|
|
315
|
-
<span class="rec-priority">${r.priority.toUpperCase()}</span>
|
|
316
|
-
<span class="rec-text">${r.text}</span>
|
|
317
|
-
</div>`).join('');
|
|
318
|
-
|
|
319
|
-
return `
|
|
320
|
-
<section class="recs-section">
|
|
321
|
-
<h2>Recommendations</h2>
|
|
322
|
-
${items}
|
|
323
|
-
</section>`;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
_footer(debt, meta) {
|
|
327
|
-
return `
|
|
328
|
-
<footer class="footer">
|
|
329
|
-
<div class="footer-brand">
|
|
330
|
-
<span>Generated by <strong>Thuban</strong> Code Health Engine v0.2.0</span>
|
|
331
|
-
<span class="footer-link">thuban.dev</span>
|
|
332
|
-
</div>
|
|
333
|
-
<div class="footer-meta">
|
|
334
|
-
<span>${debt.stats.totalFiles} files scanned in ${debt.stats.scanTime}ms</span>
|
|
335
|
-
</div>
|
|
336
|
-
</footer>`;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// ─── CSS ────────────────────────────────────────────────
|
|
340
|
-
|
|
341
|
-
_css() {
|
|
342
|
-
return `
|
|
343
|
-
:root {
|
|
344
|
-
--bg: #0a0a1a;
|
|
345
|
-
--surface: #12122a;
|
|
346
|
-
--surface2: #1a1a3e;
|
|
347
|
-
--border: #2a2a5a;
|
|
348
|
-
--text: #e0e0f0;
|
|
349
|
-
--text-dim: #8888aa;
|
|
350
|
-
--cyan: #00f6ff;
|
|
351
|
-
--green: #00ff88;
|
|
352
|
-
--yellow: #ffd93d;
|
|
353
|
-
--red: #ff4444;
|
|
354
|
-
--orange: #ff9500;
|
|
355
|
-
--purple: #a855f7;
|
|
356
|
-
--font: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
|
357
|
-
--mono: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
361
|
-
|
|
362
|
-
body {
|
|
363
|
-
background: var(--bg);
|
|
364
|
-
color: var(--text);
|
|
365
|
-
font-family: var(--font);
|
|
366
|
-
line-height: 1.6;
|
|
367
|
-
min-height: 100vh;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
.container {
|
|
371
|
-
max-width: 1200px;
|
|
372
|
-
margin: 0 auto;
|
|
373
|
-
padding: 2rem;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/* Header */
|
|
377
|
-
.header {
|
|
378
|
-
display: flex;
|
|
379
|
-
justify-content: space-between;
|
|
380
|
-
align-items: center;
|
|
381
|
-
padding: 1.5rem 2rem;
|
|
382
|
-
background: var(--surface);
|
|
383
|
-
border: 1px solid var(--border);
|
|
384
|
-
border-radius: 12px;
|
|
385
|
-
margin-bottom: 2rem;
|
|
386
|
-
}
|
|
387
|
-
.header-left { display: flex; align-items: center; gap: 1.5rem; }
|
|
388
|
-
.logo { display: flex; align-items: center; gap: 0.75rem; }
|
|
389
|
-
.logo h1 { font-size: 1.5rem; letter-spacing: 0.3em; color: var(--cyan); font-weight: 300; }
|
|
390
|
-
.subtitle { color: var(--text-dim); font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.1em; }
|
|
391
|
-
.header-right { display: flex; gap: 2rem; }
|
|
392
|
-
.meta-item { display: flex; flex-direction: column; align-items: flex-end; }
|
|
393
|
-
.meta-label { font-size: 0.7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
394
|
-
.meta-value { font-size: 0.9rem; color: var(--text); font-family: var(--mono); }
|
|
395
|
-
|
|
396
|
-
/* Score Section */
|
|
397
|
-
.score-section {
|
|
398
|
-
display: flex;
|
|
399
|
-
align-items: center;
|
|
400
|
-
gap: 3rem;
|
|
401
|
-
padding: 2rem;
|
|
402
|
-
background: var(--surface);
|
|
403
|
-
border: 1px solid var(--border);
|
|
404
|
-
border-radius: 12px;
|
|
405
|
-
margin-bottom: 2rem;
|
|
406
|
-
}
|
|
407
|
-
.score-ring-container { position: relative; width: 140px; height: 140px; flex-shrink: 0; }
|
|
408
|
-
.score-ring { width: 100%; height: 100%; }
|
|
409
|
-
.score-progress { transition: stroke-dashoffset 1s ease-out; }
|
|
410
|
-
.score-inner {
|
|
411
|
-
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
|
|
412
|
-
text-align: center;
|
|
413
|
-
}
|
|
414
|
-
.score-number { font-size: 2.5rem; font-weight: 700; color: var(--text); display: block; line-height: 1; }
|
|
415
|
-
.score-label { font-size: 0.8rem; color: var(--text-dim); }
|
|
416
|
-
.score-details { flex: 1; }
|
|
417
|
-
.grade {
|
|
418
|
-
font-size: 1.8rem; font-weight: 700; margin-bottom: 1rem;
|
|
419
|
-
letter-spacing: 0.05em;
|
|
420
|
-
}
|
|
421
|
-
.grade-good { color: var(--green); }
|
|
422
|
-
.grade-warn { color: var(--yellow); }
|
|
423
|
-
.grade-bad { color: var(--red); }
|
|
424
|
-
.score-summary { display: flex; gap: 2rem; }
|
|
425
|
-
.stat { display: flex; flex-direction: column; }
|
|
426
|
-
.stat-num { font-size: 1.4rem; font-weight: 600; font-family: var(--mono); }
|
|
427
|
-
.stat-label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; }
|
|
428
|
-
|
|
429
|
-
/* Category Cards */
|
|
430
|
-
.categories-section { margin-bottom: 2rem; }
|
|
431
|
-
.categories-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }
|
|
432
|
-
.category-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; }
|
|
433
|
-
.category-card {
|
|
434
|
-
background: var(--surface);
|
|
435
|
-
border: 1px solid var(--border);
|
|
436
|
-
border-radius: 10px;
|
|
437
|
-
padding: 1.25rem;
|
|
438
|
-
text-align: center;
|
|
439
|
-
transition: border-color 0.2s;
|
|
440
|
-
}
|
|
441
|
-
.category-card:hover { border-color: var(--cyan); }
|
|
442
|
-
.card-header { display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-bottom: 0.75rem; }
|
|
443
|
-
.card-icon { font-size: 1.2rem; }
|
|
444
|
-
.card-label { font-size: 0.85rem; color: var(--text-dim); }
|
|
445
|
-
.card-score { font-size: 2rem; font-weight: 700; font-family: var(--mono); margin-bottom: 0.5rem; }
|
|
446
|
-
.card-bar { height: 4px; background: var(--surface2); border-radius: 2px; overflow: hidden; margin-bottom: 0.5rem; }
|
|
447
|
-
.card-bar-fill { height: 100%; border-radius: 2px; transition: width 0.8s ease-out; }
|
|
448
|
-
.card-issues { font-size: 0.75rem; color: var(--text-dim); }
|
|
449
|
-
|
|
450
|
-
/* Mother Code */
|
|
451
|
-
.mother-code-section {
|
|
452
|
-
background: var(--surface);
|
|
453
|
-
border: 1px solid var(--border);
|
|
454
|
-
border-radius: 12px;
|
|
455
|
-
padding: 2rem;
|
|
456
|
-
margin-bottom: 2rem;
|
|
457
|
-
}
|
|
458
|
-
.mother-code-section h2 { margin-bottom: 1.5rem; font-size: 1.1rem; color: var(--cyan); }
|
|
459
|
-
.mc-grid { display: grid; grid-template-columns: 160px 1fr 1fr; gap: 2rem; align-items: center; }
|
|
460
|
-
.mc-circle {
|
|
461
|
-
width: 120px; height: 120px; border-radius: 50%;
|
|
462
|
-
display: flex; align-items: center; justify-content: center;
|
|
463
|
-
margin: 0 auto;
|
|
464
|
-
}
|
|
465
|
-
.mc-circle span { background: var(--bg); width: 90px; height: 90px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; font-weight: 700; font-family: var(--mono); }
|
|
466
|
-
.mc-label { text-align: center; margin-top: 0.5rem; color: var(--text-dim); font-size: 0.85rem; }
|
|
467
|
-
.mc-row { padding: 0.4rem 0; display: flex; align-items: center; gap: 0.5rem; }
|
|
468
|
-
.mc-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
|
469
|
-
.dot-green { background: var(--green); }
|
|
470
|
-
.dot-yellow { background: var(--yellow); }
|
|
471
|
-
.dot-red { background: var(--red); }
|
|
472
|
-
.mc-action code {
|
|
473
|
-
display: inline-block; background: var(--bg); padding: 0.4rem 0.8rem;
|
|
474
|
-
border-radius: 6px; font-family: var(--mono); color: var(--cyan); margin-top: 0.5rem;
|
|
475
|
-
border: 1px solid var(--border);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/* Issues Table */
|
|
479
|
-
.issues-section { margin-bottom: 2rem; }
|
|
480
|
-
.issues-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }
|
|
481
|
-
.issues-table {
|
|
482
|
-
width: 100%; border-collapse: collapse;
|
|
483
|
-
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
|
484
|
-
overflow: hidden;
|
|
485
|
-
}
|
|
486
|
-
.issues-table th {
|
|
487
|
-
text-align: left; padding: 0.75rem 1rem; background: var(--surface2);
|
|
488
|
-
font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-dim);
|
|
489
|
-
border-bottom: 1px solid var(--border);
|
|
490
|
-
}
|
|
491
|
-
.issues-table td { padding: 0.6rem 1rem; border-bottom: 1px solid rgba(42,42,90,0.5); font-size: 0.85rem; }
|
|
492
|
-
.issues-table tr:last-child td { border-bottom: none; }
|
|
493
|
-
.issues-table tr:hover { background: rgba(0,246,255,0.03); }
|
|
494
|
-
.file-cell { font-family: var(--mono); font-size: 0.8rem; color: var(--cyan); max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
495
|
-
.sev {
|
|
496
|
-
display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px;
|
|
497
|
-
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.05em;
|
|
498
|
-
}
|
|
499
|
-
.sev-critical { background: rgba(255,68,68,0.2); color: var(--red); }
|
|
500
|
-
.sev-high, .sev-error { background: rgba(255,149,0,0.2); color: var(--orange); }
|
|
501
|
-
.sev-medium, .sev-warning { background: rgba(255,217,61,0.2); color: var(--yellow); }
|
|
502
|
-
.sev-low, .sev-info { background: rgba(136,136,170,0.15); color: var(--text-dim); }
|
|
503
|
-
.fixable-badge { background: rgba(0,255,136,0.15); color: var(--green); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.7rem; }
|
|
504
|
-
.manual-badge { background: rgba(255,217,61,0.15); color: var(--yellow); padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.7rem; }
|
|
505
|
-
|
|
506
|
-
/* Fixable Section */
|
|
507
|
-
.fixable-section {
|
|
508
|
-
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
|
|
509
|
-
padding: 2rem; margin-bottom: 2rem;
|
|
510
|
-
}
|
|
511
|
-
.fixable-section h2 { margin-bottom: 0.5rem; font-size: 1.1rem; color: var(--green); }
|
|
512
|
-
.fix-intro { color: var(--text-dim); font-size: 0.9rem; margin-bottom: 1.5rem; }
|
|
513
|
-
.fix-intro code { background: var(--bg); padding: 0.2rem 0.5rem; border-radius: 4px; font-family: var(--mono); color: var(--cyan); border: 1px solid var(--border); }
|
|
514
|
-
.fix-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
515
|
-
.fix-item {
|
|
516
|
-
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
|
517
|
-
padding: 1rem; display: flex; align-items: center; gap: 1rem;
|
|
518
|
-
}
|
|
519
|
-
.fix-count { font-size: 1.8rem; font-weight: 700; color: var(--green); font-family: var(--mono); min-width: 40px; }
|
|
520
|
-
.fix-label { font-size: 0.85rem; color: var(--text); }
|
|
521
|
-
|
|
522
|
-
/* Dependencies */
|
|
523
|
-
.deps-section { margin-bottom: 2rem; }
|
|
524
|
-
.deps-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }
|
|
525
|
-
.deps-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
526
|
-
.dep-card {
|
|
527
|
-
background: var(--surface); border-radius: 10px; padding: 1.5rem;
|
|
528
|
-
border: 1px solid var(--border);
|
|
529
|
-
}
|
|
530
|
-
.dep-danger { border-left: 3px solid var(--red); }
|
|
531
|
-
.dep-warn { border-left: 3px solid var(--yellow); }
|
|
532
|
-
.dep-info { border-left: 3px solid var(--cyan); }
|
|
533
|
-
.dep-card h3 { font-size: 0.9rem; margin-bottom: 0.5rem; }
|
|
534
|
-
.dep-count { font-size: 2rem; font-weight: 700; font-family: var(--mono); color: var(--text); display: block; margin-bottom: 0.5rem; }
|
|
535
|
-
.dep-card ul { list-style: none; font-size: 0.8rem; color: var(--text-dim); font-family: var(--mono); }
|
|
536
|
-
.dep-card li { padding: 0.2rem 0; }
|
|
537
|
-
.dep-card p { font-size: 0.8rem; color: var(--text-dim); }
|
|
538
|
-
|
|
539
|
-
/* Recommendations */
|
|
540
|
-
.recs-section { margin-bottom: 2rem; }
|
|
541
|
-
.recs-section h2 { margin-bottom: 1rem; font-size: 1.1rem; color: var(--cyan); text-transform: uppercase; letter-spacing: 0.1em; }
|
|
542
|
-
.rec-item {
|
|
543
|
-
display: flex; align-items: center; gap: 1rem;
|
|
544
|
-
background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
|
|
545
|
-
padding: 1rem 1.5rem; margin-bottom: 0.5rem;
|
|
546
|
-
}
|
|
547
|
-
.rec-critical { border-left: 3px solid var(--red); }
|
|
548
|
-
.rec-high { border-left: 3px solid var(--orange); }
|
|
549
|
-
.rec-medium { border-left: 3px solid var(--yellow); }
|
|
550
|
-
.rec-low { border-left: 3px solid var(--text-dim); }
|
|
551
|
-
.rec-priority {
|
|
552
|
-
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em;
|
|
553
|
-
padding: 0.2rem 0.5rem; border-radius: 4px; min-width: 70px; text-align: center;
|
|
554
|
-
}
|
|
555
|
-
.rec-critical .rec-priority { background: rgba(255,68,68,0.2); color: var(--red); }
|
|
556
|
-
.rec-high .rec-priority { background: rgba(255,149,0,0.2); color: var(--orange); }
|
|
557
|
-
.rec-medium .rec-priority { background: rgba(255,217,61,0.2); color: var(--yellow); }
|
|
558
|
-
.rec-low .rec-priority { background: rgba(136,136,170,0.15); color: var(--text-dim); }
|
|
559
|
-
.rec-text { font-size: 0.9rem; }
|
|
560
|
-
.rec-text code { background: var(--bg); padding: 0.15rem 0.4rem; border-radius: 4px; font-family: var(--mono); color: var(--cyan); font-size: 0.8rem; }
|
|
561
|
-
.rec-clean { background: var(--surface); border: 1px solid var(--green); border-radius: 8px; padding: 1.5rem; text-align: center; color: var(--green); }
|
|
562
|
-
|
|
563
|
-
/* Footer */
|
|
564
|
-
.footer {
|
|
565
|
-
display: flex; justify-content: space-between; align-items: center;
|
|
566
|
-
padding: 1.5rem 2rem; border-top: 1px solid var(--border);
|
|
567
|
-
color: var(--text-dim); font-size: 0.8rem; margin-top: 2rem;
|
|
568
|
-
}
|
|
569
|
-
.footer-brand { display: flex; align-items: center; gap: 1.5rem; }
|
|
570
|
-
.footer-brand strong { color: var(--cyan); }
|
|
571
|
-
.footer-link { color: var(--cyan); }
|
|
572
|
-
|
|
573
|
-
/* Responsive */
|
|
574
|
-
@media (max-width: 900px) {
|
|
575
|
-
.category-grid { grid-template-columns: repeat(2, 1fr); }
|
|
576
|
-
.fix-grid { grid-template-columns: repeat(2, 1fr); }
|
|
577
|
-
.mc-grid { grid-template-columns: 1fr; }
|
|
578
|
-
.header { flex-direction: column; gap: 1rem; }
|
|
579
|
-
.score-section { flex-direction: column; text-align: center; }
|
|
580
|
-
.score-summary { justify-content: center; }
|
|
581
|
-
}
|
|
582
|
-
@media (max-width: 600px) {
|
|
583
|
-
.category-grid { grid-template-columns: 1fr; }
|
|
584
|
-
.fix-grid { grid-template-columns: 1fr; }
|
|
585
|
-
.deps-grid { grid-template-columns: 1fr; }
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/* Print */
|
|
589
|
-
@media print {
|
|
590
|
-
body { background: #fff; color: #111; }
|
|
591
|
-
.container { max-width: 100%; }
|
|
592
|
-
.header, .category-card, .mother-code-section, .fixable-section, .issues-section, .dep-card, .rec-item {
|
|
593
|
-
background: #fafafa; border-color: #ddd;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
`;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
_js() {
|
|
600
|
-
return `
|
|
601
|
-
// Animate score ring on load
|
|
602
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
603
|
-
const progress = document.querySelector('.score-progress');
|
|
604
|
-
if (progress) {
|
|
605
|
-
const final = progress.style.strokeDashoffset || progress.getAttribute('stroke-dashoffset');
|
|
606
|
-
progress.style.strokeDashoffset = progress.getAttribute('stroke-dasharray');
|
|
607
|
-
requestAnimationFrame(() => {
|
|
608
|
-
requestAnimationFrame(() => {
|
|
609
|
-
progress.style.strokeDashoffset = final;
|
|
610
|
-
});
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
`;
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// ─── Utilities ──────────────────────────────────────────
|
|
618
|
-
|
|
619
|
-
_scoreColor(score) {
|
|
620
|
-
if (score >= 80) return '#00ff88';
|
|
621
|
-
if (score >= 60) return '#ffd93d';
|
|
622
|
-
return '#ff4444';
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
_esc(str) {
|
|
626
|
-
return String(str || '')
|
|
627
|
-
.replace(/&/g, '&')
|
|
628
|
-
.replace(/</g, '<')
|
|
629
|
-
.replace(/>/g, '>')
|
|
630
|
-
.replace(/"/g, '"');
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
module.exports = HtmlReportGenerator;
|