@stati/core 1.20.3 → 1.22.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/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +49 -4
- package/dist/core/build.d.ts +6 -0
- package/dist/core/build.d.ts.map +1 -1
- package/dist/core/build.js +176 -66
- package/dist/core/dev.d.ts.map +1 -1
- package/dist/core/dev.js +100 -28
- package/dist/core/isg/builder.d.ts +4 -1
- package/dist/core/isg/builder.d.ts.map +1 -1
- package/dist/core/isg/builder.js +89 -2
- package/dist/core/isg/deps.d.ts +5 -0
- package/dist/core/isg/deps.d.ts.map +1 -1
- package/dist/core/isg/deps.js +38 -3
- package/dist/core/isg/dev-server-lock.d.ts +85 -0
- package/dist/core/isg/dev-server-lock.d.ts.map +1 -0
- package/dist/core/isg/dev-server-lock.js +248 -0
- package/dist/core/isg/hash.d.ts +4 -0
- package/dist/core/isg/hash.d.ts.map +1 -1
- package/dist/core/isg/hash.js +24 -1
- package/dist/core/isg/index.d.ts +3 -2
- package/dist/core/isg/index.d.ts.map +1 -1
- package/dist/core/isg/index.js +3 -2
- package/dist/core/markdown.d.ts +6 -0
- package/dist/core/markdown.d.ts.map +1 -1
- package/dist/core/markdown.js +23 -0
- package/dist/core/preview.js +1 -1
- package/dist/core/templates.js +5 -5
- package/dist/core/utils/bundle-matching.utils.d.ts +2 -0
- package/dist/core/utils/bundle-matching.utils.d.ts.map +1 -1
- package/dist/core/utils/index.d.ts +1 -1
- package/dist/core/utils/index.d.ts.map +1 -1
- package/dist/core/utils/index.js +1 -1
- package/dist/core/utils/logger.utils.d.ts.map +1 -1
- package/dist/core/utils/logger.utils.js +1 -0
- package/dist/core/utils/partial-validation.utils.js +2 -2
- package/dist/core/utils/paths.utils.d.ts +18 -0
- package/dist/core/utils/paths.utils.d.ts.map +1 -1
- package/dist/core/utils/paths.utils.js +23 -0
- package/dist/core/utils/tailwind-inventory.utils.d.ts +1 -16
- package/dist/core/utils/tailwind-inventory.utils.d.ts.map +1 -1
- package/dist/core/utils/tailwind-inventory.utils.js +35 -3
- package/dist/core/utils/typescript.utils.d.ts +13 -0
- package/dist/core/utils/typescript.utils.d.ts.map +1 -1
- package/dist/core/utils/typescript.utils.js +82 -3
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +51 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/metrics/index.d.ts +1 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +2 -0
- package/dist/metrics/recorder.d.ts.map +1 -1
- package/dist/metrics/types.d.ts +31 -0
- package/dist/metrics/types.d.ts.map +1 -1
- package/dist/metrics/utils/html-report.utils.d.ts +24 -0
- package/dist/metrics/utils/html-report.utils.d.ts.map +1 -0
- package/dist/metrics/utils/html-report.utils.js +1547 -0
- package/dist/metrics/utils/index.d.ts +1 -0
- package/dist/metrics/utils/index.d.ts.map +1 -1
- package/dist/metrics/utils/index.js +2 -0
- package/dist/metrics/utils/writer.utils.d.ts +6 -2
- package/dist/metrics/utils/writer.utils.d.ts.map +1 -1
- package/dist/metrics/utils/writer.utils.js +20 -4
- package/dist/search/generator.d.ts +1 -9
- package/dist/search/generator.d.ts.map +1 -1
- package/dist/search/generator.js +26 -2
- package/dist/seo/generator.d.ts.map +1 -1
- package/dist/seo/generator.js +1 -0
- package/dist/seo/utils/escape-and-validation.utils.d.ts.map +1 -1
- package/dist/seo/utils/escape-and-validation.utils.js +1 -16
- package/dist/types/logging.d.ts +31 -12
- package/dist/types/logging.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,1547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Metrics Report Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates a self-contained HTML file for visualizing build performance metrics.
|
|
5
|
+
* Handles both standard and detailed (with pageTimings) metrics files.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFile } from 'node:fs/promises';
|
|
8
|
+
/**
|
|
9
|
+
* Format bytes to human-readable string
|
|
10
|
+
*/
|
|
11
|
+
function formatBytes(bytes) {
|
|
12
|
+
if (bytes === 0)
|
|
13
|
+
return '0 B';
|
|
14
|
+
const k = 1024;
|
|
15
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
16
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
17
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Format milliseconds to human-readable duration
|
|
21
|
+
*/
|
|
22
|
+
function formatDuration(ms) {
|
|
23
|
+
if (ms < 1)
|
|
24
|
+
return `${(ms * 1000).toFixed(0)}μs`;
|
|
25
|
+
if (ms < 1000)
|
|
26
|
+
return `${ms.toFixed(2)}ms`;
|
|
27
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Format percentage
|
|
31
|
+
*/
|
|
32
|
+
function formatPercent(value) {
|
|
33
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get human-readable phase name
|
|
37
|
+
*/
|
|
38
|
+
function getPhaseLabel(key) {
|
|
39
|
+
const labels = {
|
|
40
|
+
configLoadMs: 'Config Loading',
|
|
41
|
+
contentDiscoveryMs: 'Content Discovery',
|
|
42
|
+
navigationBuildMs: 'Navigation Build',
|
|
43
|
+
cacheManifestLoadMs: 'Cache Manifest Load',
|
|
44
|
+
typescriptCompileMs: 'TypeScript Compile',
|
|
45
|
+
pageRenderingMs: 'Page Rendering',
|
|
46
|
+
shouldRebuildTotalMs: 'Rebuild Checks',
|
|
47
|
+
renderPageTotalMs: 'Render Pages',
|
|
48
|
+
fileWriteTotalMs: 'File Writing',
|
|
49
|
+
cacheEntryTotalMs: 'Cache Entry Updates',
|
|
50
|
+
searchIndexGenerationMs: 'Search Index Generation',
|
|
51
|
+
searchIndexWriteMs: 'Search Index Write',
|
|
52
|
+
assetCopyMs: 'Asset Copying',
|
|
53
|
+
cacheManifestSaveMs: 'Cache Manifest Save',
|
|
54
|
+
sitemapGenerationMs: 'Sitemap Generation',
|
|
55
|
+
rssGenerationMs: 'RSS Generation',
|
|
56
|
+
tailwindInitMs: 'Tailwind Init',
|
|
57
|
+
tailwindInventoryMs: 'Tailwind Inventory',
|
|
58
|
+
getDirectorySizeMs: 'Directory Size Calc',
|
|
59
|
+
lockAcquireMs: 'Lock Acquire',
|
|
60
|
+
lockReleaseMs: 'Lock Release',
|
|
61
|
+
hookBeforeAllMs: 'Hook: beforeAll',
|
|
62
|
+
hookAfterAllMs: 'Hook: afterAll',
|
|
63
|
+
hookBeforeRenderTotalMs: 'Hook: beforeRender (total)',
|
|
64
|
+
hookAfterRenderTotalMs: 'Hook: afterRender (total)',
|
|
65
|
+
};
|
|
66
|
+
return (labels[key] ||
|
|
67
|
+
key
|
|
68
|
+
.replace(/Ms$/, '')
|
|
69
|
+
.replace(/([A-Z])/g, ' $1')
|
|
70
|
+
.trim());
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Get detailed description for each phase
|
|
74
|
+
*/
|
|
75
|
+
function getPhaseDescription(key) {
|
|
76
|
+
const descriptions = {
|
|
77
|
+
configLoadMs: 'Time spent loading and validating the stati.config.ts configuration file. This includes parsing the config, resolving paths, and setting up the build context. A slow config load may indicate complex config logic or slow dynamic imports.',
|
|
78
|
+
contentDiscoveryMs: 'Time spent scanning the site directory for markdown files and extracting frontmatter metadata. This includes parsing YAML frontmatter, building the page list, and collecting tags/categories. Large sites with many files will have longer discovery times.',
|
|
79
|
+
navigationBuildMs: 'Time spent constructing the navigation tree from discovered pages. This includes sorting pages, building parent-child relationships, and generating breadcrumbs. Complex nested structures may increase this time.',
|
|
80
|
+
cacheManifestLoadMs: 'Time spent reading the ISG cache manifest from disk. The manifest tracks which pages are cached and their dependencies. A large manifest or slow disk I/O can increase this time.',
|
|
81
|
+
typescriptCompileMs: 'Time spent compiling TypeScript bundles using esbuild. This includes bundling client-side scripts defined in the config. More bundles or larger codebases will increase compile time.',
|
|
82
|
+
pageRenderingMs: 'Total time spent in the page rendering pipeline, including rebuild checks, template rendering, file writing, and cache updates. This is typically the largest phase in a build.',
|
|
83
|
+
shouldRebuildTotalMs: 'Aggregate time spent checking if each page needs to be rebuilt. This includes computing content hashes and comparing dependencies. Fast hash comparisons keep this time low.',
|
|
84
|
+
renderPageTotalMs: 'Aggregate time spent actually rendering pages through the Eta template engine. Complex templates with many partials or heavy computations will increase this time.',
|
|
85
|
+
fileWriteTotalMs: 'Aggregate time spent writing rendered HTML files to the dist directory. Slow disk I/O or antivirus scanning can increase this time significantly.',
|
|
86
|
+
cacheEntryTotalMs: 'Aggregate time spent updating the ISG cache manifest with new page entries. This includes serializing page metadata and dependency information.',
|
|
87
|
+
searchIndexGenerationMs: 'Time spent generating the search index from rendered page content. This includes extracting text, building the index structure, and computing relevance scores.',
|
|
88
|
+
searchIndexWriteMs: 'Time spent writing the search index JSON file to disk. Large indices with many documents will take longer to serialize and write.',
|
|
89
|
+
assetCopyMs: 'Time spent copying static assets from the public directory to dist. Many files or large files will increase copy time. File watching may add overhead in dev mode.',
|
|
90
|
+
cacheManifestSaveMs: 'Time spent persisting the updated cache manifest to disk. This happens at the end of every build to preserve ISG state for subsequent builds.',
|
|
91
|
+
sitemapGenerationMs: 'Time spent generating the sitemap.xml file. This includes formatting URLs, setting priorities, and writing the XML structure.',
|
|
92
|
+
rssGenerationMs: 'Time spent generating RSS/Atom feed files. This includes sorting posts by date, formatting content, and writing XML.',
|
|
93
|
+
tailwindInitMs: 'Time spent initializing Tailwind CSS detection and setting up the class inventory system. This is a one-time cost at the start of each build.',
|
|
94
|
+
tailwindInventoryMs: 'Time spent writing the Tailwind class inventory file at the end of the build. This file tracks which classes were used for purging unused CSS.',
|
|
95
|
+
getDirectorySizeMs: 'Time spent calculating the total size of the dist directory after the build completes. This is used for the build summary statistics.',
|
|
96
|
+
lockAcquireMs: 'Time spent acquiring the build lock to prevent concurrent builds. Lock contention can increase this time if another build is in progress.',
|
|
97
|
+
lockReleaseMs: 'Time spent releasing the build lock after the build completes. This should be nearly instantaneous.',
|
|
98
|
+
hookBeforeAllMs: 'Time spent executing the beforeAll hook defined in your config. Long-running setup code will directly impact this time.',
|
|
99
|
+
hookAfterAllMs: 'Time spent executing the afterAll hook defined in your config. Post-build processing like compression or uploads will show here.',
|
|
100
|
+
hookBeforeRenderTotalMs: 'Aggregate time spent in beforeRender hooks across all pages. Per-page setup logic accumulates here.',
|
|
101
|
+
hookAfterRenderTotalMs: 'Aggregate time spent in afterRender hooks across all pages. Per-page post-processing accumulates here.',
|
|
102
|
+
};
|
|
103
|
+
return descriptions[key] || 'No description available for this phase.';
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Define the canonical execution order of phases
|
|
107
|
+
*/
|
|
108
|
+
const PHASE_EXECUTION_ORDER = [
|
|
109
|
+
'lockAcquireMs',
|
|
110
|
+
'configLoadMs',
|
|
111
|
+
'tailwindInitMs',
|
|
112
|
+
'cacheManifestLoadMs',
|
|
113
|
+
'contentDiscoveryMs',
|
|
114
|
+
'navigationBuildMs',
|
|
115
|
+
'hookBeforeAllMs',
|
|
116
|
+
'typescriptCompileMs',
|
|
117
|
+
'shouldRebuildTotalMs',
|
|
118
|
+
'hookBeforeRenderTotalMs',
|
|
119
|
+
'renderPageTotalMs',
|
|
120
|
+
'hookAfterRenderTotalMs',
|
|
121
|
+
'fileWriteTotalMs',
|
|
122
|
+
'cacheEntryTotalMs',
|
|
123
|
+
'pageRenderingMs',
|
|
124
|
+
'searchIndexGenerationMs',
|
|
125
|
+
'searchIndexWriteMs',
|
|
126
|
+
'tailwindInventoryMs',
|
|
127
|
+
'cacheManifestSaveMs',
|
|
128
|
+
'assetCopyMs',
|
|
129
|
+
'sitemapGenerationMs',
|
|
130
|
+
'rssGenerationMs',
|
|
131
|
+
'hookAfterAllMs',
|
|
132
|
+
'getDirectorySizeMs',
|
|
133
|
+
'lockReleaseMs',
|
|
134
|
+
];
|
|
135
|
+
/**
|
|
136
|
+
* Generate phase breakdown data sorted by duration
|
|
137
|
+
*/
|
|
138
|
+
function generatePhaseData(phases) {
|
|
139
|
+
return Object.entries(phases)
|
|
140
|
+
.filter(([, value]) => value !== undefined && value > 0)
|
|
141
|
+
.map(([key, value]) => ({
|
|
142
|
+
name: key,
|
|
143
|
+
value: value,
|
|
144
|
+
label: getPhaseLabel(key),
|
|
145
|
+
description: getPhaseDescription(key),
|
|
146
|
+
}))
|
|
147
|
+
.sort((a, b) => b.value - a.value);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Generate phase data in execution order for timeline view
|
|
151
|
+
*/
|
|
152
|
+
function generateTimelineData(phases) {
|
|
153
|
+
const activePhases = Object.entries(phases)
|
|
154
|
+
.filter(([, value]) => value !== undefined && value > 0)
|
|
155
|
+
.map(([key, value]) => ({
|
|
156
|
+
name: key,
|
|
157
|
+
value: value,
|
|
158
|
+
label: getPhaseLabel(key),
|
|
159
|
+
description: getPhaseDescription(key),
|
|
160
|
+
order: PHASE_EXECUTION_ORDER.indexOf(key),
|
|
161
|
+
}));
|
|
162
|
+
// Sort by execution order, unknown phases go to the end
|
|
163
|
+
return activePhases.sort((a, b) => {
|
|
164
|
+
const orderA = a.order === -1 ? 999 : a.order;
|
|
165
|
+
const orderB = b.order === -1 ? 999 : b.order;
|
|
166
|
+
return orderA - orderB;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Generate the HTML report content
|
|
171
|
+
*/
|
|
172
|
+
export function generateMetricsHtml(metrics) {
|
|
173
|
+
const { meta, totals, phases, counts, isg, pageTimings, incremental } = metrics;
|
|
174
|
+
const phaseData = generatePhaseData(phases);
|
|
175
|
+
const timelineData = generateTimelineData(phases);
|
|
176
|
+
const totalPhaseTime = phaseData.reduce((sum, p) => sum + p.value, 0);
|
|
177
|
+
const hasDetailedTimings = pageTimings && pageTimings.length > 0;
|
|
178
|
+
const hasIncremental = !!incremental;
|
|
179
|
+
// Calculate phase percentages for the chart
|
|
180
|
+
const phaseChartData = phaseData.map((p) => ({
|
|
181
|
+
...p,
|
|
182
|
+
percent: (p.value / totals.durationMs) * 100,
|
|
183
|
+
}));
|
|
184
|
+
// Calculate timeline data with cumulative offsets for visualization
|
|
185
|
+
let cumulativeTime = 0;
|
|
186
|
+
const timelineChartData = timelineData.map((p) => {
|
|
187
|
+
const item = {
|
|
188
|
+
...p,
|
|
189
|
+
percent: (p.value / totals.durationMs) * 100,
|
|
190
|
+
startOffset: (cumulativeTime / totals.durationMs) * 100,
|
|
191
|
+
};
|
|
192
|
+
cumulativeTime += p.value;
|
|
193
|
+
return item;
|
|
194
|
+
});
|
|
195
|
+
// Sort page timings by duration for detailed view
|
|
196
|
+
const sortedPageTimings = hasDetailedTimings
|
|
197
|
+
? [...pageTimings].sort((a, b) => b.durationMs - a.durationMs)
|
|
198
|
+
: [];
|
|
199
|
+
// Generate JSON for embedding
|
|
200
|
+
const jsonData = JSON.stringify(metrics, null, 2);
|
|
201
|
+
return `<!DOCTYPE html>
|
|
202
|
+
<html lang="en">
|
|
203
|
+
<head>
|
|
204
|
+
<meta charset="UTF-8">
|
|
205
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
206
|
+
<title>Stati Build Metrics Report - ${meta.timestamp}</title>
|
|
207
|
+
<style>
|
|
208
|
+
:root {
|
|
209
|
+
--bg-primary: #0d1117;
|
|
210
|
+
--bg-secondary: #161b22;
|
|
211
|
+
--bg-tertiary: #21262d;
|
|
212
|
+
--border-color: #30363d;
|
|
213
|
+
--text-primary: #e6edf3;
|
|
214
|
+
--text-secondary: #8b949e;
|
|
215
|
+
--text-muted: #6e7681;
|
|
216
|
+
--accent-blue: #58a6ff;
|
|
217
|
+
--accent-green: #3fb950;
|
|
218
|
+
--accent-yellow: #d29922;
|
|
219
|
+
--accent-red: #f85149;
|
|
220
|
+
--accent-purple: #a371f7;
|
|
221
|
+
--accent-cyan: #39c5cf;
|
|
222
|
+
--accent-orange: #db6d28;
|
|
223
|
+
--shadow: 0 3px 6px rgba(0,0,0,0.4);
|
|
224
|
+
--radius: 8px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
* {
|
|
228
|
+
box-sizing: border-box;
|
|
229
|
+
margin: 0;
|
|
230
|
+
padding: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
body {
|
|
234
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
235
|
+
background: var(--bg-primary);
|
|
236
|
+
color: var(--text-primary);
|
|
237
|
+
line-height: 1.5;
|
|
238
|
+
min-height: 100vh;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.container {
|
|
242
|
+
max-width: 1400px;
|
|
243
|
+
margin: 0 auto;
|
|
244
|
+
padding: 24px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Header */
|
|
248
|
+
header {
|
|
249
|
+
background: var(--bg-secondary);
|
|
250
|
+
border: 1px solid var(--border-color);
|
|
251
|
+
border-radius: var(--radius);
|
|
252
|
+
padding: 24px;
|
|
253
|
+
margin-bottom: 24px;
|
|
254
|
+
box-shadow: var(--shadow);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
header h1 {
|
|
258
|
+
font-size: 24px;
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
margin-bottom: 8px;
|
|
261
|
+
display: flex;
|
|
262
|
+
align-items: center;
|
|
263
|
+
gap: 12px;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
header h1 svg {
|
|
267
|
+
width: 32px;
|
|
268
|
+
height: 32px;
|
|
269
|
+
color: var(--accent-blue);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.meta-info {
|
|
273
|
+
display: flex;
|
|
274
|
+
flex-wrap: wrap;
|
|
275
|
+
gap: 16px 32px;
|
|
276
|
+
margin-top: 16px;
|
|
277
|
+
font-size: 13px;
|
|
278
|
+
color: var(--text-secondary);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.meta-info span {
|
|
282
|
+
display: flex;
|
|
283
|
+
align-items: center;
|
|
284
|
+
gap: 6px;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.meta-info svg {
|
|
288
|
+
width: 14px;
|
|
289
|
+
height: 14px;
|
|
290
|
+
opacity: 0.7;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* Summary Cards */
|
|
294
|
+
.summary-grid {
|
|
295
|
+
display: grid;
|
|
296
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
297
|
+
gap: 16px;
|
|
298
|
+
margin-bottom: 24px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.summary-card {
|
|
302
|
+
background: var(--bg-secondary);
|
|
303
|
+
border: 1px solid var(--border-color);
|
|
304
|
+
border-radius: var(--radius);
|
|
305
|
+
padding: 20px;
|
|
306
|
+
box-shadow: var(--shadow);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.summary-card .label {
|
|
310
|
+
font-size: 12px;
|
|
311
|
+
font-weight: 500;
|
|
312
|
+
color: var(--text-secondary);
|
|
313
|
+
text-transform: uppercase;
|
|
314
|
+
letter-spacing: 0.5px;
|
|
315
|
+
margin-bottom: 8px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.summary-card .value {
|
|
319
|
+
font-size: 28px;
|
|
320
|
+
font-weight: 600;
|
|
321
|
+
color: var(--text-primary);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.summary-card .subtext {
|
|
325
|
+
font-size: 12px;
|
|
326
|
+
color: var(--text-muted);
|
|
327
|
+
margin-top: 4px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.summary-card.highlight {
|
|
331
|
+
border-color: var(--accent-blue);
|
|
332
|
+
background: linear-gradient(135deg, var(--bg-secondary) 0%, rgba(88, 166, 255, 0.1) 100%);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.summary-card.success .value {
|
|
336
|
+
color: var(--accent-green);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.summary-card.warning .value {
|
|
340
|
+
color: var(--accent-yellow);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* Section */
|
|
344
|
+
.section {
|
|
345
|
+
background: var(--bg-secondary);
|
|
346
|
+
border: 1px solid var(--border-color);
|
|
347
|
+
border-radius: var(--radius);
|
|
348
|
+
margin-bottom: 24px;
|
|
349
|
+
box-shadow: var(--shadow);
|
|
350
|
+
overflow: hidden;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.section-header {
|
|
354
|
+
display: flex;
|
|
355
|
+
align-items: center;
|
|
356
|
+
justify-content: space-between;
|
|
357
|
+
padding: 16px 20px;
|
|
358
|
+
border-bottom: 1px solid var(--border-color);
|
|
359
|
+
background: var(--bg-tertiary);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.section-header h2 {
|
|
363
|
+
font-size: 16px;
|
|
364
|
+
font-weight: 600;
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 10px;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.section-header svg {
|
|
371
|
+
width: 18px;
|
|
372
|
+
height: 18px;
|
|
373
|
+
color: var(--accent-blue);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.section-content {
|
|
377
|
+
padding: 20px;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Phase Breakdown */
|
|
381
|
+
.phase-list {
|
|
382
|
+
display: flex;
|
|
383
|
+
flex-direction: column;
|
|
384
|
+
gap: 8px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.phase-item {
|
|
388
|
+
display: grid;
|
|
389
|
+
grid-template-columns: 200px 1fr 100px 80px 32px;
|
|
390
|
+
align-items: center;
|
|
391
|
+
gap: 16px;
|
|
392
|
+
padding: 12px 16px;
|
|
393
|
+
border-radius: 8px;
|
|
394
|
+
background: var(--bg-tertiary);
|
|
395
|
+
cursor: pointer;
|
|
396
|
+
transition: all 0.2s ease;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
.phase-item:hover {
|
|
400
|
+
background: var(--bg-secondary);
|
|
401
|
+
box-shadow: 0 0 0 1px var(--border-color);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.phase-item.expanded {
|
|
405
|
+
background: var(--bg-secondary);
|
|
406
|
+
box-shadow: 0 0 0 1px var(--accent-blue);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.phase-name {
|
|
410
|
+
font-size: 14px;
|
|
411
|
+
font-weight: 500;
|
|
412
|
+
color: var(--text-primary);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.phase-bar-container {
|
|
416
|
+
height: 24px;
|
|
417
|
+
background: rgba(0,0,0,0.3);
|
|
418
|
+
border-radius: 4px;
|
|
419
|
+
overflow: hidden;
|
|
420
|
+
position: relative;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.phase-bar {
|
|
424
|
+
height: 100%;
|
|
425
|
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-cyan));
|
|
426
|
+
border-radius: 4px;
|
|
427
|
+
transition: width 0.3s ease;
|
|
428
|
+
min-width: 2px;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.phase-duration {
|
|
432
|
+
font-size: 14px;
|
|
433
|
+
font-weight: 600;
|
|
434
|
+
color: var(--text-primary);
|
|
435
|
+
text-align: right;
|
|
436
|
+
font-variant-numeric: tabular-nums;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.phase-percent {
|
|
440
|
+
font-size: 13px;
|
|
441
|
+
color: var(--text-secondary);
|
|
442
|
+
text-align: right;
|
|
443
|
+
font-variant-numeric: tabular-nums;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.phase-expand-icon {
|
|
447
|
+
width: 20px;
|
|
448
|
+
height: 20px;
|
|
449
|
+
color: var(--text-muted);
|
|
450
|
+
transition: transform 0.2s ease;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.phase-item.expanded .phase-expand-icon {
|
|
454
|
+
transform: rotate(180deg);
|
|
455
|
+
color: var(--accent-blue);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.phase-description {
|
|
459
|
+
grid-column: 1 / -1;
|
|
460
|
+
padding: 16px;
|
|
461
|
+
margin-top: 8px;
|
|
462
|
+
background: var(--bg-primary);
|
|
463
|
+
border-radius: 6px;
|
|
464
|
+
font-size: 13px;
|
|
465
|
+
line-height: 1.6;
|
|
466
|
+
color: var(--text-secondary);
|
|
467
|
+
display: none;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.phase-item.expanded .phase-description {
|
|
471
|
+
display: block;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.phase-description code {
|
|
475
|
+
background: var(--bg-tertiary);
|
|
476
|
+
padding: 2px 6px;
|
|
477
|
+
border-radius: 4px;
|
|
478
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
479
|
+
font-size: 12px;
|
|
480
|
+
color: var(--accent-cyan);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/* Timeline View */
|
|
484
|
+
.timeline-container {
|
|
485
|
+
position: relative;
|
|
486
|
+
padding: 20px 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.timeline-track {
|
|
490
|
+
height: 40px;
|
|
491
|
+
background: var(--bg-tertiary);
|
|
492
|
+
border-radius: 8px;
|
|
493
|
+
position: relative;
|
|
494
|
+
overflow: hidden;
|
|
495
|
+
margin-bottom: 16px;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.timeline-segment {
|
|
499
|
+
position: absolute;
|
|
500
|
+
height: 100%;
|
|
501
|
+
display: flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
justify-content: center;
|
|
504
|
+
font-size: 10px;
|
|
505
|
+
font-weight: 500;
|
|
506
|
+
color: white;
|
|
507
|
+
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
transition: filter 0.2s ease;
|
|
511
|
+
min-width: 2px;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.timeline-segment:hover {
|
|
515
|
+
filter: brightness(1.2);
|
|
516
|
+
z-index: 10;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.timeline-segment span {
|
|
520
|
+
white-space: nowrap;
|
|
521
|
+
overflow: hidden;
|
|
522
|
+
text-overflow: ellipsis;
|
|
523
|
+
padding: 0 4px;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.timeline-list {
|
|
527
|
+
display: flex;
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
gap: 6px;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.timeline-item {
|
|
533
|
+
display: grid;
|
|
534
|
+
grid-template-columns: 32px 180px 1fr 100px;
|
|
535
|
+
align-items: center;
|
|
536
|
+
gap: 12px;
|
|
537
|
+
padding: 10px 12px;
|
|
538
|
+
border-radius: 6px;
|
|
539
|
+
background: var(--bg-tertiary);
|
|
540
|
+
cursor: pointer;
|
|
541
|
+
transition: all 0.2s ease;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.timeline-item:hover {
|
|
545
|
+
background: var(--bg-secondary);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.timeline-item.expanded {
|
|
549
|
+
background: var(--bg-secondary);
|
|
550
|
+
box-shadow: 0 0 0 1px var(--accent-blue);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.timeline-order {
|
|
554
|
+
width: 24px;
|
|
555
|
+
height: 24px;
|
|
556
|
+
border-radius: 50%;
|
|
557
|
+
background: var(--bg-primary);
|
|
558
|
+
display: flex;
|
|
559
|
+
align-items: center;
|
|
560
|
+
justify-content: center;
|
|
561
|
+
font-size: 11px;
|
|
562
|
+
font-weight: 600;
|
|
563
|
+
color: var(--text-secondary);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.timeline-item:nth-child(1) .timeline-order { background: var(--accent-blue); color: white; }
|
|
567
|
+
.timeline-item:nth-child(2) .timeline-order { background: var(--accent-cyan); color: white; }
|
|
568
|
+
.timeline-item:nth-child(3) .timeline-order { background: var(--accent-green); color: white; }
|
|
569
|
+
|
|
570
|
+
.timeline-name {
|
|
571
|
+
font-size: 13px;
|
|
572
|
+
font-weight: 500;
|
|
573
|
+
color: var(--text-primary);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.timeline-bar-container {
|
|
577
|
+
height: 20px;
|
|
578
|
+
background: rgba(0,0,0,0.3);
|
|
579
|
+
border-radius: 4px;
|
|
580
|
+
overflow: hidden;
|
|
581
|
+
position: relative;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.timeline-bar {
|
|
585
|
+
height: 100%;
|
|
586
|
+
border-radius: 4px;
|
|
587
|
+
min-width: 2px;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.timeline-time {
|
|
591
|
+
font-size: 13px;
|
|
592
|
+
font-weight: 600;
|
|
593
|
+
color: var(--text-primary);
|
|
594
|
+
text-align: right;
|
|
595
|
+
font-variant-numeric: tabular-nums;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.timeline-description {
|
|
599
|
+
grid-column: 1 / -1;
|
|
600
|
+
padding: 12px;
|
|
601
|
+
margin-top: 8px;
|
|
602
|
+
background: var(--bg-primary);
|
|
603
|
+
border-radius: 6px;
|
|
604
|
+
font-size: 12px;
|
|
605
|
+
line-height: 1.6;
|
|
606
|
+
color: var(--text-secondary);
|
|
607
|
+
display: none;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
.timeline-item.expanded .timeline-description {
|
|
611
|
+
display: block;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/* Phase colors for timeline */
|
|
615
|
+
.phase-color-0 { background: linear-gradient(90deg, #58a6ff, #388bfd); }
|
|
616
|
+
.phase-color-1 { background: linear-gradient(90deg, #39c5cf, #2ea9b3); }
|
|
617
|
+
.phase-color-2 { background: linear-gradient(90deg, #3fb950, #2ea043); }
|
|
618
|
+
.phase-color-3 { background: linear-gradient(90deg, #a371f7, #8957e5); }
|
|
619
|
+
.phase-color-4 { background: linear-gradient(90deg, #d29922, #bb8009); }
|
|
620
|
+
.phase-color-5 { background: linear-gradient(90deg, #f85149, #da3633); }
|
|
621
|
+
.phase-color-6 { background: linear-gradient(90deg, #db6d28, #bd561d); }
|
|
622
|
+
.phase-color-7 { background: linear-gradient(90deg, #8b949e, #6e7681); }
|
|
623
|
+
|
|
624
|
+
/* Waterfall Chart */
|
|
625
|
+
.waterfall-container {
|
|
626
|
+
overflow-x: auto;
|
|
627
|
+
margin-top: 16px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.waterfall {
|
|
631
|
+
min-width: 600px;
|
|
632
|
+
position: relative;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.waterfall-row {
|
|
636
|
+
display: flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
height: 32px;
|
|
639
|
+
gap: 12px;
|
|
640
|
+
border-bottom: 1px solid var(--border-color);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.waterfall-row:last-child {
|
|
644
|
+
border-bottom: none;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.waterfall-label {
|
|
648
|
+
width: 160px;
|
|
649
|
+
flex-shrink: 0;
|
|
650
|
+
font-size: 12px;
|
|
651
|
+
color: var(--text-secondary);
|
|
652
|
+
overflow: hidden;
|
|
653
|
+
text-overflow: ellipsis;
|
|
654
|
+
white-space: nowrap;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.waterfall-track {
|
|
658
|
+
flex: 1;
|
|
659
|
+
height: 16px;
|
|
660
|
+
background: var(--bg-tertiary);
|
|
661
|
+
border-radius: 2px;
|
|
662
|
+
position: relative;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
.waterfall-bar {
|
|
666
|
+
position: absolute;
|
|
667
|
+
height: 100%;
|
|
668
|
+
border-radius: 2px;
|
|
669
|
+
min-width: 3px;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.waterfall-bar.cached {
|
|
673
|
+
background: var(--accent-green);
|
|
674
|
+
opacity: 0.6;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.waterfall-bar.rendered {
|
|
678
|
+
background: var(--accent-blue);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.waterfall-time {
|
|
682
|
+
width: 80px;
|
|
683
|
+
flex-shrink: 0;
|
|
684
|
+
font-size: 12px;
|
|
685
|
+
color: var(--text-secondary);
|
|
686
|
+
text-align: right;
|
|
687
|
+
font-variant-numeric: tabular-nums;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* Stats Grid */
|
|
691
|
+
.stats-grid {
|
|
692
|
+
display: grid;
|
|
693
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
694
|
+
gap: 16px;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
.stat-item {
|
|
698
|
+
display: flex;
|
|
699
|
+
flex-direction: column;
|
|
700
|
+
gap: 4px;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
.stat-label {
|
|
704
|
+
font-size: 12px;
|
|
705
|
+
color: var(--text-secondary);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
.stat-value {
|
|
709
|
+
font-size: 18px;
|
|
710
|
+
font-weight: 600;
|
|
711
|
+
color: var(--text-primary);
|
|
712
|
+
font-variant-numeric: tabular-nums;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/* ISG Section */
|
|
716
|
+
.isg-grid {
|
|
717
|
+
display: grid;
|
|
718
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
719
|
+
gap: 20px;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.isg-stat {
|
|
723
|
+
text-align: center;
|
|
724
|
+
padding: 20px;
|
|
725
|
+
background: var(--bg-tertiary);
|
|
726
|
+
border-radius: var(--radius);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.isg-stat .value {
|
|
730
|
+
font-size: 32px;
|
|
731
|
+
font-weight: 700;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.isg-stat .label {
|
|
735
|
+
font-size: 13px;
|
|
736
|
+
color: var(--text-secondary);
|
|
737
|
+
margin-top: 4px;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.isg-stat.hit .value {
|
|
741
|
+
color: var(--accent-green);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.isg-stat.miss .value {
|
|
745
|
+
color: var(--accent-yellow);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/* Cache Hit Rate Ring */
|
|
749
|
+
.cache-ring-container {
|
|
750
|
+
display: flex;
|
|
751
|
+
justify-content: center;
|
|
752
|
+
align-items: center;
|
|
753
|
+
padding: 20px;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.cache-ring {
|
|
757
|
+
position: relative;
|
|
758
|
+
width: 150px;
|
|
759
|
+
height: 150px;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
.cache-ring svg {
|
|
763
|
+
transform: rotate(-90deg);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.cache-ring-bg {
|
|
767
|
+
fill: none;
|
|
768
|
+
stroke: var(--bg-tertiary);
|
|
769
|
+
stroke-width: 12;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.cache-ring-progress {
|
|
773
|
+
fill: none;
|
|
774
|
+
stroke: var(--accent-green);
|
|
775
|
+
stroke-width: 12;
|
|
776
|
+
stroke-linecap: round;
|
|
777
|
+
transition: stroke-dashoffset 0.5s ease;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.cache-ring-text {
|
|
781
|
+
position: absolute;
|
|
782
|
+
top: 50%;
|
|
783
|
+
left: 50%;
|
|
784
|
+
transform: translate(-50%, -50%);
|
|
785
|
+
text-align: center;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.cache-ring-value {
|
|
789
|
+
font-size: 28px;
|
|
790
|
+
font-weight: 700;
|
|
791
|
+
color: var(--accent-green);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.cache-ring-label {
|
|
795
|
+
font-size: 11px;
|
|
796
|
+
color: var(--text-secondary);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/* Page Timings Table */
|
|
800
|
+
.page-table {
|
|
801
|
+
width: 100%;
|
|
802
|
+
border-collapse: collapse;
|
|
803
|
+
font-size: 13px;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.page-table th {
|
|
807
|
+
text-align: left;
|
|
808
|
+
padding: 12px 16px;
|
|
809
|
+
background: var(--bg-tertiary);
|
|
810
|
+
color: var(--text-secondary);
|
|
811
|
+
font-weight: 500;
|
|
812
|
+
font-size: 12px;
|
|
813
|
+
text-transform: uppercase;
|
|
814
|
+
letter-spacing: 0.5px;
|
|
815
|
+
border-bottom: 1px solid var(--border-color);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.page-table td {
|
|
819
|
+
padding: 12px 16px;
|
|
820
|
+
border-bottom: 1px solid var(--border-color);
|
|
821
|
+
vertical-align: middle;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.page-table tr:hover {
|
|
825
|
+
background: var(--bg-tertiary);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.page-table .url {
|
|
829
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
830
|
+
font-size: 12px;
|
|
831
|
+
color: var(--accent-blue);
|
|
832
|
+
max-width: 400px;
|
|
833
|
+
overflow: hidden;
|
|
834
|
+
text-overflow: ellipsis;
|
|
835
|
+
white-space: nowrap;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.page-table .duration {
|
|
839
|
+
font-variant-numeric: tabular-nums;
|
|
840
|
+
font-weight: 500;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.page-table .status {
|
|
844
|
+
display: inline-flex;
|
|
845
|
+
align-items: center;
|
|
846
|
+
gap: 6px;
|
|
847
|
+
padding: 4px 10px;
|
|
848
|
+
border-radius: 20px;
|
|
849
|
+
font-size: 11px;
|
|
850
|
+
font-weight: 500;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.page-table .status.cached {
|
|
854
|
+
background: rgba(63, 185, 80, 0.15);
|
|
855
|
+
color: var(--accent-green);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.page-table .status.rendered {
|
|
859
|
+
background: rgba(88, 166, 255, 0.15);
|
|
860
|
+
color: var(--accent-blue);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/* Tabs */
|
|
864
|
+
.tabs {
|
|
865
|
+
display: flex;
|
|
866
|
+
gap: 4px;
|
|
867
|
+
padding: 8px;
|
|
868
|
+
background: var(--bg-tertiary);
|
|
869
|
+
border-radius: var(--radius);
|
|
870
|
+
margin-bottom: 16px;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.tab {
|
|
874
|
+
padding: 8px 16px;
|
|
875
|
+
border-radius: 6px;
|
|
876
|
+
font-size: 13px;
|
|
877
|
+
font-weight: 500;
|
|
878
|
+
color: var(--text-secondary);
|
|
879
|
+
background: transparent;
|
|
880
|
+
border: none;
|
|
881
|
+
cursor: pointer;
|
|
882
|
+
transition: all 0.2s ease;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.tab:hover {
|
|
886
|
+
color: var(--text-primary);
|
|
887
|
+
background: var(--bg-secondary);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.tab.active {
|
|
891
|
+
color: var(--text-primary);
|
|
892
|
+
background: var(--bg-secondary);
|
|
893
|
+
box-shadow: var(--shadow);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.tab-content {
|
|
897
|
+
display: none;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.tab-content.active {
|
|
901
|
+
display: block;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.phase-view {
|
|
905
|
+
display: none;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.phase-view.active {
|
|
909
|
+
display: block;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/* JSON View */
|
|
913
|
+
.json-view {
|
|
914
|
+
background: var(--bg-tertiary);
|
|
915
|
+
border-radius: var(--radius);
|
|
916
|
+
padding: 16px;
|
|
917
|
+
overflow-x: auto;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
.json-view pre {
|
|
921
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
922
|
+
font-size: 12px;
|
|
923
|
+
line-height: 1.6;
|
|
924
|
+
color: var(--text-primary);
|
|
925
|
+
margin: 0;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/* Incremental Section */
|
|
929
|
+
.incremental-info {
|
|
930
|
+
display: grid;
|
|
931
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
932
|
+
gap: 20px;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.trigger-file {
|
|
936
|
+
font-family: 'SF Mono', Monaco, Consolas, monospace;
|
|
937
|
+
font-size: 12px;
|
|
938
|
+
color: var(--accent-yellow);
|
|
939
|
+
background: rgba(210, 153, 34, 0.1);
|
|
940
|
+
padding: 8px 12px;
|
|
941
|
+
border-radius: 4px;
|
|
942
|
+
word-break: break-all;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/* Badges */
|
|
946
|
+
.badge {
|
|
947
|
+
display: inline-flex;
|
|
948
|
+
align-items: center;
|
|
949
|
+
gap: 4px;
|
|
950
|
+
padding: 2px 8px;
|
|
951
|
+
border-radius: 12px;
|
|
952
|
+
font-size: 11px;
|
|
953
|
+
font-weight: 500;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
.badge.ci {
|
|
957
|
+
background: rgba(163, 113, 247, 0.15);
|
|
958
|
+
color: var(--accent-purple);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.badge.local {
|
|
962
|
+
background: rgba(88, 166, 255, 0.15);
|
|
963
|
+
color: var(--accent-blue);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
.badge.detailed {
|
|
967
|
+
background: rgba(57, 197, 207, 0.15);
|
|
968
|
+
color: var(--accent-cyan);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/* Flags */
|
|
972
|
+
.flags {
|
|
973
|
+
display: flex;
|
|
974
|
+
gap: 8px;
|
|
975
|
+
flex-wrap: wrap;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.flag {
|
|
979
|
+
display: inline-flex;
|
|
980
|
+
align-items: center;
|
|
981
|
+
gap: 4px;
|
|
982
|
+
padding: 4px 10px;
|
|
983
|
+
background: var(--bg-tertiary);
|
|
984
|
+
border-radius: 4px;
|
|
985
|
+
font-size: 12px;
|
|
986
|
+
color: var(--text-secondary);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
.flag.enabled {
|
|
990
|
+
color: var(--accent-green);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.flag.disabled {
|
|
994
|
+
color: var(--text-muted);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* Tooltip */
|
|
998
|
+
[data-tooltip] {
|
|
999
|
+
position: relative;
|
|
1000
|
+
cursor: help;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
[data-tooltip]:hover::after {
|
|
1004
|
+
content: attr(data-tooltip);
|
|
1005
|
+
position: absolute;
|
|
1006
|
+
bottom: 100%;
|
|
1007
|
+
left: 50%;
|
|
1008
|
+
transform: translateX(-50%);
|
|
1009
|
+
padding: 6px 10px;
|
|
1010
|
+
background: var(--bg-tertiary);
|
|
1011
|
+
color: var(--text-primary);
|
|
1012
|
+
font-size: 12px;
|
|
1013
|
+
border-radius: 4px;
|
|
1014
|
+
white-space: nowrap;
|
|
1015
|
+
z-index: 100;
|
|
1016
|
+
box-shadow: var(--shadow);
|
|
1017
|
+
border: 1px solid var(--border-color);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/* Responsive */
|
|
1021
|
+
@media (max-width: 768px) {
|
|
1022
|
+
.container {
|
|
1023
|
+
padding: 16px;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
.phase-item {
|
|
1027
|
+
grid-template-columns: 1fr;
|
|
1028
|
+
gap: 8px;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
.phase-duration,
|
|
1032
|
+
.phase-percent {
|
|
1033
|
+
text-align: left;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.meta-info {
|
|
1037
|
+
flex-direction: column;
|
|
1038
|
+
gap: 8px;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/* Print styles */
|
|
1043
|
+
@media print {
|
|
1044
|
+
body {
|
|
1045
|
+
background: white;
|
|
1046
|
+
color: black;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
.section {
|
|
1050
|
+
break-inside: avoid;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.tab {
|
|
1054
|
+
display: none !important;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.tab-content {
|
|
1058
|
+
display: block !important;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/* Animations */
|
|
1063
|
+
@keyframes fadeIn {
|
|
1064
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
1065
|
+
to { opacity: 1; transform: translateY(0); }
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
.section {
|
|
1069
|
+
animation: fadeIn 0.3s ease-out;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.summary-card {
|
|
1073
|
+
animation: fadeIn 0.3s ease-out;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.summary-card:nth-child(2) { animation-delay: 0.05s; }
|
|
1077
|
+
.summary-card:nth-child(3) { animation-delay: 0.1s; }
|
|
1078
|
+
.summary-card:nth-child(4) { animation-delay: 0.15s; }
|
|
1079
|
+
.summary-card:nth-child(5) { animation-delay: 0.2s; }
|
|
1080
|
+
.summary-card:nth-child(6) { animation-delay: 0.25s; }
|
|
1081
|
+
</style>
|
|
1082
|
+
</head>
|
|
1083
|
+
<body>
|
|
1084
|
+
<div class="container">
|
|
1085
|
+
<!-- Header -->
|
|
1086
|
+
<header>
|
|
1087
|
+
<h1>
|
|
1088
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1089
|
+
<path d="M12 20V10M18 20V4M6 20v-4"/>
|
|
1090
|
+
</svg>
|
|
1091
|
+
Stati Build Metrics Report
|
|
1092
|
+
${hasDetailedTimings ? '<span class="badge detailed">Detailed</span>' : ''}
|
|
1093
|
+
${meta.ci ? '<span class="badge ci">CI</span>' : '<span class="badge local">Local</span>'}
|
|
1094
|
+
</h1>
|
|
1095
|
+
<div class="meta-info">
|
|
1096
|
+
<span>
|
|
1097
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1098
|
+
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
|
|
1099
|
+
</svg>
|
|
1100
|
+
${new Date(meta.timestamp).toLocaleString()}
|
|
1101
|
+
</span>
|
|
1102
|
+
<span>
|
|
1103
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1104
|
+
<path d="M4 19.5A2.5 2.5 0 016.5 17H20"/>
|
|
1105
|
+
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/>
|
|
1106
|
+
</svg>
|
|
1107
|
+
Command: ${meta.command}
|
|
1108
|
+
</span>
|
|
1109
|
+
<span>
|
|
1110
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1111
|
+
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
|
|
1112
|
+
</svg>
|
|
1113
|
+
${meta.platform} / ${meta.arch}
|
|
1114
|
+
</span>
|
|
1115
|
+
<span>
|
|
1116
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1117
|
+
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
|
1118
|
+
</svg>
|
|
1119
|
+
Node ${meta.nodeVersion}
|
|
1120
|
+
</span>
|
|
1121
|
+
<span>
|
|
1122
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1123
|
+
<path d="M20 7h-9M14 17H5M17 17a3 3 0 100-6 3 3 0 000 6zM7 7a3 3 0 100-6 3 3 0 000 6z"/>
|
|
1124
|
+
</svg>
|
|
1125
|
+
CLI v${meta.cliVersion} / Core v${meta.coreVersion}
|
|
1126
|
+
</span>
|
|
1127
|
+
${meta.gitBranch
|
|
1128
|
+
? `
|
|
1129
|
+
<span>
|
|
1130
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1131
|
+
<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/>
|
|
1132
|
+
<path d="M18 9a9 9 0 01-9 9"/>
|
|
1133
|
+
</svg>
|
|
1134
|
+
${meta.gitBranch}${meta.gitCommit ? ` (${meta.gitCommit.substring(0, 7)})` : ''}
|
|
1135
|
+
</span>`
|
|
1136
|
+
: ''}
|
|
1137
|
+
</div>
|
|
1138
|
+
${Object.keys(meta.flags).length > 0
|
|
1139
|
+
? `
|
|
1140
|
+
<div class="flags" style="margin-top: 12px;">
|
|
1141
|
+
${meta.flags.force !== undefined ? `<span class="flag ${meta.flags.force ? 'enabled' : 'disabled'}">--force: ${meta.flags.force}</span>` : ''}
|
|
1142
|
+
${meta.flags.clean !== undefined ? `<span class="flag ${meta.flags.clean ? 'enabled' : 'disabled'}">--clean: ${meta.flags.clean}</span>` : ''}
|
|
1143
|
+
${meta.flags.includeDrafts !== undefined ? `<span class="flag ${meta.flags.includeDrafts ? 'enabled' : 'disabled'}">--include-drafts: ${meta.flags.includeDrafts}</span>` : ''}
|
|
1144
|
+
</div>`
|
|
1145
|
+
: ''}
|
|
1146
|
+
</header>
|
|
1147
|
+
|
|
1148
|
+
<!-- Summary Cards -->
|
|
1149
|
+
<div class="summary-grid">
|
|
1150
|
+
<div class="summary-card highlight">
|
|
1151
|
+
<div class="label">Total Duration</div>
|
|
1152
|
+
<div class="value">${formatDuration(totals.durationMs)}</div>
|
|
1153
|
+
<div class="subtext">${(totals.durationMs / 1000).toFixed(3)} seconds</div>
|
|
1154
|
+
</div>
|
|
1155
|
+
<div class="summary-card ${counts.cachedPages > 0 ? 'success' : ''}">
|
|
1156
|
+
<div class="label">Pages</div>
|
|
1157
|
+
<div class="value">${counts.totalPages}</div>
|
|
1158
|
+
<div class="subtext">${counts.renderedPages} rendered, ${counts.cachedPages} cached</div>
|
|
1159
|
+
</div>
|
|
1160
|
+
<div class="summary-card">
|
|
1161
|
+
<div class="label">Cache Hit Rate</div>
|
|
1162
|
+
<div class="value" style="color: ${isg.cacheHitRate > 0.5 ? 'var(--accent-green)' : isg.cacheHitRate > 0 ? 'var(--accent-yellow)' : 'var(--text-secondary)'}">${formatPercent(isg.cacheHitRate)}</div>
|
|
1163
|
+
<div class="subtext">${isg.manifestEntries} manifest entries</div>
|
|
1164
|
+
</div>
|
|
1165
|
+
<div class="summary-card">
|
|
1166
|
+
<div class="label">Peak Memory</div>
|
|
1167
|
+
<div class="value">${formatBytes(totals.peakRssBytes)}</div>
|
|
1168
|
+
<div class="subtext">Heap: ${formatBytes(totals.heapUsedBytes)}</div>
|
|
1169
|
+
</div>
|
|
1170
|
+
<div class="summary-card">
|
|
1171
|
+
<div class="label">Templates Loaded</div>
|
|
1172
|
+
<div class="value">${counts.templatesLoaded}</div>
|
|
1173
|
+
<div class="subtext">${(counts.templatesLoaded / counts.totalPages).toFixed(1)} avg per page</div>
|
|
1174
|
+
</div>
|
|
1175
|
+
<div class="summary-card">
|
|
1176
|
+
<div class="label">Assets Copied</div>
|
|
1177
|
+
<div class="value">${counts.assetsCopied}</div>
|
|
1178
|
+
<div class="subtext">${counts.markdownFilesProcessed} markdown files</div>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
|
|
1182
|
+
<!-- Phase Breakdown -->
|
|
1183
|
+
<div class="section">
|
|
1184
|
+
<div class="section-header">
|
|
1185
|
+
<h2>
|
|
1186
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1187
|
+
<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
|
|
1188
|
+
</svg>
|
|
1189
|
+
Phase Breakdown
|
|
1190
|
+
</h2>
|
|
1191
|
+
<span style="font-size: 13px; color: var(--text-secondary);">
|
|
1192
|
+
${phaseData.length} phases tracked • ${formatDuration(totalPhaseTime)} total
|
|
1193
|
+
</span>
|
|
1194
|
+
</div>
|
|
1195
|
+
<div class="section-content">
|
|
1196
|
+
<!-- View Tabs -->
|
|
1197
|
+
<div class="tabs" style="margin-bottom: 20px;">
|
|
1198
|
+
<button class="tab active" onclick="switchPhaseView('duration')">
|
|
1199
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
|
|
1200
|
+
<path d="M3 3v18h18"/><path d="M18 9l-5 5-4-4-3 3"/>
|
|
1201
|
+
</svg>
|
|
1202
|
+
By Duration
|
|
1203
|
+
</button>
|
|
1204
|
+
<button class="tab" onclick="switchPhaseView('timeline')">
|
|
1205
|
+
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="margin-right: 6px;">
|
|
1206
|
+
<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>
|
|
1207
|
+
</svg>
|
|
1208
|
+
Timeline
|
|
1209
|
+
</button>
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
<!-- Duration View (sorted by duration) -->
|
|
1213
|
+
<div id="duration-view" class="phase-view active">
|
|
1214
|
+
<p style="font-size: 12px; color: var(--text-muted); margin-bottom: 16px;">
|
|
1215
|
+
Click on any phase to see a detailed description of what happens during that phase.
|
|
1216
|
+
</p>
|
|
1217
|
+
<div class="phase-list">
|
|
1218
|
+
${phaseChartData
|
|
1219
|
+
.map((phase) => `
|
|
1220
|
+
<div class="phase-item" onclick="togglePhase(this)">
|
|
1221
|
+
<div class="phase-name" title="${phase.name}">${phase.label}</div>
|
|
1222
|
+
<div class="phase-bar-container">
|
|
1223
|
+
<div class="phase-bar" style="width: ${Math.max(phase.percent, 0.5)}%"></div>
|
|
1224
|
+
</div>
|
|
1225
|
+
<div class="phase-duration">${formatDuration(phase.value)}</div>
|
|
1226
|
+
<div class="phase-percent">${phase.percent.toFixed(1)}%</div>
|
|
1227
|
+
<svg class="phase-expand-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1228
|
+
<polyline points="6,9 12,15 18,9"/>
|
|
1229
|
+
</svg>
|
|
1230
|
+
<div class="phase-description">${phase.description}</div>
|
|
1231
|
+
</div>`)
|
|
1232
|
+
.join('')}
|
|
1233
|
+
</div>
|
|
1234
|
+
</div>
|
|
1235
|
+
|
|
1236
|
+
<!-- Timeline View (in execution order) -->
|
|
1237
|
+
<div id="timeline-view" class="phase-view" style="display: none;">
|
|
1238
|
+
<p style="font-size: 12px; color: var(--text-muted); margin-bottom: 16px;">
|
|
1239
|
+
Phases shown in their approximate execution order during the build process.
|
|
1240
|
+
</p>
|
|
1241
|
+
|
|
1242
|
+
<!-- Visual Timeline Track -->
|
|
1243
|
+
<div class="timeline-track">
|
|
1244
|
+
${timelineChartData
|
|
1245
|
+
.map((phase, i) => `
|
|
1246
|
+
<div class="timeline-segment phase-color-${i % 8}"
|
|
1247
|
+
style="left: ${phase.startOffset}%; width: ${Math.max(phase.percent, 0.5)}%;"
|
|
1248
|
+
title="${phase.label}: ${formatDuration(phase.value)} (${phase.percent.toFixed(1)}%)"
|
|
1249
|
+
onclick="expandTimelineItem(${i})">
|
|
1250
|
+
<span>${phase.percent > 8 ? phase.label : ''}</span>
|
|
1251
|
+
</div>`)
|
|
1252
|
+
.join('')}
|
|
1253
|
+
</div>
|
|
1254
|
+
|
|
1255
|
+
<!-- Timeline List -->
|
|
1256
|
+
<div class="timeline-list">
|
|
1257
|
+
${timelineChartData
|
|
1258
|
+
.map((phase, i) => `
|
|
1259
|
+
<div class="timeline-item" id="timeline-item-${i}" onclick="toggleTimeline(this)">
|
|
1260
|
+
<div class="timeline-order">${i + 1}</div>
|
|
1261
|
+
<div class="timeline-name">${phase.label}</div>
|
|
1262
|
+
<div class="timeline-bar-container">
|
|
1263
|
+
<div class="timeline-bar phase-color-${i % 8}" style="width: ${Math.max(phase.percent, 0.5)}%"></div>
|
|
1264
|
+
</div>
|
|
1265
|
+
<div class="timeline-time">${formatDuration(phase.value)}</div>
|
|
1266
|
+
<div class="timeline-description">${phase.description}</div>
|
|
1267
|
+
</div>`)
|
|
1268
|
+
.join('')}
|
|
1269
|
+
</div>
|
|
1270
|
+
</div>
|
|
1271
|
+
</div>
|
|
1272
|
+
</div>
|
|
1273
|
+
|
|
1274
|
+
<!-- ISG Cache Details -->
|
|
1275
|
+
<div class="section">
|
|
1276
|
+
<div class="section-header">
|
|
1277
|
+
<h2>
|
|
1278
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1279
|
+
<path d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"/>
|
|
1280
|
+
</svg>
|
|
1281
|
+
ISG Cache Performance
|
|
1282
|
+
</h2>
|
|
1283
|
+
<span style="font-size: 13px; color: ${isg.enabled ? 'var(--accent-green)' : 'var(--text-muted)'};">
|
|
1284
|
+
${isg.enabled ? '● Enabled' : '○ Disabled'}
|
|
1285
|
+
</span>
|
|
1286
|
+
</div>
|
|
1287
|
+
<div class="section-content">
|
|
1288
|
+
<div style="display: flex; gap: 40px; align-items: center; flex-wrap: wrap;">
|
|
1289
|
+
<div class="cache-ring-container">
|
|
1290
|
+
<div class="cache-ring">
|
|
1291
|
+
<svg width="150" height="150" viewBox="0 0 150 150">
|
|
1292
|
+
<circle class="cache-ring-bg" cx="75" cy="75" r="60"/>
|
|
1293
|
+
<circle class="cache-ring-progress" cx="75" cy="75" r="60"
|
|
1294
|
+
stroke-dasharray="${2 * Math.PI * 60}"
|
|
1295
|
+
stroke-dashoffset="${2 * Math.PI * 60 * (1 - isg.cacheHitRate)}"/>
|
|
1296
|
+
</svg>
|
|
1297
|
+
<div class="cache-ring-text">
|
|
1298
|
+
<div class="cache-ring-value">${formatPercent(isg.cacheHitRate)}</div>
|
|
1299
|
+
<div class="cache-ring-label">Cache Hit Rate</div>
|
|
1300
|
+
</div>
|
|
1301
|
+
</div>
|
|
1302
|
+
</div>
|
|
1303
|
+
<div class="isg-grid" style="flex: 1;">
|
|
1304
|
+
<div class="isg-stat hit">
|
|
1305
|
+
<div class="value">${counts.cachedPages}</div>
|
|
1306
|
+
<div class="label">Pages from Cache</div>
|
|
1307
|
+
</div>
|
|
1308
|
+
<div class="isg-stat miss">
|
|
1309
|
+
<div class="value">${counts.renderedPages}</div>
|
|
1310
|
+
<div class="label">Pages Rendered</div>
|
|
1311
|
+
</div>
|
|
1312
|
+
<div class="isg-stat">
|
|
1313
|
+
<div class="value" style="color: var(--accent-purple)">${isg.manifestEntries}</div>
|
|
1314
|
+
<div class="label">Manifest Entries</div>
|
|
1315
|
+
</div>
|
|
1316
|
+
<div class="isg-stat">
|
|
1317
|
+
<div class="value" style="color: var(--accent-orange)">${isg.invalidatedEntries}</div>
|
|
1318
|
+
<div class="label">Invalidated</div>
|
|
1319
|
+
</div>
|
|
1320
|
+
</div>
|
|
1321
|
+
</div>
|
|
1322
|
+
</div>
|
|
1323
|
+
</div>
|
|
1324
|
+
|
|
1325
|
+
${hasIncremental
|
|
1326
|
+
? `
|
|
1327
|
+
<!-- Incremental Rebuild -->
|
|
1328
|
+
<div class="section">
|
|
1329
|
+
<div class="section-header">
|
|
1330
|
+
<h2>
|
|
1331
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1332
|
+
<path d="M23 4v6h-6M1 20v-6h6"/>
|
|
1333
|
+
<path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/>
|
|
1334
|
+
</svg>
|
|
1335
|
+
Incremental Rebuild
|
|
1336
|
+
</h2>
|
|
1337
|
+
</div>
|
|
1338
|
+
<div class="section-content">
|
|
1339
|
+
<div class="incremental-info">
|
|
1340
|
+
<div class="stat-item">
|
|
1341
|
+
<div class="stat-label">Trigger File</div>
|
|
1342
|
+
<div class="trigger-file">${incremental.triggerFile}</div>
|
|
1343
|
+
</div>
|
|
1344
|
+
<div class="stat-item">
|
|
1345
|
+
<div class="stat-label">Trigger Type</div>
|
|
1346
|
+
<div class="stat-value">${incremental.triggerType}</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
<div class="stat-item">
|
|
1349
|
+
<div class="stat-label">Rebuild Duration</div>
|
|
1350
|
+
<div class="stat-value">${formatDuration(incremental.durationMs)}</div>
|
|
1351
|
+
</div>
|
|
1352
|
+
<div class="stat-item">
|
|
1353
|
+
<div class="stat-label">Pages Affected</div>
|
|
1354
|
+
<div class="stat-value">${incremental.renderedPages} rendered / ${incremental.cachedPages} cached</div>
|
|
1355
|
+
</div>
|
|
1356
|
+
</div>
|
|
1357
|
+
</div>
|
|
1358
|
+
</div>`
|
|
1359
|
+
: ''}
|
|
1360
|
+
|
|
1361
|
+
${hasDetailedTimings
|
|
1362
|
+
? `
|
|
1363
|
+
<!-- Detailed Page Timings -->
|
|
1364
|
+
<div class="section">
|
|
1365
|
+
<div class="section-header">
|
|
1366
|
+
<h2>
|
|
1367
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1368
|
+
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/>
|
|
1369
|
+
<path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/>
|
|
1370
|
+
</svg>
|
|
1371
|
+
Page Timings
|
|
1372
|
+
</h2>
|
|
1373
|
+
<span style="font-size: 13px; color: var(--text-secondary);">
|
|
1374
|
+
${sortedPageTimings.length} pages • Sorted by duration
|
|
1375
|
+
</span>
|
|
1376
|
+
</div>
|
|
1377
|
+
<div class="section-content">
|
|
1378
|
+
<div class="tabs">
|
|
1379
|
+
<button class="tab active" onclick="switchTab('table')">Table View</button>
|
|
1380
|
+
<button class="tab" onclick="switchTab('waterfall')">Waterfall View</button>
|
|
1381
|
+
</div>
|
|
1382
|
+
|
|
1383
|
+
<div id="tab-table" class="tab-content active">
|
|
1384
|
+
<div style="overflow-x: auto;">
|
|
1385
|
+
<table class="page-table">
|
|
1386
|
+
<thead>
|
|
1387
|
+
<tr>
|
|
1388
|
+
<th>URL</th>
|
|
1389
|
+
<th>Duration</th>
|
|
1390
|
+
<th>Status</th>
|
|
1391
|
+
<th>Templates</th>
|
|
1392
|
+
</tr>
|
|
1393
|
+
</thead>
|
|
1394
|
+
<tbody>
|
|
1395
|
+
${sortedPageTimings
|
|
1396
|
+
.map((page) => `
|
|
1397
|
+
<tr>
|
|
1398
|
+
<td class="url" title="${page.url}">${page.url}</td>
|
|
1399
|
+
<td class="duration">${formatDuration(page.durationMs)}</td>
|
|
1400
|
+
<td><span class="status ${page.cached ? 'cached' : 'rendered'}">${page.cached ? '● Cached' : '◐ Rendered'}</span></td>
|
|
1401
|
+
<td>${page.templatesLoaded !== undefined ? page.templatesLoaded : '-'}</td>
|
|
1402
|
+
</tr>`)
|
|
1403
|
+
.join('')}
|
|
1404
|
+
</tbody>
|
|
1405
|
+
</table>
|
|
1406
|
+
</div>
|
|
1407
|
+
</div>
|
|
1408
|
+
|
|
1409
|
+
<div id="tab-waterfall" class="tab-content">
|
|
1410
|
+
<div class="waterfall-container">
|
|
1411
|
+
<div class="waterfall">
|
|
1412
|
+
${(() => {
|
|
1413
|
+
const maxDuration = Math.max(...sortedPageTimings.map((p) => p.durationMs));
|
|
1414
|
+
return sortedPageTimings
|
|
1415
|
+
.slice(0, 50)
|
|
1416
|
+
.map((page) => {
|
|
1417
|
+
const widthPercent = (page.durationMs / maxDuration) * 100;
|
|
1418
|
+
return `
|
|
1419
|
+
<div class="waterfall-row">
|
|
1420
|
+
<div class="waterfall-label" title="${page.url}">${page.url.split('/').pop() || page.url}</div>
|
|
1421
|
+
<div class="waterfall-track">
|
|
1422
|
+
<div class="waterfall-bar ${page.cached ? 'cached' : 'rendered'}" style="width: ${Math.max(widthPercent, 1)}%"></div>
|
|
1423
|
+
</div>
|
|
1424
|
+
<div class="waterfall-time">${formatDuration(page.durationMs)}</div>
|
|
1425
|
+
</div>`;
|
|
1426
|
+
})
|
|
1427
|
+
.join('');
|
|
1428
|
+
})()}
|
|
1429
|
+
${sortedPageTimings.length > 50
|
|
1430
|
+
? `
|
|
1431
|
+
<div style="text-align: center; padding: 12px; color: var(--text-muted); font-size: 12px;">
|
|
1432
|
+
Showing 50 of ${sortedPageTimings.length} pages
|
|
1433
|
+
</div>`
|
|
1434
|
+
: ''}
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>`
|
|
1440
|
+
: ''}
|
|
1441
|
+
|
|
1442
|
+
<!-- Raw JSON -->
|
|
1443
|
+
<div class="section">
|
|
1444
|
+
<div class="section-header">
|
|
1445
|
+
<h2>
|
|
1446
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
1447
|
+
<path d="M16 18l6-6-6-6M8 6l-6 6 6 6"/>
|
|
1448
|
+
</svg>
|
|
1449
|
+
Raw JSON Data
|
|
1450
|
+
</h2>
|
|
1451
|
+
<button onclick="copyJson()" style="background: var(--bg-secondary); border: 1px solid var(--border-color); color: var(--text-secondary); padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;">
|
|
1452
|
+
Copy to Clipboard
|
|
1453
|
+
</button>
|
|
1454
|
+
</div>
|
|
1455
|
+
<div class="section-content">
|
|
1456
|
+
<div class="json-view">
|
|
1457
|
+
<pre id="json-content">${jsonData.replace(/</g, '<').replace(/>/g, '>')}</pre>
|
|
1458
|
+
</div>
|
|
1459
|
+
</div>
|
|
1460
|
+
</div>
|
|
1461
|
+
|
|
1462
|
+
<!-- Footer -->
|
|
1463
|
+
<footer style="text-align: center; padding: 24px; color: var(--text-muted); font-size: 12px;">
|
|
1464
|
+
Generated by Stati Build Metrics • ${new Date().toISOString()}
|
|
1465
|
+
</footer>
|
|
1466
|
+
</div>
|
|
1467
|
+
|
|
1468
|
+
<script>
|
|
1469
|
+
function switchTab(tabName) {
|
|
1470
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
1471
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
1472
|
+
document.querySelector(\`[onclick="switchTab('\${tabName}')"]\`).classList.add('active');
|
|
1473
|
+
document.getElementById(\`tab-\${tabName}\`).classList.add('active');
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function switchPhaseView(viewName) {
|
|
1477
|
+
// Update tabs
|
|
1478
|
+
document.querySelectorAll('.tabs .tab').forEach(t => t.classList.remove('active'));
|
|
1479
|
+
document.querySelector(\`[onclick="switchPhaseView('\${viewName}')"]\`).classList.add('active');
|
|
1480
|
+
|
|
1481
|
+
// Update views
|
|
1482
|
+
document.querySelectorAll('.phase-view').forEach(v => {
|
|
1483
|
+
v.style.display = 'none';
|
|
1484
|
+
v.classList.remove('active');
|
|
1485
|
+
});
|
|
1486
|
+
const activeView = document.getElementById(\`\${viewName}-view\`);
|
|
1487
|
+
if (activeView) {
|
|
1488
|
+
activeView.style.display = 'block';
|
|
1489
|
+
activeView.classList.add('active');
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function togglePhase(element) {
|
|
1494
|
+
element.classList.toggle('expanded');
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
function toggleTimeline(element) {
|
|
1498
|
+
element.classList.toggle('expanded');
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
function expandTimelineItem(index) {
|
|
1502
|
+
const item = document.getElementById(\`timeline-item-\${index}\`);
|
|
1503
|
+
if (item) {
|
|
1504
|
+
// Collapse others first
|
|
1505
|
+
document.querySelectorAll('.timeline-item.expanded').forEach(el => {
|
|
1506
|
+
if (el !== item) el.classList.remove('expanded');
|
|
1507
|
+
});
|
|
1508
|
+
item.classList.add('expanded');
|
|
1509
|
+
item.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function copyJson() {
|
|
1514
|
+
const json = document.getElementById('json-content').textContent;
|
|
1515
|
+
navigator.clipboard.writeText(json).then(() => {
|
|
1516
|
+
const btn = event.target;
|
|
1517
|
+
const originalText = btn.textContent;
|
|
1518
|
+
btn.textContent = 'Copied!';
|
|
1519
|
+
btn.style.color = 'var(--accent-green)';
|
|
1520
|
+
setTimeout(() => {
|
|
1521
|
+
btn.textContent = originalText;
|
|
1522
|
+
btn.style.color = '';
|
|
1523
|
+
}, 2000);
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
</script>
|
|
1527
|
+
</body>
|
|
1528
|
+
</html>`;
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Write the HTML metrics report to a file.
|
|
1532
|
+
*
|
|
1533
|
+
* @param metrics - The build metrics data
|
|
1534
|
+
* @param outputPath - Path to write the HTML file
|
|
1535
|
+
* @returns Promise resolving to success boolean and path
|
|
1536
|
+
*/
|
|
1537
|
+
export async function writeMetricsHtml(metrics, outputPath) {
|
|
1538
|
+
try {
|
|
1539
|
+
const html = generateMetricsHtml(metrics);
|
|
1540
|
+
await writeFile(outputPath, html, 'utf-8');
|
|
1541
|
+
return { success: true, path: outputPath };
|
|
1542
|
+
}
|
|
1543
|
+
catch (error) {
|
|
1544
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1545
|
+
return { success: false, error: `Failed to write HTML report: ${errorMessage}` };
|
|
1546
|
+
}
|
|
1547
|
+
}
|